quinta-feira, 19 de dezembro de 2013

Objetos Java via JNI

Existem trechos de código na linguagem C que não podem ser substituídos por fortes razões. No meu caso, a codificação é de terceiros para interpretação de arquivos binários com formato bem específico. Uma vez que possuo o código, eu poderia reescrevê-lo, mas assumiria riscos desnecessários. Portanto, meu código fonte teria que ser desenvolvido também em C ou C++ ou então teria que fazer a ponte com JNI para Java.

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:

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.

Nenhum comentário:

Postar um comentário