Acesso ao código nativo usando JNA (Java Native Access)

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
 (4)  (0)

Veja nesse artigo como utilizar a biblioteca JNA (Java Native Access) para acesso a funções nativas, sem necessitar usar a JNI (Java Native Interface), que é extremamente complexa e exige do programador conhecimentos adicionais de C/C++.

Introdução

O ecossistema Java provê centenas de frameworks e API´s para as mais diversas finalidades, sendo essa uma das características que torna Java uma das linguagens mais utilizadas do mundo. Porém, existem certas situações que podem exigir que os desenvolvedores acessem código nativo para realizar determinadas tarefas. Os principais motivos são:

  • Reescrever código legado (de outra linguagem) leva tempo, e talvez fosse mais interessante acessá-lo diretamente;
  • Questões de desempenho;
  • Acesso a drivers e API´s fornecidos através de DLL´s e Shared Libraries;
  • Não há biblioteca similar em Java.

Para isso, a Oracle disponibiliza a API JNI, que permite acessar código nativo a partir de aplicações Java. O problema do JNI é que ele é extremamente complexo, exige do programador certo conhecimento de C/C++, além de ser algo realmente enfadonho de se programar.

Felizmente, existe uma alternativa a altura, o framework JNA (Java Native Access), que abstrai as questões mais complexas do JNI e torna a integração entre código nativo e Java muito mais leve e produtiva.

Funcionamento da JNA

Configurando o JNA

Os códigos desse artigo podem rodar tanto no Netbeans como no Eclipse. A versão do JNA a ser utilizada é a 3.3.0.

Links: http://java.net/projects/jna/downloads/directory/3.3.0.

Jars: jna.jar e plataform.jar.

O jna.jar provê toda a infraestrutura necessária para acesso a código nativo, enquanto o plataform.jar provê diversas facilidades, como mapeadores para funcionalidades amplamente usados em várias plataformas, como funções Win32, X11, etc.

Nesse artigo trabalharemos com o Linux, mas os conceitos abordados servem para o sistema Windows também.

Acessando funções nativas em Linux

No Linux (será utilizado o Ubuntu 12.04 – 32 bits), faremos uso do compilador gcc para compilar um programa que faz uso das funções printf e atoi. (se o gcc não estiver instalado, é só fazer uso do bom e velho apt-get).

Listagem 1: Hello World em C usando printf e atoi

#include 

int main() {

  int ano = atoi("2012");
  printf("Hello [%s] [%d] \n", "World", ano);
  return 0;
}

Listagem 2: Para compilar e executar o programa

 - Compilando e gerando o executável helloWorld:
 gcc -o helloWorld HelloWorld.c
 
 - Executando:
 ./helloWorld

 Saída: 
  Hello [World] [2012] 

Nota: printf - utilizado para formatar e imprimir diversos tipos de dado
atoi – utilizado para converter string (terminadas com NULL) em número inteiro

Veremos agora como utilizar as funções printf e atoi diretamente no código Java.

Nota: Os exemplos abaixo não rodaram corretamente utilizando o OpenJDK. Foi necessário instalar a versão 1.6.0_37 do jdk da Oracle (32-bits).

Listagem 3: Interface CLibrary

package br.com.devmedia.jna;

import com.sun.jna.Library;

public interface CLibrary extends Library {
    void printf(String format, Object... args);
    int atoi(String value);
}

Toda função nativa que desejamos usar, no nosso caso printf e atoi, deve ser declarada numa interface que estende com.sun.jna.Library do JNA. Isso é pre-requisito para que o JNA consiga descobrir e carregar essas funções internamente.

Observe como são declarados os protótipos das funções em C e os métodos correspondentes em Java:

Listagem 4: Métodos em Java

    void printf(String format, Object... args);
    int atoi(String value);

Listagem 5: Métodos em C

    int printf (__const char *__restrict __format, ...); - arquivo /usr/include/stdio.h
    int atoi (__const char *__nptr); - arquivo /usr/include/stdio.h

Podemos fazer as seguintes associações:

JavaC
Stringconst char*
Object......
intint

Nota: apesar de retornar int, podemos ignorar o valor de retorno de printf. Por isso foi usado o void na declaração do método em Java.

Na documentação do JNA, encontra-se a tabela de mapeamento de tipos entre Java e C:

Listagem 6: Classe CLibraryFunctions

package br.com.devmedia.jna;

import com.sun.jna.Native;

public final class CLibraryFunctions {
    
    private CLibrary cLibraryInstance;
    
    public CLibraryFunctions() {
        cLibraryInstance = (CLibrary)Native.loadLibrary("c", CLibrary.class);
    }

    public void printf(String format, Object... args) {
        cLibraryInstance.printf(format, args);
    }    
    
    public int atoi(String data) {
        return cLibraryInstance.atoi(data);
    }    
}

Depois de declarar as funções nativas a serem utilizadas, devemos informar ao JNA qual biblioteca dinâmica possue as funções. Para isso utilizamos a classe Native.

A classe Native possue o método loadLibrary, que recebe como argumentos o nome da lib (Shared Library) que queremos usar e a interface com os métodos que queremos acessar dentro da Shared Library (definidas na classe CLibrary). O retorno dela é um Object, o que obriga a conversão de tipo para a interface CLibrary.

A lib utilizada foi a libc.so (especificamente no meu caso: /lib/i386-linux-gnu/libc.so.6). O método loadLibrary utiliza uma convenção. Ao passarmos qualquer nome de Shared Library no primeiro argumento, ela irá adicionar o prefixo "lib" e o sufixo ".so" no nome, portanto ao passar "c" como argumento, temos: "libc.so".

A título de curiosidade, podemos verificar se a libc possue os métodos prinft e atoi. Para isso, basta executar o seguinte comando:

nm -D /lib/i386-linux-gnu/libc.so.6 | egrep -ie " printf|atoi"

Cuja saída é:

00031620 T atoi
0004ced0 T printf
0004c6f0 T printf_size
0004ce70 T printf_size_info

Uma vez recuperado o objeto (que implementa a interface passada como argumento), podemos utilizá-lo para chamar as funções nativas.

Listagem 7: Classe JNAHelloWorld

package br.com.devmedia.jna;

public class JNAHelloWorld {

    public static void main(String[] args) {
        CLibraryFunctions cLib = new CLibraryFunctions();
        int ano = cLib.atoi("2012");
        cLib.printf("Hello [%s] [%d]", "World", ano);
    }
}

Listagem 8: Saída da classe JNAHelloWorld

Hello [World] [2012]

Temos então a saída da execução do programa, o que demonstra que realmente acessamos as funções printf e atoi da libc.so.

Trabalhando com funções que usam Callback

Várias funções em C recebem como parâmetro um ponteiro de função, que atua muitas vezes como uma função de callback (similiar aos eventos do Java). Veremos como declarar funções callback no JNA.

Listagem 9: Capturando o CTRL+Z

#include 
#include 
#include 

void handler_SIGTSTP(int sig)
{
   printf("\nCTRL+Z pressionado\n");
   exit(1);
}
 
int main(void)
{   
   signal(SIGTSTP, handler_SIGTSTP);
   
   for( ; ; ) {
     // Loop infinito
   }
}

Compilando: gcc -o signal signal.c

Executando: ./signal

A função signal do C permite que sinais do Linux (CTRL+Z, CTRL+C, sinais enviados pelo comando kill, etc) sejam capturados e manipulados pelo programa. Para isso basta passar como argumento o sinal que vai ser interceptado e a função de callback que será executada quando o sinal vier (A constante SIGTSTP têm valor 20, e indica que queremos capturar o sinal de CTRL+Z).

O programa ficará executando o laço até que o comando CTRL+Z seja pressionado. Quando isso acontece, a função handler_ SIGTSTP é chamada, é feito um print na tela e depois o programa é finalizado.

Agora veremos como utilizar a função signal em Java.

Listagem 10: Classe ClibraryFunctions com signal

package br.com.devmedia.jna;

import com.sun.jna.Callback;
import com.sun.jna.Library;

public interface CLibrary extends Library {

    void printf(String format, Object... args);
    int atoi(String value);
    
    public interface SignalFunction extends Callback {
        void invoke(int signal);
    }
    SignalFunction signal(int signal, SignalFunction func);    
}

Listagem 11: Classe CLibraryFunctionss

package br.com.devmedia.jna;

import com.sun.jna.Native;

public final class CLibraryFunctions {
   
    private CLibrary cLibraryInstance;
    
    public static int SIGTSTP = 20;
    
    public CLibraryFunctions() {
        cLibraryInstance = (CLibrary)Native.loadLibrary("c", CLibrary.class);
    }

    public void printf(String format, Object... args) {
        cLibraryInstance.printf(format, args);
    }    
    
    public int atoi(String data) {
        return cLibraryInstance.atoi(data);
    }    
    
    public void signal(int signal, CLibrary.SignalFunction sf) {
        cLibraryInstance.signal(signal, sf);
    }        
}

Listagem 12: Classe de teste

package br.com.devmedia.jna;

public class JNAHelloWorld {
    
    public static void main(String[] args) {
        final CLibraryFunctions cLib = new CLibraryFunctions();
        int ano = cLib.atoi("2012");
        cLib.printf("Hello [%s] [%d]\n", "World", ano);
        
        // Registrando "listener" para o CTRL+Z
        cLib.signal(CLibraryFunctions.SIGTSTP, new CLibrary.SignalFunction() {
            @Override
            public void invoke(int signal) {
                cLib.printf("CTRL+Z pressionado");
                System.exit(1);
            }
        });
        
        // Loop infinito
        while (true) {}
    }
}
A novidade no código é essa:

Listagem 13: Novidade no código

    public interface SignalFunction extends Callback {
        void invoke(int signal);
    }
    SignalFunction signal(int signal, SignalFunction func);    
A função signal em C tem essa declaração:

Listagem 14: Declaração da função em C

void (*signal(int sig, void (*func)(int)))(int);

Ou seja, ela aceita no segundo argumento, um ponteiro de função que não retorna nada(void), e cujo argumento deve ser um valor inteiro. A função signal também retorna um ponteiro de função (retorno void e com um argumento int).

Por isso declaramos tanto o tipo de retorno, quanto o segundo argumento, como sendo do tipo SignalFunction, que estende a interface Callback e representa o ponteiro de função utilizado pelo signal.

Além de estender Callback, devemos sempre declarar um método público com a quantidade (e tipo) de parâmetros da função em C.

O método pode ter qualquer nome (não necessariamente invoke), o tipo de retorno pode ser diferente de void, a única restrição é que aja apenas um único método público declarado. Se houver mais de um método público, será lançada uma exceção em tempo de execução.

Ao rodar a aplicação dentro do Netbeans/Eclipse, talvez não seja possível utilizar o CTRL+Z. Uma forma equivalente é enviar um sinal 20 (SIGTSTP) através do comando kill.

kill -20 <pid_do_processo_java>

Nota: Cuidado para não enviar o kill ao PID do processo do Netbeans/Eclipse.

Isso é o mesmo que efetuar CTRL+Z no seu programa. Com isso a mensagem "CTRL+Z pressionado" será impressa e o programa finalizado.

Trabalhando com Estruturas

Existem duas formas de se trabalhar com estruturas em C: por valor e por referência. Em Java, passagens por valor podem ser representadas por métodos que recebem valores primitivos:

Listagem 15: Passagem por valor em Java

   int soma(int x, int y) {
      x = 2;
      y = 3;
      return x + y;
   }

   int x = 1;
   int y = 1;
   System.out.println("x + y = " + soma(x,y)); // imprime 5
   System.out.println("x = " + x);  // Imprime 1
   System.out.println("y = " + y);  // Imprime 1

Os valores impressos de x e y serão 1, mesmo eles tendo sido modificados dentro do método. Isso porque eles foram passados por valor.

Listagem 16: Passagem por referência

   class Valores {
      public int x;
      public int y;
   }

   int soma(final Valores v) {
      v.x = 2;
      v.y = 3;
      return v.x + v.y;
   }
   
   Valores val = new Valores();
   val.x = 1;
   val.y = 1;
   System.out.println("x + y = " + soma(val)); // Imprime 5 
   System.out.println("val .x = " + val.x); // Imprime 2
   System.out.println("val .y = " + val.y); // Imprime 3

Nesse caso, é possível alterar os valores x e y do objeto val. Isso é conhecido como passagem por referência. Álias, referências em Java são semelhantes a ponteiros em C.

Para passarmos objetos por referência em JNA estendemos a classe abstrata Structure. Para passar objetos por valor devemos trabalhar com Structure.ByValue.

Passando um Objeto por Valor

Criaremos uma shared library em C no Linux para demonstrar a passagem por valor.

Listagem 17: Shared Library (passagem por valor)

#include 
#include 

typedef struct Retangulo {
  int base;
  int altura;  
} Retangulo;

int calculaBase(Retangulo ret) { // Passagem por valor
  return ret.base * ret.altura;
}  

Nesse exemplo será criado uma shared libray (biblioteca dinâmica) que contêm a função calculaBase, que recebe uma estrutura Retangulo por valor. Para compilar e gerar a lib, execute os seguintes comandos:

  • Compilando: gcc -fPIC -c Retangulo.c
  • Gerando shared library: gcc -shared -o libRetangulo.so Retangulo.o
  • Verificando se a função calculaBase está na lib: nm libRetangulo.so

Uma vez criada o libRetangulo.so (shared object), íremos criar o código em Java para utilizar essa função (calculaBase), passando uma classe Retangulo com os atributos base e altura, por valor.

Listagem 18: Interface IRetangulo (passagem por valor)

package br.com.devmedia.jna;

import com.sun.jna.Library;

public interface IRetangulo extends Library {

    // declaracao da funcao a ser utilizada dentro da lib
    int calculaBase(Retangulo.ByValue r);  // Estamos dizendo que a passagem sera por valor
}   

Listagem 19:Classe Retangulo (passagem por valor)

package br.com.devmedia.jna;

import com.sun.jna.Structure;

// Seja por Valor ou Referencia, deve estender Structure
public class Retangulo extends Structure {
    
    public int base;
    public int altura;    

    // Inclui suporte a passagem por valor (ByValue)
    public static class ByValue extends Retangulo implements Structure.ByValue {}
}

Listagem 20: Classe de Teste (passagem por valor)

package br.com.devmedia.jna;

import com.sun.jna.Native;

public class TesteClasse {
     public static void main(String[] args) {  
         IRetangulo service = (IRetangulo) Native.loadLibrary("/libRetangulo.so", IRetangulo.class);  
         Retangulo.ByValue r = new Retangulo.ByValue();  
         r.base = 30;  
         r.altura = 30;  
         System.out.println("Base = " + service.calculaBase(r));  // Sera 900
     } 
}

Para indicar que estamos passando a classe Retangulo por valor, ao invés de criar diretamente uma instância de Retangulo, criamos o objeto do tipo Retangulo.ByValue. Somente isso é necessário.

Se Retangulo.ByValue indica objetos que serão passados por valor, o leitor deve estar imaginando: se eu passar um objeto que é instância de Retangulo, a passagem será por referência? A resposta é sim.

Passando um Objeto por Referência

Modificaremos a biblioteca libRetangulo.so, para incluir uma outra função que recebe um ponteiro da struct Retangulo.

Listagem 21: Shared Library (passagem por referência)

#include 
#include 

typedef struct Retangulo {
  int base;
  int altura;  
} Retangulo;

// Por valor
int calculaBase(Retangulo ret) {
  return ret.base * ret.altura;
}  

// Por referencia
int transformaEmQuadrado(Retangulo* ret) {
  ret->base = ret->altura;
  return ret->base * ret->altura;
}  

Regere a lib novamente conforme indicado anteriormente. A função transformaEmQuadrado recebe um ponteiro de Retangulo (passagem por referência), e dentro dela copia o valor da altura para a base e calcula a área.

Veremos agora como passar o objeto Retangulo por referência em Java.

Listagem 22: Interface IRetangulo (passagem por referência)

package br.com.devmedia.jna;

import com.sun.jna.Library;

public interface IRetangulo extends Library {

    // declaracao das funcoes a serem utilizadas dentro da lib
    int calculaBase(Retangulo.ByValue r);  // Estamos dizendo que a passagem sera por valor
    int transformaEmQuadrado(Retangulo r); // Passagem por referência
}   

Listagem 23: Classe de Teste (passagem por referência)

package br.com.devmedia.jna;

import com.sun.jna.Native;

public class TesteClasse {
      public static void main(String[] args) {  
         IRetangulo service = (IRetangulo) Native.loadLibrary("/libRetangulo.so", IRetangulo.class);  
         Retangulo.ByValue r = new Retangulo.ByValue();  
         r.altura = 10;  
         r.base = 30;  
         // Passagem por valor
         System.out.println("Base = " + service.calculaBase(r));  // Imprime 300
         System.out.println("r.base = " + r.base);  // Imprime  30
         System.out.println("r.altura = " + r.altura);  // Imprime 10
         
         Retangulo r2 = new Retangulo();  
         r2.altura = 10;  
         r2.base = 30;           
         // Passagem por referencia
         System.out.println("Base = " + service.transformaEmQuadrado(r2));  //Imprime 100
         System.out.println("r2.base = " + r2.base);  //Imprime 10
         System.out.println("r2.altura = " + r2.altura);  //Imprime 10
     } 
}

Conclusão

A API JNA provê uma série de recursos para lidar com alocação de memória (através das classes Memory/Pointer), callbacks (Callback), structs (Struture), arrays, etc de uma forma bem mais simples do que o JNI. Utilizar JNA no Windows também é tão simples quanto o que foi exposto nos exemplos acima. Com isso fechamos a análise dessa excelente biblioteca, que facilita a integração entre código nativo e a plataforma Java.

Obrigado pessoal e até a próxima!

Referências

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