quinta-feira, 12 de dezembro de 2013

JNI: Java e C++ Num Único Projeto Maven

Desenvolver projetos que empreguem a tecnologia JNI necessariamente não significa ter um projeto para Java e um outro para C++ (ou C) separados. Com Maven e o plugin nar-maven-plugin é possível ter os dois projetos como se fossem um só. O plugin permite a compilação do código Java, a geração dos cabeçalhos JNI em C++/C, a compilação do código C++/C e a execução dos testes unitários, tudo isso com pouca configuração.

Além de uma configuração fácil, as vantagens práticas de se usar o nar-maven-plugin para projetos com JNI são:
  1. Os cabeçalhos JNI em C++/C são gerados automaticamente;
  2. Os códigos C++ e Java são compilados juntos;
  3. Os artefatos gerados, sendo o JAR e DLL, sempre estarão casados com o mesmo número de versão;
  4. O nome da DLL (que inclui o número da versão conforme o padrão Maven) é automaticamente ajustado no código Java para seu carregamento na JVM;
  5. Projetos que dependem da biblioteca Java com JNI fazem apenas uma referência de dependência que inclui tanto o JAR quanto a DLL;
Cada item descrito acima merece uma descrição mais elaborada que será apresentada no restante do artigo. Outra grande vantagem, que não será explorada neste artigo, é possibilidade de gerar diferentes versões de DLL para cada tipo de plataforma de sistema operacional.

Além das vantagens de se usar o nar-maven-plugin, ainda existem outras vantagens de se usar o Maven também para o projeto C++/C.

O detalhamento da utilização da tecnologia JNI também não é abordado neste artigo, além no necessário para se entender o projeto.

Objetivos do Projeto JNI

O projeto apresentado aqui é para fins didáticos com o objetivo de testar o nar-maven-plugin, compilando código Java e C++ num único projeto, mais a execução de um teste unitário JUnit.

A parte Java do projeto constitui apenas de uma classe com a definição de um método nativo para chamada do código em C++. Assim a parte C++ consiste na implementação da função JNI cujo cabeçalho é gerado pelo plugin. Um teste unitário é criado para validar a codificação.

Tanto o código C+++ quanto o código Java  não serão detalhado neste artigo, mas estão disponíveis para download para melhor entendimento do projeto.

Preparação do 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. Embora seja simples, ele serve de modelo inicial para projetos reais.


O código também serve para aqueles mais versados nas tecnologias envolvidas aqui, para que possam aprender mais rapidamente sem ter que ler todo o artigo, apenas analisando os códigos.

Configuração do Projeto JNI

O arquivo pom.xml contem todas as configurações do projeto. O arquivo completo não é apresentado aqui, mas as partes mais relevantes são apresentadas e comentadas, ou seja, as configurações mínimas exigidas pelo Maven estão suprimidas. Obtenha o arquivo pom.xml completo fazendo o download do projeto

 Packaging

A primeira coisa que se deve fazer num projeto que utiliza o nar-maven-plugin é alterar a configuração (obrigatória) de packaging do  Maven. Num projeto java, esta configuração é definida como JAR, mas com o nar-maven-plugin ela tem que ser definida como NAR, conforme abaixo:

<!-- Importante que JAR seja substituído por NAR -->
<packaging>nar</packaging>

Dependências

Apenas a dependência do JUnit para a execução do teste unitário deve ser descrita no arquivo de configuação pom.xml. A configuração de dependência é padrão do  Maven; veja que o escopo da dependência é apenas para teste:

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Plugin

A configuração do nar-maven-plugin segue abaixo:

<build>
    <plugins>            
        <plugin>
            <groupId>com.github.maven-nar</groupId>
            <artifactId>nar-maven-plugin</artifactId>    
            <version>3.0.0</version>  
            <extensions>true</extensions>
            <configuration>                                                     
                <cpp>                                                        
                    <name>g++</name> 
                </cpp>
                <linker>
                    <name>g++</name>
                    <options> 
                        <option>-Wl,--kill-at</option>               
                    </options>
                </linker>
                <libraries>
                    <library>
                        <type>jni</type>
                        <narSystemPackage>com.prototype.jni.project</narSystemPackage>
                    </library>                        
                </libraries>                     
                <javah>                 
                    <jniDirectory>${basedir}\src\main\c++</jniDirectory>
                    <classDirectory>${project.build.outputDirectory}\com\prototype\jni\project</classDirectory>                       
                </javah> 
            </configuration>             
        </plugin>
    </plugins>
</build>

A declaração do plugin é bastante trival e segue os convenções do Maven. A partir da tag <configuration> estão as configurações particulares do plugin. Estas configurações, suas particularidades e funcionamento do plugin são descritos abaixo

O nar-maven-plugin tem compiladores e linkers definidos como padrão (default) de acordo com o sistema operacional. No caso do Windows, o padrão é o Microsoft Visual C++. Se este fosse o caso do projeto apresentado, não seria necessário definir o nome do compilador C++ e do linker (que seria mvsc). Portanto, como o Mingw é o compilador e o linker utilizados no projeto, o plugin precisa saber disso, informando o nome como g++ para o compilador (tag <cpp>) e para o linker (tag <linker>).

Ainda sobre o linker, no caso especificamente do g++ (ou gcc), é importante passar a opção -Wl,--kill-at para que a DLL seja gerada corretamente. Sem esta opção, a máquina virtual Java, ao carregar a DLL, não encontra as funções que espera encontrar, pois o linker adicionar um sufixo @número ao nome de cada função. Na opção descrita, note que após a vírgula não existe espaço em branco, portanto estamos falando de uma única opção e não de duas opções aqui!

A tag <libraries> contém a tag <library> com o tipo de binário gerado. No caso deste projeto, o tipo válido corresponde a JNI (em minúsculo) o que significa que o código Java deve ser compilado e gerado um arquivo do tipo JAR e o código C++ (ou C) deve ser compilado e gerado um arquivo do tipo DLL. Ainda, aqui também se define o package para a classe java NarSystem cujo código é gerado automaticamente pelo plugin para carregamento da DLL JNI. Informações adicionais sobre esta classe serão apresentadas mais adiante, quando a execução do plugin é abordada.

A geração automática do cabeçalho JNI a partir de classes Java com métodos nativos é feita com a configuração da tag <javah>. Aqui, é importante configurar duas coisas: o caminho onde os cabeçalhos C++/C serão gerados (tag <jniDirectory>) e o caminho onde os byte codes das classes Java (com métodos nativos) estão localizados (tag <classDirectory>).

Propriedades

Uma propriedade em especial precisa ser declarada no arquivo pom.xml (completamente fora da declaração e configuração do plugin). A propriedade <skipTests> deve ser definida como false para que o plugin surefire do Maven não execute os testes unitários. Este plugin não precisa ser declarado no arquivo pom.xml pois é padrão do Maven.

<properties>
    <skipTests>true</skipTests>
</properties>

Com esta propriedade definida como falsa, a execução dos testes unitários fica por conta do nar-maven-plugin. A razão disto é que ele inclui no classpath o caminho da DLL JNI antes do início da execução do teste.

Construção do Projeto e Funcionamento do Plugin

Uma vez o arquivo pom.xml configurado, a classe Java, com pelo menos um método nativo, já pode ser criada num determinado pacote (package) java. Uma vez compilado o projeto, o nar-maven-plugin invoca o javah do JDK para criar automaticamente os cabeçalhos JNI (arquivos com a extensão H) no diretório \src\main\c++, conforme fora configurado pela tag <jniDirectory>.

Para cada classe com métodos nativos, tem-se um arquivo gerado com a assinatura das funções que devem ser criadas em C++ ou C. Um arquivo adicional com cabeçalho é criado pelo plugin para uma classe 'mágica' chamada de NarSystem. Este cabeçalho contém a assinatura de uma função que corresponde ao método runUnitTestsNative da classe NarSystem. Esta função pode ser implementada para que testes unitários em C++ ou em C possam se executados, porém isso não é obrigatório e não será discutido neste artigo.

A classe NarSystem é gerada no diretório \target\nar\nar-generated, mais os diretórios que compõem o pacote (package) Java onde está a classe, conforme configurado no arquivo pom.xml pela a tag <narSystemPackage>. Analisando esta classe, que é bem simples e intuitiva, ela é composta por dois métodos estáticos e um método nativo e por isso o cabeçalho JNI adicional é gerado automaticamente. Um método desta classe destina-se ao carregamento da DLL e o outro a execução dos testes unitários em C++ ou C. A vantagem na utilização desta classe para carregamento da DLL é que o número da versão do projeto que compõe o nome da DLL é corrigido automaticamente pelo pelo nar-maven-plugin, para seu carregamento adequado.

Embora a compilação tenha ocorra com sucesso até este ponto, o projeto ainda não está completo. Faltam as implementações das funções que são chamadas pelo Java. O código da implementação deve estar na pasta \src\main\c++ (ou pelo menos a partir dela) pois este é o diretório padrão que o plugin espera encontrar código C++. Este diretório, além de ser o padrão, foi exatamente o configurado pela tag <jniDirectory> no arquivo pom.xml.

Para finalizar, o teste unitário precisa ser criado para testar o código Java que invoca o código C++. O importante aqui está no carregamento da DLL JNI pelo Java. A classe NarSystem com o método estático loadLibrary faz este trabalho, mantendo o nome da DLL sincronizado com o nome do JAR e o número da versão do projeto. Este método deve ser chamado assim que o programa Java for carregado.

Uma vez tudo compilado, o nar-maven-plugin adiciona ao classpath (Java) o diretório da DLL para rodar o teste unitário.

Caso o teste seja executado com sucesso, o plugin compacta o binário JAR e DLL num único arquivo com extensão NAR e o instala no repositório local Maven. Um outro projeto que tenha dependência deste projeto precisa também utilizar o nar-maven-plugin, que se encarrega de descompactar o arquivo NAR e disponibilizar tanto o arquivo JAR quanto o arquivo DLL para uso.

Conclusão

O nar-maven-plugin é ideal para a criação de código Java e C++ que emprega a tecnologia JNI. Ele mantem um único projeto para códigos de ambos compiladores dentro dos padrões Maven e sua configuração é de baixa complexidade.

A automatização da geração de cabeçalhos com assinaturas de funções que fazem interface com Java e C++ garante que elas sempre estarão casadas. Todavia, isso não suficiente para evitar erros no projeto: mesmo que os cabeçalhos gerados com as assinaturas das funções C++ e as respectivas implementações das funções C++ estejam descasados, o código C++ compila sem erros.

O plugin ainda permite convenientemente que o nome da DLL gerado siga o padrão Maven e esteja casado com o nome do binário Java (JAR). Esta padronização de nomes tem consequência no carregamento da DLL pelo programa Java, pois caso ocorra alteração do número da versão do projeto, o nome da DLL é também alterado (e seu nome no programa Java também deveria ser alterado). O nar-maven-plugin mantem isso funcional criando automaticamente no projeto a classe NarSystem para carregar a DLL no programa Java, ajustando adequadamente o nome da DLL.

Nenhum comentário:

Postar um comentário