terça-feira, 16 de fevereiro de 2010

Tutorial simples de JNI no Linux c/ GCC

Introdução

O JNI (Java Native Interface) é uma interface que viabiliza interagir a JVM com bibliotecas nativas do SO subjacente. É utilizada principalmente para tarefas de mais baixo nível que a API Java não oferece ou, ainda, quando necessita-se de uma performance maior sem o "overhead" da JVM no caminho.

É importante ressaltar que a utilização de métodos nativos torna sua aplicação dependente de plataforma. Em Linux trabalharemos com arquivos de extensão SO (Shared Objects), já em Windows trabalha-se com DLLs (Dynamic Linked Libraries). O JNI, no entanto, não se limita a somente Linux e Windows, é possível utilizá-lo em outras plataformas (Solaris, AIX, etc) de acordo com a implementação JVM.

Esse tutorial tem o objetivo de mostrar um simples exemplo de utilização do JNI. Diga-se de passagem, a documentação da Sun sobre o assunto é muito rica e os links se encontram nas referências.

Primeiro passo: criar a classe que conterá o(s) método(s) nativo(s).

Crie um diretório "Carnival" (apenas por questão de organização), uma classe e um método com o modificador "native" sem implementação para trabalharmos no exemplo.

public class Carnival
{

// Carregamento da biblioteca que será criada.
static {
System.loadLibrary("Carnival");
}

// Método marcado com modificador "native".
public native void dance(String verse);

public static void main(String[] args) {
new Carnival().dance("...pode chorar, mas chora...");
}
}



Para que a JVM possa "linkar" a chamada do método da classe Carnival com uma biblioteca compartilhada nativa, ela precisará saber o nome. Aqui carregamos a biblioteca assim que a classe Carnival é carregada pela JVM:

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


Logo em seguida declaramos o método "dance()" que será chamado pelo método "main()". Como a implementação de um método "native" será escrita em outra linguagem (C ou C++), não iremos fornecer implementação e terminaremos a declaração com ponto-e-vírgula.

Segundo passo: compilar a classe.

javac Carnival.java


Ela será compilada normalmente, mesmo que as bibliotecas nativas ainda não estejam acessíveis. Você só não conseguirá executar ainda.

Terceiro passo: utilizar o "javah" para gerar os headers em C/C++.

javah -jni Carnival


* Certifique-se que a classe esteja em seu "classpath".

Isso gerará um arquivo Carnival.h que será usado ao criar a implementação do método.

Se quiser, examine o Carnival.h. Você encontrará os protótipos das funções que implementará em seguida.

Quarto passo: implementar o método nativo.

Crie um arquivo chamado Carnival.c e dentro dele virá a implementação do método "dance()".

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

JNIEXPORT void JNICALL
Java_Carnival_dance(JNIEnv * env, jobject obj, jstring verse)
{
const char * vstr = (*env)->GetStringUTFChars(env, verse, NULL);
printf("%s\n", vstr);
return;
}



Quinto passo: compilar a implementação.

Neste tutorial utilizaremos o GCC. No meu desktop utilizei um Makefile para poupar o trabalho, mas aqui vai o comando direto para compilar.

Antes criaremos um diretório "lib" que irá conter o arquivo "Shared Object" gerado pelo GCC:

mkdir lib

E então compilaremos:

gcc -o lib/libCarnival.so -shared -Wl,-soname,libCarnival.so \
-I/opt/jdk/include \
-I/opt/jdk/include/linux Carnival.c \
-static -lc

É importante lembrar que a biblioteca precisa estar no PATH de bibliotecas do Linux, então defina a variável de ambiente LD_LIBRARY_PATH para o diretório "lib" criado:

LD_LIBRARY_PATH="$(pwd)/lib"
export LD_LIBRARY_PATH

Com isso, ao executar a classe, a "JVM" conseguirá encontrar a biblioteca libCarnival.so.

Sexto (e último) passo: executar.

java Carnival

Espera-se, aqui, que seja impressa a mensagem que foi enviada através do parâmetro "verse" (String) pelo método "dance()" da classe Java Carnival.

Referências:

http://java.sun.com/docs/books/jni/html/start.html#26346

http://java.sun.com/developer/onlineTraining/Programming/JDCBook/jniexamp.html

http://www.ibm.com/developerworks/aix/library/au-JNI_AIX_PAPER.html

Quando as chamadas polimórficas mudam tudo...

Estudando p/ o exame SCJP, ao fazer uma revisão de assuntos dos primeiros capítulos antes de prosseguir, notei uma coisa que não foi abordada pelo livro.

Aparentemente, fazer a chamada polimórfica de um método declarado na interface geraria o mesmo resultado:
interface Doable {
public void doSomething();
}

class Okay implements Doable {
@Override

public void doSomething() {
System.out.println("Oi, sou o método doSomething subscrito em Okay.");
}

public static void main(String[] args) {
Okay o = new Okay();
Doable d = o;

// Aparentemente o comportamento é o mesmo.
d.doSomething(); // OK
o.doSomething(); // OK
}
}
Será, então, impresso:
Oi, sou o método doSomething subscrito em Okay.
Oi, sou o método doSomething subscrito em Okay.
Ora, basta declarar o lançamento de exceções na declaração do método "doSomething()" da interface que tudo muda. Veja:
class BoringHolidayException extends Exception {}

interface Doable {

public void doSomething() throws BoringHolidayException;
}

class Okay implements Doable {
// Não declara a exceção BoringHolidayException.
@Override
public void doSomething() {
System.out.println("Oi, sou o método doSomething subscrito em Okay.");
}

public static void main(String[] args) {
Okay o = new Okay();
Doable d = o;

// Como a declarção de doSomething() na interface
// declara lançar a
exceção BoringHolidayException,
// a chamada polimórfica do método
através de uma
// referência à interface exigirá tratamento das exceções.

d.doSomething(); // Erro em tempo de compilação.

// Enquanto chamar o método diretamente por uma
// referência Okay não irá lançar a exceção, uma
// vez que, propositalmente, o método subscritor
// não lança nenhuma exceção.
o.doSomething(); // OK. Sem problemas.
}
}

O que já se sabe (se você também já leu o capítulo 1 do mencionado livro) é que duas das regras de subscrição são:
  1. não declarar exceções mais abrangentes e;
  2. não declarar novas exceções no método subscritor.
Embora você nem precise declarar qualquer exceção declarada pelo método subscrito, nunca faça nenhuma dessas duas coisas.

Então, neste caso, é necessário tratar a exceção declarada pela chamada polimórfica de doSomething(), mas não é necessário se preocupar pela chamada a partir da referência Okay.

Certo! Fiz esse post, porque sei que a SCJP adora pegadinhas desse tipo, então, tendo escrito aqui, não vou esquecer na hora da prova :ô) e, quem sabe, algum dia, alguém se preocupe em pesquisar isso no Google...