Artigo Java Magazine 36 - Acessando Código Nativo com JNI
Artigos Java Artigo Java Magazine 36 - Acessando Código Nativo com JNI
A tecnologia JNI (Java Native Interface) permite integrar o Java com aplicações criadas em outras linguagens de programação, tornando possível a invocação de métodos ou funções em ambas as direções. Neste artigo explicaremos os principais conceitos da JNI e como criar uma aplicação totalmente funcional. Abordaremos também a biblioteca JENIE, uma solução para desenvolver aplicações JNI sem necessidade de escrever código C/C++.
Conceitos básicos de JNI
Um exemplo prático: exclusão de arquivos
Uma alternativa: JENIE
No desenvolvimento de aplicações complexas, é comum nos deparamos com situações em que o Java oferece limitações. A implementação de algumas soluções exige uma abordagem híbrida, pois o software em questão não pode ser desenvolvido utilizando somente recursos do Java. Alguns exemplos típicos incluem:
Implementação de código em baixo nível, para acesso direto ao hardware;
Acesso a código legado a partir de aplicações Java;
Acesso a código Java a partir de aplicações legadas;
Acesso a funcionalidades dependentes da plataforma, e que não são suportadas pelas bibliotecas padrão do Java.
Como veremos ao longo desse artigo, através da JNI é possível tratar todas essas situações. A JNI permite que o código que executa dentro da JVM trabalhe em conjunto com aplicações e bibliotecas desenvolvidas em outras linguagens, como C/C++, Delphi e Assembly.
Utilizando JNI é possível escrever partes da aplicação em código nativo[1] e acessá-las diretamente do Java, como se os métodos em questão tivessem sido escritos em Java "puro". Também é possível executar o processo inverso: acessar funções Java diretamente do código nativo.
O uso de JNI evita duas soluções típicas: reimplementação de aplicações (o que nem sempre é viável) e chamadas ao método Rutime.exec() (que só podem feitas se a aplicação a ser chamada for um executável). Diversas classes do Java SE utilizam métodos nativos, a exemplo das classes dos pacotes java.io e java.net, que executam chamadas ao sistema operacional.
A JNI é uma solução robusta e consolidada, e parte integrante do Java SE. Porém, no desenvolvimento de aplicações híbridas (Java + código nativo), é importante considerar os riscos envolvidos. O primeiro ponto que merece destaque é que aplicações JNI não são totalmente portáveis, pois, ainda que o código nativo da aplicação seja recompilado sempre que a plataforma de implantação mudar, o uso de APIs proprietárias pode inviabilizar a migração.
O segundo ponto ao qual devemos estar atentos ao utilizar uma solução híbrida é que, como o código nativo executa fora da JVM, não existem garantias de segurança para a parte nativa (que deve ser tratada separadamente). Assim, mais um cuidado deve ser considerado, pois um método nativo com comportamento indesejável pode corromper toda a aplicação.
Uma boa prática quando se desenvolve soluções JNI é isolar os métodos nativos em poucas classes, diminuindo o impacto sobre o resto da aplicação.
Usando JNI, aplicações Java fazem chamadas a código nativo contido em bibliotecas (.dll no Windows e .so no Linux), enquanto as aplicações nativas carregam a JVM e chamam métodos disponíveis nas classes Java. Neste artigo abordaremos apenas as chamadas a métodos nativos, feitas a partir de classes Java, visto que nosso foco é nessa linguagem. Usaremos exemplos em C/C++ pois, depois do Java, talvez essas sejam as linguagens que mais possuem linhas de código escritas e a necessidade de integrá-las com Java é natural.
Como veremos adiante, a JNI exige que as funções de bibliotecas que serão chamadas a partir do Java sigam uma regra especial de nomenclatura. Assim, não será possível chamar diretamente funções de uma biblioteca existente, que não foi projetada de acordo com o padrão exigido pela JNI. Neste caso será necessário criar uma biblioteca "wrapper", que forneça as nomenclaturas corretas e chame as funções correspondentes da biblioteca legada.
Para disparar código C/C++ a partir do Java são necessários seis passos:
Codificar a classe Java que contém o método nativo. Essa classe, além de declarar o método nativo, é responsável por carregar a biblioteca (DLL) que contém a implementação nativa.
Compilar o código Java.
Criar o arquivo de cabeçalho (header file) a partir do bytecode Java criado anteriormente. O arquivo de cabeçalho declara o método nativo que será executado e usado na compilação da DLL.
Escrever o código da DLL, ou seja, desenvolver a função nativa que implementa a funcionalidade desejada (ou chama a aplicação que implementa essa funcionalidade). A assinatura da função deve obedecer à assinatura declarada no arquivo de cabeçalho.
Compilar a DLL.
Executar o programa (a biblioteca é carregada durante a execução).
Declaração do método nativo nas duas linguagens
Um método nativo é declarado numa classe Java de forma semelhante a um método abstrato. Apenas a assinatura é declarada, ou seja, o método não contém corpo. Por exemplo: