Artigo Java Magazine 36 - Acessando Código Nativo com JNI

Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Para efetuar o download você precisa estar logado. Clique aqui para efetuar o login
Confirmar voto
0
 (0)  (1)

Artigo Publicado pela Java Magazine 36.

Esse artigo faz parte da revista Java Magazine edição 36. Clique aqui para ler todos os artigos desta edição

jm36.jpg

Acessando Código Nativo com JNI

Primeiros passos com a Java Native Interface

Aprenda passo a passo a utilizar a tecnologia JNI e comunique seus aplicativos Java com aplicações C/C++

André Dantas Rocha

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++.

Introdução

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.

Conceitos básicos de JNI

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:

1.        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.

2.        Compilar o código Java.

3.        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.

4.        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.

5.        Compilar a DLL.

6.        Executar o programa (a biblioteca é carregada durante a execução).

Entendendo as etapas

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:

 

private native void metodoNativo();

 

Este código indica à JVM que a implementação de metodoNativo() não se encontra na classe Java e deve ser localizada externamente. Para que a ligação entre o Java e o código nativo seja feita corretamente, e a JVM encontre a função[2] que deve ser executada, a JNI define regras de nomenclatura que devem ser obedecidas do lado do C/C++. Para o método definido anteriormente, e supondo que a classe Java é br.com.jm.ClasseExemplo, temos a seguinte nomenclatura em C/C++:

 

JNIEXPORT void JNICALL

    Java_br_com_jm_ClasseExemplo_metodoNativo(

        JNIEnv *env, jobject obj);

 

As assinaturas do método nativo em Java e da função em C/C++ são bastante diferentes, mas o entendimento não é difícil. No C/C++, as construções JNIEXPORT e JNICALL definem as convenções para exportação e nomenclatura da função de acordo com o sistema operacional (não há porque se preocupar com elas. Essas duas “palavras” sempre farão parte da assinatura de uma função nativa).

O retorno da função (neste caso, void) é um tipo C/C++ que possui correspondência direta com um tipo Java (os tipos da JNI são detalhados a seguir). O nome da função sempre inicia com a palavra Java, seguida de um sublinhado (_), o nome completo da classe (com pontos substituídos por sublinhados), e depois outro sublinhado e o nome do método.

Em adição aos parâmetros declarados no método nativo da classe Java, as funções em C/C++ possuem dois parâmetros: um ponteiro para uma estrutura chamada JNIEnv e um jobject[3]. A estrutura JNIEnv (Figura 1) aponta para uma tabela contendo ponteiros para diversas funções JNI. De maneira simplificada podemos dizer que o JNIEnv é um tipo, que contém diversas funções utilitárias que podem ser chamadas de dentro do código nativo.

O parâmetro jobject, por sua vez, referencia o objeto Java corrente, permitindo a manipulação de objetos Java a partir do código C/C++.

Codificação da biblioteca

A assinatura do método nativo sempre é declarada num arquivo de header[4] e referenciada no arquivo que implementa a DLL. Arquivos de header possuem extensão .h, enquanto a implementação da DLL está contida em arquivos .c ou .cpp (supondo o uso de C/C++).

Como vimos, a nomenclatura dos métodos nativos em Java deve obedecer a uma regra cujos detalhes são fáceis de esquecer. No entanto, através do aplicativo javah (disponível no JDK) podemos gerar o arquivo de header diretamente a partir do .class da classe com o método nativo, evitando assim os erros de um trabalho manual.

Para gerar o arquivo ClasseExemplo.h, contendo a assinatura do método nativo da classe de exemplo, basta executar este comando:

 

javah -jni -o ClasseExemplo.h br.com.jm.ClasseExemplo

 

Execução

Após a criação do arquivo de header e a implementação do código da DLL, só falta um detalhe para que o exemplo possa ser executado. Antes de o método nativo ser chamado, é necessário carregar a biblioteca que contém sua implementação. A carga da biblioteca é feita através do método System.loadLibrary(), e é uma boa prática chamá-lo na inicialização da classe, como mostrado no código a seguir:

 

static {

  System.loadLibrary("BibliotecaNativa");

}

 

O parâmetro indica ao Java que a biblioteca de nome BibliotecaNativa.dll deve ser carregada. Para que a biblioteca seja encontrada pela JVM é necessário definir corretamente o(s) caminho(s) onde as bibliotecas nativas residem, ou será lançada uma exceção do tipo java.lang.UnsatisfiedLinkError.

No Windows, a JVM tentará localizar as bibliotecas no diretório corrente ou em algum dos diretóritos do PATH. Caso seja necessário especificar um outro diretório, é possível indicá-lo através da propriedade de sistema java.library.path, como mostrado a seguir (a opção –D seta uma propriedade de sistema):

 

java -Djava.library.path=. br.com.jm.ClasseExemplo

 

Mapeamento de tipos

Em aplicações híbridas, na maioria das vezes é necessário passar parâmetros entre o código Java e o código nativo e dele receber resultados. A JNI define um mapeamento entre tipos Java e tipos C/C++, para permitir que parâmetros e retornos de funções trafeguem de forma “transparente” entre as linguagens.

Como sabemos, a linguagem Java possui basicamente dois tipos de dados: primitivos (int, float etc.) e de referência (objetos, arrays etc.). Na JNI a correspondência entre tipos primitivos é direta, como mostrado na Tabela 1.

Os tipos de referência são mapeados para código nativo de acordo com uma hierarquia de tipos, como apresentado na Tabela 2. Todas as referências são do tipo jobject, sendo especializadas em subtipos que correspondem às referências mais comuns utilizadas no Java.

Em JNI todas as referências são passadas aos métodos nativos como referências opacas (ponteiros que referenciam objetos Java). Como veremos na próxima seção, esses objetos sempre são manipulados via funções específicas da JNI, disponíveis na estrutura JNIEnv.

Um exemplo prático: exclusão de arquivos

Agora que os conceitos básicos da JNI foram apresentados, partiremos para implementação de um exemplo real. Na aplicação apresentada a seguir melhoraremos uma das funcionalidades da classe java.io.File: a exclusão de arquivos.

Atualmente a execução do método File.delete() em plataformas Windows exclui o arquivo permanentemente, quando o desejável seria apenas enviá-lo para a lixeira. Esse comportamento será provido pela nossa aplicação (como o exemplo envolve chamadas a funções específicas da API do Windows, a aplicação será compatível apenas com esse sistema operacional).

Na codificação do exemplo utilizaremos duas abordagens distintas. Primeiro a implementação será feita manualmente, codificando a DLL e executado a chamada ao método nativo via JNI. Depois uma segunda alternativa será implementada através da biblioteca JENIE (servertec.com/products/jenie), que encapsula chamadas à JNI. A Figura 2 mostra a estrutura de pacotes e as classes que serão usadas para as duas soluções.

Começaremos implementando a solução mais complexa: a exclusão de arquivos através de JNI. Apesar de não ser essencial o conhecimento em C, noções básicas dessa linguagem ajudarão no entendimento.

Implementação da classe Java

O primeiro passo da solução consiste em implementar a classe Java que contém o método nativo e carregará a biblioteca. Como a exclusão de arquivos será implementada de duas formas distintas, foi criada a interface Exclusao (Listagem 1), que deve ser obedecida por cada uma das “estratégias” de exclusão.

A Listagem 2 exibe o código da classe ExclusaoJNI, que implementa a interface Exclusao e representa a estratégia de exclusão via JNI. A classe é bastante simples. Ela contém apenas um bloco static, que garante que a DLL será carregada assim que a classe iniciar, e a declaração do método nativo excluir() que obedece à assinatura definida na interface Exclusao.

É importante observar que o modificador native não faz parte da declaração do método excluir() na interface "

A exibição deste artigo foi interrompida :(
Este post está disponível para assinantes MVP

 
Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Receba nossas novidades
Ficou com alguma dúvida?