Motivação
Assim, tenho a seguinte situação: preciso ler um conjunto de dados em C e transferir para o Java. Os dados são modelados como objetos em Java. Ler atributo por atributo em C via JNI para instanciar o objeto em Java seria inviável. Também, ler um conjunto de atributos por vez para instanciar o objeto Java em C pode haver um overhead para máquina mais lentas. O ideal parece ler os atributos e instanciar objetos Java via JNI em C e retornar um array ou uma lista destes objetos pelo método nativo declarado em Java. Ufa! deu para entender? Vamos para a prática mesmo...Objetivo
Eu costumo produzir códigos como "prova de conceito", para testar se a ideia principal funciona (ou seja, validar os pontos críticos). Neste exemplo, é fazer com que código em C/C++ "instancie" objetos definidos no Java via JNI. Em outras palavras, uma classe Java é definida, no código C/C++ ela é instanciada via JNI e tem seus atributos modificados. Um método nativo declarado em Java é implementado no C/C++ para retornar um array destes objetos instanciados; outro método para retornar uma lista e um outro para retornar apenas uma instância de objeto. Assim, uma comparação é feita para determinar qual método é mais eficiente.Pré-requisito
O projeto desenvolvido como exemplo emprega Maven, a utilização do nar-maven-plugin e JUnit para teste unitário. No artigo "JNI: Java e C++ Num Único Projeto" deste blog está a descrição do uso do nar-maven-plugin para Maven.Ambiente
O projeto foi compilado e testado na plataforma Windows 7 64 bits utilizando os seguintes sistemas:- Maven 3
- Mingw 4.8.1
- JDK 7 (32 bits)
- JUnit 4.11
- Nar Maven Plugin 3.0
Para o Maven, Mingw e Java, seus diretórios de instalação (com \BIN) devem ser incluídos manualmente no PATH do Windows.
O nar-maven-plugin e o JUnit serão baixados automaticamente pelo Maven.
Embora a plataforma de desenvolvimento seja 64 bits, o MingW compila código apenas para plataforma 32 bits. Por este motivo, é importante que o JDK também seja 32 bits para que seja possível o carregamento da DLL pela JVM.
Download
O código do projeto está disponível para download pelo Google Drive. O teste unitário em JUnit não é apresentado neste artigo mas está disponível para download.O código também serve para aqueles mais versados nas tecnologias envolvidas aqui, para que possam aprender mais rapidamente, apenas analisando os códigos, sem ter que ler todo o artigo.
Desenvolvimento
A parte mais fácil deste projeto é o código Java. Duas classes são definidas: um Java Bean para representar o dado lido e outra com os métodos nativos que representam a interface JNI com o código C++.Código Java
O código para a classe Java de dados é muito simples e dispensa comentários:public class DataObject { public long id; public long getId() { return id; } public void setId(long id) { this.id = id; } }
Os métodos nativos estão declarados na classe DataLoader. O primeiro retorna uma única instância do objeto DataObject, o segundo retorna uma array e o terceiro uma lista. Esta classe também tem um bloco estático que carrega a DLL JNI pela invocação de um método estático da Classe NarSystem. Esta outra classe é gerada automaticamente pelo nar-maven-plugin (veja o artigo recomendado sobre este assunto).
public class DataLoader { static { NarSystem.loadLibrary(); } public native DataObject loadObject(int id); public native DataObject[] loadArray(); public native List<DataObject> loadList(); }
Código C++
Compilando o programa, o nar-maven-plugin vai gerar os cabeçalhos C JNI para implementação das funções (correlacionados com os métodos nativos Java) em C++.A primeira função cria apenas uma instância de DataObject cujo atributo 'id' é passado como argumento. Basicamente, um identificador da classe é localizado, bem como um identificador do seu construtor default; um identificador do atributo 'id' também é recuperado. Com estes identificadores, cria-se a instância do Objeto Java na JVM via JNI e atribui-se um valor ao atributo 'id' do objeto.
JNIEXPORT jobject JNICALL Java_com_prototype_jni_DataLoader_loadObject (JNIEnv *env, jobject, jint id) { jclass dataObjectClassId = env->FindClass("com/prototype/jni/DataObject"); jmethodID dataObjectContructorId = env->GetMethodID(dataObjectClassId, "<init>", "()V"); jobject dataObject = NULL; jfieldID fid = env->GetFieldID(dataObjectClassId, "id", "J"); dataObject = env->NewObject(dataObjectClassId, dataObjectContructorId); env->SetLongField(dataObject, fid, (jlong) id); return dataObject; }
A segunda função retorna um array de instâncias de DataObject. A idéia para instanciar tanto o array de DataObject quanto as instância de DataObject é a mesma apresentada anteriormente. Aqui, 100.000 instância são colocadas no array retornado pela função.
JNIEXPORT jobjectArray JNICALL Java_com_prototype_jni_DataLoader_loadArray (JNIEnv *env, jobject) { int size = 100000; jclass dataObjectArrayClassId= env->FindClass("com/prototype/jni/DataObject"); jobjectArray dataObjectArray = env->NewObjectArray(size, dataObjectArrayClassId, NULL); jclass dataObjectClassId = env->FindClass("com/prototype/jni/DataObject"); jmethodID dataObjectContructorId = env->GetMethodID(dataObjectClassId, "<init>", "()V"); jobject dataObject = NULL; jfieldID fid = env->GetFieldID(dataObjectClassId, "id", "J"); for (int i = 0; i < size; i++) { dataObject = env->NewObject(dataObjectClassId, dataObjectContructorId); env->SetLongField(dataObject, fid, (jlong) i); // Add to the array env->SetObjectArrayElement(dataObjectArray, i, dataObject); } return dataObjectArray; }
A função que retorna uma lista não apresenta muita diferença das demais. Vale a pena mencionar que o método nativo Java retorna uma lista (generics) de DataObject e 'generics' é uma característica do compilador Java. Via JNI, a instância do ArrayList recebe instâncias de qualquer objeto. Neste caso uma exceção do tipo java.lang.ClassCastException ocorrerá somente quando houver uma iteração da lista.
JNIEXPORT jobject JNICALL Java_com_prototype_jni_DataLoader_loadList (JNIEnv *env, jobject) { jclass arrayListClassId = env->FindClass( "java/util/ArrayList" ); jmethodID arrayListContructorId = env->GetMethodID(arrayListClassId, "<init>", "()V"); jobject arrayListObject = env->NewObject(arrayListClassId, arrayListContructorId); jmethodID addMethodID = env->GetMethodID(arrayListClassId, "add", "(Ljava/lang/Object;)Z"); jclass dataObjectClassId = env->FindClass("com/prototype/jni/DataObject"); jmethodID dataObjectContructorId = env->GetMethodID(dataObjectClassId, "<init>", "()V"); jobject dataObject = NULL; jfieldID fid = env->GetFieldID(dataObjectClassId, "id", "J"); for (int i = 0; i < 100000; i++) { dataObject = env->NewObject(dataObjectClassId, dataObjectContructorId); env->SetLongField(dataObject, fid, (jlong) i); // Add to the list env->CallBooleanMethod(arrayListObject, addMethodID, dataObject); } return arrayListObject; }
Testes
Três testes foram realizados para validar o código criado e computar o tempo para se criar 100.000 instância de DataObject via JNI.No primeiro teste, o código JNI é invocado para cada instância criada do objeto, e esta instância é adicionada numa lista em Java.
O segundo teste obtém um array de instâncias de DataObject via JNI. Este array é então, em Java, convertido numa lista do tipo ArrayList (pois seria desta forma que o conjunto de instância seria manipulado no código posteriormente).
Finalmente, no terceiro teste, a lista de DataObject é criada via JNI e recuperada em Java.
O tempo calculado para cada teste varia de máquina para máquina e portanto é muito relativo. Criar o array de DataObjects é sempre mais rápido que criar a lista via JNI, mesmo convertendo em Java o array numa lista. Como era esperado, obter cada instância de DataObject via JNI para depois adicioná-las numa lista em Java é muito oneroso.