Guia para JNI (Java Native Interface)

Guia para JNI (Java Native Interface)

1. Introdução

Como sabemos, um dos principais pontos fortes do Java é sua portabilidade - o que significa que, uma vez que escrevemos e compilamos o código, o resultado desse processo é um bytecode independente da plataforma.

Simplificando, isso pode ser executado em qualquer máquina ou dispositivo capaz de executar uma Java Virtual Machine e funcionará da maneira mais fácil possível.

No entanto, às vezeswe do actually need to use code that’s natively-compiled for a specific architecture.

Pode haver alguns motivos para a necessidade de usar código nativo:

  • A necessidade de lidar com algum hardware

  • Melhoria de desempenho para um processo muito exigente

  • Uma biblioteca existente que queremos reutilizar em vez de reescrevê-la em Java.

To achieve this, the JDK introduces a bridge between the bytecode running in our JVM and the native code (geralmente escrito em C ou C ++).

A ferramenta é chamada Java Native Interface. Neste artigo, veremos como é escrever código com ele.

2. Como funciona

2.1. Métodos nativos: a JVM encontra o código compilado

Java fornece a palavra-chavenative que é usada para indicar que a implementação do método será fornecida por um código nativo.

Normalmente, ao criar um programa executável nativo, podemos optar por usar bibliotecas estáticas ou compartilhadas:

  • Bibliotecas estáticas - todos os binários da biblioteca serão incluídos como parte do nosso executável durante o processo de vinculação. Assim, não precisaremos mais das libs, mas isso aumentará o tamanho do nosso arquivo executável.

  • Bibliotecas compartilhadas - o executável final possui apenas referências às bibliotecas, não ao código em si. Requer que o ambiente em que executamos nosso executável tenha acesso a todos os arquivos das bibliotecas usadas por nosso programa.

O último é o que faz sentido para JNI, já que não podemos misturar bytecode e código compilado nativamente no mesmo arquivo binário.

Portanto, nossa biblioteca compartilhada manterá o código nativo separadamente em seu arquivo.so/.dll/.dylib (dependendo de qual sistema operacional estamos usando) em vez de fazer parte de nossas classes.

A palavra-chavenative transforma nosso método em uma espécie de método abstrato:

private native void aNativeMethod();

Com a principal diferença de queinstead of being implemented by another Java class, it will be implemented in a separated native shared library.

Uma tabela com ponteiros na memória para a implementação de todos os nossos métodos nativos será construída para que eles possam ser chamados a partir do nosso código Java.

2.2. Componentes Necessários

Aqui está uma breve descrição dos principais componentes que precisamos levar em consideração. Iremos explicá-los mais tarde neste artigo

  • Código Java - nossas aulas. Eles incluirão pelo menos um métodonative.

  • Código nativo - a lógica real de nossos métodos nativos, geralmente codificados em C ou C ++.

  • Arquivo de cabeçalho JNI - este arquivo de cabeçalho para C / C ++ (include/jni.h no diretório JDK) inclui todas as definições de elementos JNI que podemos usar em nossos programas nativos.

  • C/C++ Compiler – we can choose between GCC, Clang, Visual Studio, or any other we like as far as it’s able to generate a native shared library for our platform.

2.3. Elementos JNI no código (Java e C / C ++)

Elementos Java:

  • Palavra-chave "nativa" - como já cobrimos, qualquer método marcado como nativo deve ser implementado em uma biblioteca nativa compartilhada.

  • System.loadLibrary(String libname) - um método estático que carrega uma biblioteca compartilhada do sistema de arquivos na memória e disponibiliza suas funções exportadas para nosso código Java.

C/C++ elements (many of them defined within jni.h)

  • JNIEXPORT- marca a função na lib compartilhada como exportável, portanto ela será incluída na tabela de funções e, portanto, a JNI poderá encontrá-la

  • JNICALL - combinado comJNIEXPORT, garante que nossos métodos estejam disponíveis para o framework JNI

  • JNIEnv - uma estrutura que contém métodos que podemos usar nosso código nativo para acessar elementos Java

  • JavaVM - uma estrutura que nos permite manipular uma JVM em execução (ou mesmo iniciar uma nova) adicionando threads a ela, destruindo-a, etc.

3. Hello World JNI

A seguir,let’s look at how JNI works in practice.

Neste tutorial, usaremos C como a linguagem nativa e G como compilador e vinculador.

Podemos usar qualquer outro compilador de nossa preferência, mas veja como instalar o G ++ no Ubuntu, Windows e MacOS:

  • Ubuntu Linux - execute o comando“sudo apt-get install build-essential” em um terminal

  • Windows -Install MinGW

  • MacOS - execute o comando“g++” em um terminal e, se ainda não estiver presente, ele o instalará.

3.1. Criação da classe Java

Vamos começar a criar nosso primeiro programa JNI implementando um clássico “Hello World”.

Para começar, criamos a seguinte classe Java que inclui o método nativo que executará o trabalho:

package com.example.jni;

public class HelloWorldJNI {

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

    public static void main(String[] args) {
        new HelloWorldJNI().sayHello();
    }

    // Declare a native method sayHello() that receives no arguments and returns void
    private native void sayHello();
}

Como podemos ver,we load the shared library in a static block. Isso garante que estará pronto quando precisarmos e de onde for necessário.

Alternativamente, neste programa trivial, podemos carregar a biblioteca antes de chamar nosso método nativo, porque não estamos usando a biblioteca nativa em nenhum outro lugar.

3.2. Implementando um método em C ++

Agora, precisamos criar a implementação do nosso método nativo em C ++.

Em C ++, a definição e a implementação são normalmente armazenadas em arquivos.he.cpp, respectivamente.

Primeiro,to create the definition of the method, we have to use the -h flag of the Java compiler:

javac -h . HelloWorldJNI.java

Isso irá gerar um arquivocom_example_jni_HelloWorldJNI.h com todos os métodos nativos incluídos na classe passados ​​como parâmetro, neste caso, apenas um:

JNIEXPORT void JNICALL Java_com_example_jni_HelloWorldJNI_sayHello
  (JNIEnv *, jobject);

Como podemos ver, o nome da função é gerado automaticamente usando o pacote totalmente qualificado, a classe e o nome do método.

Além disso, algo interessante que podemos notar é que estamos recebendo dois parâmetros passados ​​para nossa função; um ponteiro para oJNIEnv; atual e também o objeto Java ao qual o método está anexado, a instância de nossa classeHelloWorldJNI.

Agora, temos que criar um novo arquivo.cpp para a implementação da funçãosayHello. This is where we’ll perform actions that print “Hello World” to console.

Nomearemos nosso arquivo.cpp com o mesmo nome do arquivo .h que contém o cabeçalho e adicionaremos este código para implementar a função nativa:

JNIEXPORT void JNICALL Java_com_example_jni_HelloWorldJNI_sayHello
  (JNIEnv* env, jobject thisObject) {
    std::cout << "Hello from C++ !!" << std::endl;
}

3.3. Compilando e vinculando

Neste ponto, temos todas as peças de que precisamos no lugar e temos uma conexão entre elas.

Precisamos construir nossa biblioteca compartilhada a partir do código C ++ e executá-lo!

Para fazer isso, temos que usar o compilador G ++,not forgetting to include the JNI headers from our Java JDK installation.

Versão do Ubuntu:

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o

Versão do Windows:

g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o

Versão MacOS;

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o

Assim que tivermos o código compilado para nossa plataforma no arquivocom_example_jni_HelloWorldJNI.o, temos que incluí-lo em uma nova biblioteca compartilhada. Whatever we decide to name it is the argument passed into the method System.loadLibrary.

Chamamos o nosso de “nativo” e vamos carregá-lo ao executar nosso código Java.

O vinculador G então vincula os arquivos de objeto C à nossa biblioteca em ponte.

Versão do Ubuntu:

g++ -shared -fPIC -o libnative.so com_example_jni_HelloWorldJNI.o -lc

Versão do Windows:

g++ -shared -o native.dll com_example_jni_HelloWorldJNI.o -Wl,--add-stdcall-alias

Versão do MacOS:

g++ -dynamiclib -o libnative.dylib com_example_jni_HelloWorldJNI.o -lc

E é isso!

Agora podemos executar nosso programa na linha de comando.

No entanto,we need to add the full path to the directory containing the library we’ve just generated. Desta forma, o Java saberá onde procurar por nossas bibliotecas nativas:

java -cp . -Djava.library.path=/NATIVE_SHARED_LIB_FOLDER com.example.jni.HelloWorldJNI

Saída do console:

Hello from C++ !!

4. Usando recursos avançados de JNI

Dizer olá é bom, mas não muito útil. Usually, we would like to exchange data between Java and C++ code and manage this data in our program.

4.1. Adicionando parâmetros aos nossos métodos nativos

Vamos adicionar alguns parâmetros aos nossos métodos nativos. Vamos criar uma nova classe chamadaExampleParametersJNI com dois métodos nativos usando parâmetros e retornos de diferentes tipos:

private native long sumIntegers(int first, int second);

private native String sayHelloToMe(String name, boolean isFemale);

E repita o procedimento para criar um novo arquivo .h com "javac -h", como fizemos anteriormente.

Agora crie o arquivo .cpp correspondente com a implementação do novo método C ++:

...
JNIEXPORT jlong JNICALL Java_com_example_jni_ExampleParametersJNI_sumIntegers
  (JNIEnv* env, jobject thisObject, jint first, jint second) {
    std::cout << "C++: The numbers received are : " << first << " and " << second << std::endl;
    return (long)first + (long)second;
}
JNIEXPORT jstring JNICALL Java_com_example_jni_ExampleParametersJNI_sayHelloToMe
  (JNIEnv* env, jobject thisObject, jstring name, jboolean isFemale) {
    const char* nameCharPointer = env->GetStringUTFChars(name, NULL);
    std::string title;
    if(isFemale) {
        title = "Ms. ";
    }
    else {
        title = "Mr. ";
    }

    std::string fullName = title + nameCharPointer;
    return env->NewStringUTF(fullName.c_str());
}
...

Usamos o ponteiro*env do tipoJNIEnv para acessar os métodos fornecidos pela instância do ambiente JNI.

JNIEnv nos permite, neste caso, passar JavaStrings em nosso código C ++ e voltar sem nos preocupar com a implementação.

Podemos verificar a equivalência de tipos Java e tipos C JNI emOracle official documentation.

Para testar nosso código, temos que repetir todas as etapas de compilação do exemploHelloWorld anterior.

4.2. Usando objetos e chamando métodos Java a partir de código nativo

Neste último exemplo, vamos ver como podemos manipular objetos Java em nosso código C ++ nativo.

Começaremos criando uma nova classeUserData que usaremos para armazenar algumas informações do usuário:

package com.example.jni;

public class UserData {

    public String name;
    public double balance;

    public String getUserInfo() {
        return "[name]=" + name + ", [balance]=" + balance;
    }
}

Em seguida, criaremos outra classe Java chamadaExampleObjectsJNI com alguns métodos nativos com os quais gerenciaremos objetos do tipoUserData:

...
public native UserData createUser(String name, double balance);

public native String printUserData(UserData user);

Mais uma vez, vamos criar o cabeçalho.h e então a implementação C ++ de nossos métodos nativos em um novo arquivo.cpp:

JNIEXPORT jobject JNICALL Java_com_example_jni_ExampleObjectsJNI_createUser
  (JNIEnv *env, jobject thisObject, jstring name, jdouble balance) {

    // Create the object of the class UserData
    jclass userDataClass = env->FindClass("com/example/jni/UserData");
    jobject newUserData = env->AllocObject(userDataClass);

    // Get the UserData fields to be set
    jfieldID nameField = env->GetFieldID(userDataClass , "name", "Ljava/lang/String;");
    jfieldID balanceField = env->GetFieldID(userDataClass , "balance", "D");

    env->SetObjectField(newUserData, nameField, name);
    env->SetDoubleField(newUserData, balanceField, balance);

    return newUserData;
}

JNIEXPORT jstring JNICALL Java_com_example_jni_ExampleObjectsJNI_printUserData
  (JNIEnv *env, jobject thisObject, jobject userData) {

    // Find the id of the Java method to be called
    jclass userDataClass=env->GetObjectClass(userData);
    jmethodID methodId=env->GetMethodID(userDataClass, "getUserInfo", "()Ljava/lang/String;");

    jstring result = (jstring)env->CallObjectMethod(userData, methodId);
    return result;
}

Novamente, estamos usando o ponteiroJNIEnv *env para acessar as classes, objetos, campos e métodos necessários da JVM em execução.

Normalmente, precisamos fornecer o nome completo da classe para acessar uma classe Java ou o nome e a assinatura corretos do método para acessar um método de objeto.

Estamos até criando uma instância da classecom.example.jni.UserData em nosso código nativo. Once we have the instance, we can manipulate all its properties and methods in a way similar to Java reflection.

Podemos verificar todos os outros métodos deJNIEnv emOracle official documentation.

4. Desvantagens do uso de JNI

A ponte JNI tem suas armadilhas.

A principal desvantagem é a dependência da plataforma subjacente; Recursowe essentially lose the “write once, run anywhere” do Java. Isso significa que teremos que construir uma nova lib para cada nova combinação de plataforma e arquitetura que desejamos oferecer suporte. Imagine o impacto que isso poderia ter no processo de criação se suportássemos Windows, Linux, Android, MacOS ...

O JNI não apenas adiciona uma camada de complexidade ao nosso programa. It also adds a costly layer of communication entre o código em execução na JVM e nosso código nativo: precisamos converter os dados trocados em ambas as maneiras entre Java e C ++ em um processo de empacotamento / desempacotamento.

Às vezes não há nem mesmo uma conversão direta entre os tipos, então teremos que escrever nosso equivalente.

5. Conclusão

Compilar o código para uma plataforma específica (normalmente) torna-o mais rápido do que executar o bytecode.

Isso o torna útil quando precisamos acelerar um processo exigente. Além disso, quando não temos outras alternativas, como quando precisamos usar uma biblioteca que gerencia um dispositivo.

No entanto, isso tem um preço, pois teremos que manter um código adicional para cada plataforma que oferecemos suporte.

É por isso que geralmente é uma boa ideiaonly use JNI in the cases where there’s no Java alternative.

Como sempre, o código deste artigo está disponívelover on GitHub.