Escrevendo métodos nativos em Java com 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 😛
    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?

Tags: , , , ,

19 Comentários

  1. Rafael 25/03/2014 at 14:25 #

    Tem diferença de performance?

  2. Joviane Jardim 25/03/2014 at 21:16 #

    Rafael,

    Sim, tem diferença de performance. Apesar de o JNA ser mais simples, ele perde em performance para o JNI.

  3. Henrique Mota Esteves 25/03/2014 at 21:36 #

    Uma outra boa alternativa é o projeto JNR (https://github.com/jnr/jnr-ffi). Este projeto vem sendo utilizado pelo projeto JRuby.

  4. Roberto Shizuo 26/03/2014 at 17:21 #

    excelente post mais uma vez! pessoal da caelum detona em java

  5. Arthur Gomes 27/03/2014 at 23:01 #

    Muito bom…

  6. Klin 29/03/2014 at 15:06 #

    Bem especificamente na plataforma java apresenta diferença de performance,mas na plataforma android foi otimizado de uma maneira incrível.O desempenho é equivalente.

  7. Ramon 28/08/2014 at 09:19 #

    E para chamar o método de uma dll já existente? O modo de se fazer é modificado?
    Quero chamar o método de uma dll em VB para o Java.

  8. Mário Amaral 28/08/2014 at 13:05 #

    Olá Ramon

    Não é muito diferente não, você só tem que saber mapear os tipos. Tem um exemplo de algo parecido neste link: http://blog.mwrobel.eu/how-to-call-dll-methods-from-java/

  9. Facundo 28/11/2014 at 16:04 #

    Esse exemplos funcionam a partir de que versão de Java?

  10. Mário Amaral 28/11/2014 at 16:12 #

    Oi Facundo

    JNI existe desde o java 1.2, quanto ao JNA não encontrei na página do projeto nenhuma indicação de versão mínima, se fosse para chutar diria que Java 5, mas você teria que testar em uma versão mais antiga para ter certeza.

  11. Jefferson 07/08/2015 at 22:14 #

    Olá, segui os passos do tutorial e mesmo após gerar o “libsomador.so” (gcc -o libsomador.so -shared -I C:\Java\jdk1.8.0_45\include somadorJNI.c) continua dando o erro:
    [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)]

    O que poderia estar causando esse erro?

    Obs:Como “jni_md.h” em um diretório diferente “C:\Java\jdk1.8.0_45\include\win32” foi necessário copiá-lo para o diretório pai, o mesmo diretório de “jni.h”

    Obrigado!

  12. Juliana 09/11/2015 at 12:31 #

    Adorei as dicas, vou recomendar seu site, esse é realmente um assunto que gera muitas dúvidas

  13. Felipe 29/02/2016 at 11:42 #

    Jeferson,

    Estava com o mesmo problema que você. No meu caso foi o padrão de nome do arquivo C compilado gerado. Como meu ambiente é windows precisei gerar o arquivo sem lib e com .DLL. Veja se resolve pra você. Abs.

  14. Lázaro Boldrini Pandolfi 01/05/2016 at 11:41 #

    Bom dia a todos.

    Primeiramente quero parabenizar pelo post. Deste já agradeço toda ajuda. Pesquisei na internet e verifiquei que com jna é possível criar um objeto e mudar seu atributos por referencia. Exemplo: tenho um programa java em execução com um objeto instanciado, também tenho um método que modifica seus atributos por referencia. É possível que um programa/processo em c linux consiga encontrar e guardar em um ponteiro o endereço dos atributos desse objeto instanciado no java(isso se existe um ponteiro para esses atributos quando o objeto é instanciado)?

  15. Leandro 28/07/2017 at 14:44 #

    Muito bom Joviane,

    legal ver mulheres mandando tb bem tecnicamente.

  16. Diego D. M 15/08/2017 at 12:44 #

    Uma dúvida. Existem DLL’s tipo da Bematech, Epson de comunicação que eram utilizadas em impressoras fiscais. Com estas DLL’s era possível em qualquer linguagem fazer a integração com a impressora fiscal. Minha dúvida. Como eu programar em JAVA um método e criar uma DLL que qualquer linguagem chame, execute o método JAVA e devolva o resultado? Assim consigo fazer um programa JAVA que funcione com qualquer linguagem através de uma DLL. Obrigado.

  17. Igor Ribeiro da Fonseca 26/10/2018 at 18:29 #

    Boa tarde, poderiam explicar melhor a parte da compilação com gcc? desculpa a ignorância é que estou usando o post de vocês para me auxiliar no projeto de iniciação cientifica na faculdade e sou muito leigo ainda na area de programação e como podem perceber, estou precisando de ajuda, aguardo resposta, grato!

  18. Joviane Jardim 26/10/2018 at 23:14 #

    Oi Igor, tudo bom?

    Como geramos um arquivo .c, ele precisa ser compilado para que a JVM consiga executá-lo. O gcc é o compilador padrão para arquivos escritos em C, e no momento da compilação é gerado o arquivo executável.

    No comando gcc -o libsomador.so -shared -I/caminho/para/jdk/headers somadorJNI.c o parâmetro -o serve para informarmos o nome do arquivo de saída (“ouptut”), o parâmetro -shared serve para gerar um objeto compartilhado e linkar com as bibliotecas necessárias, o parâmetro -I serve para especificarmos o diretório que tem os arquivos de header (.h) e por último vem o nome do arquivo.c a ser compilado, no nosso caso o somadorJNI.c

    Ficou mais claro?

    Abraços!

  19. igor 01/11/2018 at 02:23 #

    Joviane você é 10 demais me ajudou bastante expliacando mais detalhadamente, porém esta dando erro quando executo o comando com a seguinte mensagem “no such file or directory”, estou preso nesse problema ainda …

Deixe uma resposta