Escrevendo métodos nativos em Java com JNI e JNA
Métodos Nativos em Java: JNI e JNA
Imagine que você tenha duas aplicações (uma escrita em C e outra em Java) e você precisa fazer a integração entre as duas. Existem várias formas de se fazer isto, como escrever um Web Service, troca de arquivos, bancos de dados compartilhados e etc, como vemos no curso SOA na prática.
Se você quiser apenas usar um método de uma biblioteca já existente, existe outra possibilidade, podemos fazer o Java executar esse código diretamente, através de uma chamada nativa.
Vamos testar duas alternativas para usar código nativo em Java, o JNI (Java Native Interface) e o JNA (Java Native API). Como código de teste, vamos escrever um método em C que efetua a soma de dois números.
Vamos começar com JNI. Ele é uma especificação do Java que permite a chamada de métodos em linguagem nativa através da palavra chave native
.
Primeiramente precisamos colocar a assinatura do método que queremos com o modificador native
:
public class CalculadoraJNI { //declaração do método nativo public native int soma(int num1, int num2); }
Depois é necessário pedir para que a JVM carregue a biblioteca que contém o código em C. Isso é feito usando o método loadLibrary
da classe System
:
public class CalculadoraJNI { public native int soma(int num1, int num2); //Bloco estático para carregar a biblioteca "somador" static{ System.loadLibrary("somador"); } }
Vamos escrever uma classe para testar nossa calculadora:
public class TestaCalculadoraJNI { public static void main(String\[\] args) { CalculadoraJNI calc = new CalculadoraJNI();
int num1 = Integer.parseInt(args\[0\]); int num2 = Integer.parseInt(args\[1\]);
int resultado = calc.soma(num1, num2); System.out.println("A soma é: " + resultado); } }
Para compilar o código, vamos rodar o comando javac
:
javac CalculadoraJNI.java javac TestaCalculadoraJNI.java
Serão gerados os arquivos CalculadoraJNI.class
e TestaCalculadoraJNI.class
.
E para executar, o comando java
:
java TestaCalculadoraJNI 2 3
Mas ao executarmos, aparece o erro abaixo:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no somador in java.library.path at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860) at java.lang.Runtime.loadLibrary0(Runtime.java:845) at java.lang.System.loadLibrary(System.java:1084) at CalculadoraJNI.(CalculadoraJNI.java:6) at TestaCalculadoraJNI.main(TestaCalculadoraJNI.java:3)
Este erro ocorre pois ainda não criamos a nossa biblioteca chamada de "somador". Vamos criá-la.
Para o JNI conseguir chamar o código existente é necessário que a assinatura do método em C seja equivalente a do método declarado como native
em Java. Além disso, precisamos usar um conjunto de tipos específicos do JNI para fazer a conversão entre as duas linguagens.
Dentro do JDK existe uma ferramenta chamada javah
que já gera essa assinatura para que não precisemos nos preocupar com esses detalhes, basta passar como parâmetro a classe que possui o método nativo:
javah CalculadoraJNI
Será gerado um arquivo chamado CalculadoraJNI.h
, que contém a assinatura do método e os tipos equivalentes:
/\* DO NOT EDIT THIS FILE - it is machine generated \*/ #include <stdio.h> /\* Header for class CalculadoraJNI \*/
#ifndef \_Included\_CalculadoraJNI #define \_Included\_CalculadoraJNI #ifdef \_\_cplusplus extern "C" { #endif /\* \* Class: Calculadora \* Method: soma \* Signature: (II)I \*/ JNIEXPORT jint JNICALL Java\_CalculadoraJNI\_soma(JNIEnv \*, jobject, jint, jint);
#ifdef \_\_cplusplus } #endif #endif
No arquivo somadorJNI.c
vamos colocar nossa implementação da calculadora. Não podemos nos esquecer de importar o arquivo gerado anteriormente.
#include <stdio.h> #include "CalculadoraJNI.h"
/\* \* Método que executa a soma \*/ int soma(int num1, int num2){ int resultado = num1 + num2; return resultado; }
/\* \* Método com a mesma assinatura do CalculadoraJNI.h \*/ JNIEXPORT jint JNICALL Java\_Calculadora\_soma(JNIEnv \* env, jobject jobj, jint num1, jint num2){ //chamada ao método da soma :P return soma(num1, num2); }
Para compilar, utilizaremos o gcc, que é o compilador padrão do Linux. Precisaremos também passar o caminho para os arquivos de header do JNI (jni.h e jni_md.h) como parâmetro. Esses arquivos geralmente estão no diretório de instalação da JDK. Uma outra restrição é que o nome da biblioteca compilada deve começar com "lib", seguido do nome que escolhemos. O comando final será parecido com o seguinte:
gcc -o libsomador.so -shared -I/caminho/para/jdk/headers somadorJNI.c
Será gerado o arquivo libsomador.so
no mesmo diretório. Vamos tentar rodar novamente o TestaCalculadoraJNI
e ver o resultado:
java TestaCalculadoraJNI 2 3 A soma é: 5
Pronto! implementamos um método usando C, e o executamos diretamente do Java!
Apesar de ser uma das maneiras mais usadas internamente na JVM para executar código nativo, ainda precisamos poluir o código em C com as chamadas e tipos específicos do JNI.
Uma alternativa menos invasiva é usar o JNA, que é uma biblioteca que abstrai de nós a complexidade de lidar diretamente com o código e as chamadas do JNI, tanto do lado do Java quanto no C. Vamos ver como fica o mesmo método mas agora usando JNA.
Nossa implementação da calculadora é a mesma de antes, mas sem nenhum código estranho.
#include <stdio.h>
int soma(int num1, int num2){ int resultado = num1 + num2; return resultado; }
Vamos compilar o código em C :
gcc -o libsomadorJNA.so -shared somadorJNA.c
Precisamos baixar o jar que pode ser encontrado no repositório oficial do JNA.
Para invocar o código em C diretamente do Java, precisamos criar uma interface que representará a nossa biblioteca nativa. Esta interface deve herdar de com.sun.jna.Library
:
import com.sun.jna.Library;
public interface CalculadoraJNA extends Library { public int soma(int num1, int num2); }
Para usar a Calculadora, vamos carregar a biblioteca e fazer o binding com a interface:
import com.sun.jna.Native;
public class TestaCalculadoraJNA {
public static void main(String\[\] args) { CalculadoraJNA calculadora = (CalculadoraJNA) Native.loadLibrary("somadorJNA", CalculadoraJNA.class);
int num1 = Integer.parseInt(args\[0\]); int num2 = Integer.parseInt(args\[1\]);
int resultado = calculadora.soma(num1, num2); System.out.println("A soma é: " + resultado); } }
Para compilar nosso código Java, precisamos adicionar o jar da jna no classpath:
javac -cp jna-4.0.0.jar CalculadoraJNA.java javac -cp jna-4.0.0.jar:. TestaCalculadoraJNA.java
Agora podemos executar:
java -classpath jna-4.0.0.jar:. TestaCalculadoraJNA 2 3 A soma é: 5
O resultado é o mesmo, porém o JNA abstrai a complexidade do JNI e o código em C fica totalmente independente do Java.
OBS: Para gerar os arquivos compilados, devemos seguir o padrão de cada sistema operacional. Para MacOSx a lib gerada deve ter a extensão dylib
e para Windows a extensão é dll
.
Vimos aqui duas maneiras de usar código nativo em Java. Qual você achou mais simples? Já precisou usar alguma delas? Conhece uma maneira diferente?