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:
- Os cabeçalhos JNI em C++/C são gerados automaticamente;
- Os códigos C++ e Java são compilados juntos;
- Os artefatos gerados, sendo o JAR e DLL, sempre estarão casados com o mesmo número de versão;
- 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;
- 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.