Interface Nativa Java

Muito se fala do Java, principalmente do fato dele ser multiplataforma, talvez essa característica em especial, de se compilar uma vez e poder rodar em diversas plataformas, tornou o Java a linguagem e plataforma mais popular no mundo. Muito se tem explorado já que toda a complexidade é feita pela JVM, mas surge a seguinte dúvida: Como a JVM faz isso? Como ela conseguir abstrair as outras plataformas para min? Obter esse conhecimento é muito importante uma vez que pode ser necessário utilizar um recurso específico da máquina e fazer com esse recurso converse diretamente com a JVM.

A comunicação entre a JVM e o código nativo quase em sua grande maioria é feito na linguagem C/C++ uma vez que elas possuem o padrão ANSI e que o mesmo é compatível com a grande maioria das plataformas, assim é possível um grande aproveitamento de código, mas em alguns casos é necessário que seja feita uma implementação específica para cada plataforma ou que existe compatibilidade com um sistema legado que foi feito em C, por exemplo. A porta entre o código nativo e o Java é conhecido como JNI (Java Native Interface). Esse recurso é muito interessante também para fazer a ponte para plataformas em que o Java ainda não atingiu. Dentro da JVM esse recurso é usado para alguns na plataforma JSE, por exemplo, Java I/O, alguns métodos da classe java.lang.System, JavaSound, a comunicação entre a classe, o arquivo .class, e a sua representação dentro da JVM, a implementação da interface java.lang.Class, as implementações do Garbage Colector dentre outros recursos. Com o JNI é possível chamar método do objeto, instanciar classes, verificar estado do objeto criado dentro da JVM, dentre outros recursos.

No entanto, usar o requer algumas responsabilidades, vale lembrar que usar o JNI perde a portabilidade, um erro nativo não é controlado pela JVM (Vale lembrar na parte em que se falou de registrados, a pilha nativa e o PC quando aponta para um processo nativo, não se sabe o seu valor preciso), não é possível debugar o código nativo através da plataforma Java, caso acontece um erro nativo pode quebrar a execução da JVM, ele não prover um Garbage Collector automático ou qualquer gerenciamento por parte da JVM. Assim é muito importante saber o momento em que se usará o JNI.

Os objetos em Java podem ser mapeados para objetos nativos e virse-versa, para garantir a comunicação de duas mãos, assim é possível estar passando um objeto Java para o lado nativo ver o seu valor.

Tipo em Java Tipo Nativo
boolean jboolean
byte jbyte
char jchar
double jdouble
float jfloat
int jint
long jlong
short jshort
void void

Com o objetivo de dar um pequeno exemplo com o JNI será mostrado dois simples exemplos, o primeiro será o “olá mundo” com o JNI e o segundo será um método estático que calcula o dobro do resultado passado, para isso é necessário que se tenha instalado o GCC e o JDK. O código será bem simples, no primeiro caso será enviado o nome por parâmetro e essa String será passada para o valor nativo, uma vez no nativo será concatenado o “Hello world” com o nome digitado, no segundo exemplo, o segundo parâmetro seria calculado o seu dobro.

Primeiramente será criado a classe HelloWorld.java.

public class HelloWorld { 

    private native void chamarMensagem(String nome); 

    public native static int dobrar(int valor); 

    public static void main(String[] args) { 

        String nome=args[0]==null?"nome":args[0]; 
        int valor=args[1]==null?2:Integer.valueOf(args[1]); 

        HelloWorld helloWorld=new HelloWorld(); 
        helloWorld.chamarMensagem(nome); 

        int resultado=HelloWorld.dobrar(valor); 
        System.out.println("O dobro de "+valor+" é: "+ resultado); 
    } 

    static {System.loadLibrary("HelloWorld");} 

}

Em seguida compilamos com o seguinte comando:

  • javac HelloWorld.java

Uma vez o arquivo compilado, será necessário gerar a interface JNI como seguinte comando:

  • javah -jni HelloWorld

Com o comando será gerado um arquivo HelloWorld.h.

/* DO NOT EDIT THIS FILE - it is machine generated */ 
#include <jni.h> 
/* Header for class HelloWorld */ 

#ifndef _Included_HelloWorld 
#define _Included_HelloWorld 
#ifdef __cplusplus 
extern "C" { 
#endif 
/* 
 * Class:     HelloWorld 
 * Method:    chamarMensagem 
 * Signature: (Ljava/lang/String;)V 
 */ 
JNIEXPORT void JNICALL Java_HelloWorld_chamarMensagem 
  (JNIEnv *, jobject, jstring); 

/* 
 * Class:     HelloWorld 
 * Method:    dobro 
 * Signature: (I)I 
 */ 
JNIEXPORT jint JNICALL Java_HelloWorld_dobrar 
  (JNIEnv *, jclass, jint); 

#ifdef __cplusplus 
} 
#endif 
#endif

Repare que na interface o método possui o seguinte formato:

Java_NomeClasse_nomeMetodo. Em relação aos parâmetros o primeiro elemento, o JNIEnv, ele é um ponteiro que aponta para um vetor no qual possui todas as funções do JNI, o segundo depende se é método da classe ou da instância. Caso seja estático, ou seja o método possua a palavra-chave static, o próximo parâmetro será o jclass que conterá as informações da classe, caso seja da instância o próximo parâmetro será o jobject que conterá as informações da instância.

O próximo passo é a criação do arquivo que “implemente” a interface do HelloWorld.h, assim será criado o HelloWorld.c que implemente tal interface.

#include <jni.h>  
#include "HelloWorld.h" 
#include <stdio.h>  

JNIEXPORT void JNICALL Java_HelloWorld_chamarMensagem(JNIEnv * env, jobject obj,jstring nome){ 
    const char * nomeNativo=(*env)->GetStringUTFChars(env, nome, NULL);  
    printf("Hello World!!!!  %s\n", nomeNativo);  
    return;  
} 

JNIEXPORT jint JNICALL Java_HelloWorld_dobrar(JNIEnv * env, jclass classe, jint valor){ 

    return 2*valor; 
}

Com o arquivo criado o próximo passo é a compilação, levando em consideração as devidas importações, como se trata de libs nativas as pastas variam de acordo com a plataforma. No caso do linux para compilar será necessário o seguinte comando:

  • gcc -o libHelloWorld.so -shared -I$JAVA_HOME/include -I$JAVA_HOME/linux HelloWorld.c

Uma vez compilado o código-fonte e o transformado em uma lib, no caso do linux o arquivo com extensão .so de Shared Object. O próximo passo será “linkar” o arquivo nativo com o projeto, o primeiro passo é carregar a biblioteca dentro do código java (para isso será utilizado o comando System.loadLibrary("NomedaLib");).

O próximo passo é colocar a lib nativa no classpath no sistema operacional ou definir o seu caminho pelo parâmetro java.library.path ao executar o projeto java. Nesse exemplo será utilizado a segunda opção juntamente o parâmetro que será o nome da que será impresso no console, assim o comando ficará:

  • java -Djava.library.path=. HelloWorld Otávio 4

A saída será:

  • Hello World!!!! Otávio
  • `O dobro de 5 é: 10

Com isso se apresentou o recurso do JNI, a interface que se comunica o JVM para linguagens nativa como C e C++ e da sua importância para a JVM como a implementação do Garbage Collector, sua existência em algumas APIs como o JavaSound além de se integrar com código legado e com plataformas cujo a JVM até o momento não atingiu. No entanto, vale salientar que se perde o fator multiplataforma e não será mais gerenciado pela JVM usando este recurso. Aprender sobre JNI é muito importante para compreender o código da máquina virtual Java, mas é necessário um conhecimento na linguagem C e C++.`