Criação de uma interface Java para TerraLib 5 Fernando Bagnara Mussio 1 Eduardo Laino 1 1 Empresa Brasileira de Aeronáutica S.A - Embraer Caixa Postal 96 – 12227-901 – São José dos Campos - SP, Brasil {fernando.bagnara, eduardo.laino}@embraer.com.br Abstract. This report presents a brief explanation of how to use TerraLib framework, a powerful platform for GIS development, with the Java programming language. TerraLib code is written in pure C++. The Java programming language has been widely adopted for its portability and user friendliness. In order to use TerraLib with other languagens than C++ a binding to the specific language is necessary. A language binding is a common strategy to allow C/C++ code to be used from another language. It improves software reuse as it avoids the need for reimplementing the library in several languages. Sometimes it also provides more efficiency due to the impossibility of implementing certain algorithms (efficiently) in high-level languages like Java. The TerraLib platform provides bindings for several higher-level languages including Java. In this report is explained how to add native TerraLib C++ code to a Java program by using an interface for hybrid implementation called Java Native Interface, or JNI. Java classes were implemented in order to allow Java users to develop GIS applications in a similar way C++ users should done using TerraLib framework. For the Java classes which add native TerraLib code it shall be indicated the use of the corresponding dynamic library containing the native code. Palavras-chave: C++, JNI, GIS, geoprocessamento. 1. Introdução Este artigo descreve como adicionar implementação nativa da biblioteca TerraLib, escrita em C++, a programas Java usando a Java Native Interface, ou JNI. TerraLib 5 é uma plataforma para a construção de aplicativos geográficos que apresenta classes escritas em C++ com código fonte aberto e distribuída como um software livre. Ela é compilada em um ambiente multiplataforma, Windows e Linux, e em diferentes compiladores C++. A plataforma é desenvolvida seguindo os padrões especificados pela OGC e ISO, tais como Simple Feature Specification (SFS), Geography Markup Language (GML) e OGC Web Services. Além disso, TerraLib suporta diferentes tipos de fontes de dados geográficos, como SGBD (ex; PostGIS, Oracle Spatial and SQLite), arquivos vetoriais e matriciais (ex: shapefile, kml e geotiff) e serviços WEB (ex: WMS, WFS e WCS). A arquitetura macro da TerraLib é mostrada na Figura 1. Figura 1 – Arquitetura Macro da TerraLib Um aplicativo geográfico que exemplifica a utilização da biblioteca TerraLib é o TerraView. Na figura 2 é possível observarmos o uso desta biblioteca pelo TerraView. Java foi desenvolvida por volta de 1990, pouco antes da explosão da Internet. Tendo sido originalmente concebida para o desenvolvimento de pequenos aplicativos e programas de controle de aparelhos eletrodomésticos e eletroeletrônicos, Java mostrou-se ideal para ser usada na rede Internet. Desde então a linguagem de programação Java vem ganhando cada vez mais adeptos devido à sua simplicidade, riqueza de bibliotecas e portabilidade entre plataformas e sistemas operacionais. Java é ainda uma linguagem orientada ao objeto, distribuída, interpretada, robusta, segura, de arquitetura neutra, de alto desempenho, multithreaded, e dinâmica. Devido a estas características este artigo descreve como utilizar uma interface de programação híbrida para adicionar implementação nativa da biblioteca TerraLib, escrita em C++, a programas Java. Utilizou-se para isto uma solução da Sun, empresa responsável pela criação e atualização da linguagem Java e do seu ambiente de execução (JVM – Java Virtual Machine), de execução, de dentro do programa Java, de código nativo, compilado para uma determinada plataforma a partir de código C ou C++. Esta interface para a programação híbrida é chamada de Java Native Interface, ou JNI. Ao usar a JNI – Java Native Interface – implementa-se em Java as partes do programa para as quais esta linguagem melhor se adapte, como, por exemplo, interfaces com o usuário e com a rede, deixando as partes intensivas em processamento ou recursos locais para serem implementadas em C ou C++ gerando código nativo. Tomando-se alguns cuidados com o projeto e a implementação da parte nativa, é de se esperar que os programas resultantes sejam portáteis, ou seja, recompilando-se a parte nativa pode-se transferir o programa rapidamente para outras plataformas. Como a JNI faz parte da especificação de Java, a maioria das Java Virtual Machines (JVM) implementa a mesma, assim como os Java Development Kits (JDK) provêm suporte apropriado. Figura 2 – Utilização da TerraLib pelo aplicativo TerraView 2. Metodologia de Trabalho Considera-se importante delimitar bem as funções de interface entre as linguagens para um bom projeto JNI. As funções em C ou C++ de contato direto com Java devem traduzir os dados passados como parâmetros e encaminhá-los às funções da implementação nativa que realizarão efetivamente o trabalho que se espera. Da mesma forma, devem existir funções que traduzam de volta para o ambiente Java os resultados produzidos pelas funções nativas ficando assim identificada o que é interface entre os ambientes e o que é codificação de cada um. A figura 3 apresenta a arquitetura padrão proposta para a interface via JNI. Figura 3 – Arquitetura padrão proposta para a interface via JNI. Para implementação em linguagem Java utilizou-se o ambiente de desenvolvimento do Eclipse. Do lado Java ao redigir programas que utilizem a JNI, os métodos que serão implementados em C ou C++ são declarados em qualquer classe como sendo private native, retornando valores de qualquer tipo, tais como void, int, string, ou mesmo objetos. Nas classes que contém chamadas para funções nativas, deve ser indicado o uso da correspondente biblioteca dinâmica TerraLib que contém o código nativo. A forma para se indicar isso é utilizar o método System.loadLibrary ("nomeDaBiblioteca"). A biblioteca dinâmica é gerada a partir de código C ou C++ e na compilação em ambiente Windows Win32 deve ser gerada uma biblioteca .dll. Para implementação em linguagem C++ utilizou-se o ambiente de desenvolvimento do Visual Studio C++. Do lado C++, o aplicativo javah pode ser utilizado para facilitar a implementação do lado nativo dos métodos além de reduzir o número de possíveis erros de programação. Uma vez definidos na classe Java, pode-se utilizar este aplicativo para gerar um arquivo .h contendo os protótipos dos métodos da interface na forma C / C++. O módulo de definição contém as declarações C / C++ necessárias para a compilação dos componentes dos módulos nativos que interagirão com a JNI. O arquivo gerado pelo javah deve incluir dois arquivos localizados no pacote do JDK. Estes arquivos de inclusão são adicionados como se fossem arquivos do compilador usando um comando #include <nome-do-arquivo>, sem qualquer indicação de onde estão os arquivos de inclusão. O programador deve configurar corretamente o seu ambiente de desenvolvimento C ou C++ de modo que os arquivos de inclusão sejam encontrados durante a compilação. Para adquirir, atribuir e devolver valores para um objeto Java, é necessário usar o pacote de funções padrão C ou C++ que lidam com os elementos Java passados para a implementação. Parâmetros primitivos (int, float, etc.) podem ser utilizados sem necessidade de qualquer tradução e podem ser retornados da mesma forma. No caso de strings, no entanto, é prudente lembrar-se de liberar a memória ocupada pelo string antes de retornar. 2.1 Usando javah para gerar o .h Se o programador optar por utilizar o javah para gerar o .h com as definições das funções nativas correspondentes aos métodos definidos na classe Java, o programador deve primeiro compilar a classe. Serão gerados diversos arquivos .class, um para cada classe contida no arquivo .java compilado: javac arquivo.java Após isto o programador pode gerar o .h: javah –jni Nome-Da-Classe Será gerado o arquivo .h que obedece a estrutura mostrada na figura 4. O arquivo .h inclui o arquivo jni.h do sistema, localizado no diretório include do pacote JDK. Javah insere comentários sobre cada método nativo criado, logo antes de seus protótipos. Estes comentários contém o nome da classe, o nome do método em Java e as classes dos objetos usados como parâmetros ao método. A definição do protótipo da função nativa que implementa o método sempre começa com JNIEXPORT, seguido do tipo de retorno do método, seguido de JNICALL, e finalmente o nome da função, composto pela concatenação: Java_ + <nome da classe que contém o método>_ + <nome do método>. O tipo de retorno é um dos fornecidos por JNI: jboolean, jbyte, jchar, jshort, jint, jlong, jfloat, jdouble ou void. #include <jni.h> #ifndef _Included_nomeClasse #define _Included_nomeClasse #ifdef __cplusplus extern "C" { #endif /* * Comentários sobre o método nativo aqui... */ JNIEXPORT tipoRetorno JNICALL Java_nomeClasse_nomeMétodo (JNIEnv *, jobject[, tipo-parâmetro[...]]); ... #ifdef __cplusplus } #endif Figura 4 – Estrutura do arquivo .h gerado pelo javah No caso dos parâmetros passados à função, JNIEnv constitui um portal de comunicação entre o lado nativo e o lado Java. O primeiro jobject também sempre é passado: ele é um ponteiro para o objeto que chamou este método nativo. Depois há um tipo para cada parâmetro definido para o método. 2.2 Regras do lado C++ Ao desenvolver programas que interagem através da JNI devem ser observadas regras do lado do código nativo. Somente objetos visíveis nos parâmetros passados às funções de interface da implementação nativa podem ser acessados por estas funções. Se um parâmetro primitivo for passado (int, por exemplo), este parâmetro será acessado diretamente sem ser necessária a utilização de uma função JNI para traduzi-lo. O programador deve ter apenas o cuidado de saber lidar com o tamanho em bits deste tipo primitivo e utilizar os tipos nativos certos. Vale lembrar que String não é um tipo primitivo. Se o parâmetro passado for um objeto, então qualquer operação que o envolva deverá ser feita por via de uma função JNI. Desta forma, o ambiente Java permanece razoavelmente encapsulado e sua estrutura interna é irrelevante para a implementação nativa. Um elemento de um objeto não é acessado diretamente, sendo separado em duas etapas: adquirir o identificador deste elemento (determinando a classe) e adquirir o valor através deste identificador. Para adquirir a classe a que o objeto pertence, utiliza-se a seguinte função (notação C): jclass GetObjectClass( JNIEnv* pEnv, jobject obj ); onde: pEnv é o ponteiro do tipo JNIEnv * recebido pela função nativa obj é o objeto do qual se deseja determinar a classe. Para interagir com um atributo da classe é necessário primeiro obter o seu identificador. Com este identificador pode-se então interagir com o atributo. Para adquirir um identificador, quando se conhece a classe do objeto, utiliza-se a seguinte função se o elemento não for estático: jfieldID GetFieldID( JNIEnv* pEnv, jclass classe, const char* pNome, const char* pTipo ); onde: pEnv é o ponteiro do tipo JNIEnv * adquirido pela função nativa, classe é a classe obtida com a função anterior, pNome é umstring contendo o nome do elemento a ser identificado e pTipo é umstring que identifica o tipo deste elemento, O valor deste elemento pode então ser adquirido visto que o identificador já é conhecido: tipoNativo GetTipoField( JNIEnv* pEnv, jobject obj, jfieldID id ); onde: pEnv e obj foram adquiridos pela função nativa, e id foi adquirido pela função anterior. tipoNativo é um dos diversos tipos primitivos de C ou C++ (int, por exemplo). Tipo é o tipo primitivo do lado Java. Existem funções GettipoField ou GetStatictipoField para cada tipo conhecido por Java. O tema Tipo no nome das funções identifica o tipo do campo a ser acessado. 2.2.1 Regras do lado C++ para Strings Uma string Java pode ser adquirida como parâmetro para a função nativa ou como parte de um objeto passado como parâmetro. Para o caso da string ser parte de um objeto, ela deve antes ser identificada também. Java usa strings no formato Unicode. Ao adquirir valores de strings passadas por Java, a implementação nativa pode adquirir cópias traduzidas para ASCII ou cópias em Unicode mesmo. JNI fornece funções capazes de lidar com ambos os casos ASCII e Unicode. Algumas funções fornecidas por JNI: Para saber o comprimento de uma string Java retornando o número de caracteres Unicode de string: jsize GetStringLength( JNIEnv* pEnv, jstring string); Para saber o comprimento de uma string Java retornando o número de caracteres UTF-8 (compatível com ASCII): jsize GetStringUTFLength( JNIEnv* pEnv, jstring string); Para adquirir uma cópia de uma string Java, em Unicode: const jchar* GetStringChars( JNIEnv* pEnv, jstring string, jboolean* pIsCopy ); Para adquirir uma cópia de uma string Java, no formato UTF-8: const char* GetStringUTFChars( JNIEnv* pEnv, jstring string, jboolean* pIsCopy ); Para liberar a cópia adquirida deverá utilizar uma das funções seguintes de acordo com o tipo de cópia: void ReleaseStringChars( JNIEnv* pEnv, jstring string, const jchar* pUniStr ); ou void ReleaseStringUTFChars( JNIEnv* pEnv, jstring string, const char* pStr ); Para criação de strings Java pelo lado nativo baseando-se em variáveis string nativas: jstring NewString( JNIEnv* pEnv, const jchar* pUniStr, jsize comprimento ); jstring NewStringUTF( JNIEnv* pEnv, const char* pStr ); 2.2.2 Regras do lado C++ para vetores O conjunto de funções JNI para acesso a vetores apresenta funções para acesso completo a vetores de tipo primitivo, acesso parcial a vetores de tipo primitivo e acesso a elementos de vetores de objetos. Se o vetor é de elementos de tipos primitivos (int, por exemplo), o programador pode optar por adquirir uma cópia completa do vetor ou uma cópia parcial, dependendo do desempenho que desejado. As funções de acesso a vetores de tipo primitivo são: tipoNativo* GetTipoArrayElements( JNIEnv* pEnv,TipoArray vetor, jboolean* pIsCopy ); onde: tipoNativo e Tipo são equivalentes ao tipo primitivo dos elementos do vetor para o lado nativo e para o lado Java. Para o caso da definição do vetor, o tipo primitivo é seguido de Array. Para se adquirir uma cópia completa do vetor e liberá-la, as modificações no vetor copiado só terão garantia de serem espelhadas para o vetor Java ao executar esta função: void ReleaseTipoArrayElements( JNIEnv* pEnv, tipoArray vetor, tipoNativo* copia, jint opcao ); onde Tipo e tipoNativo são os tipos primitivos, copia é a cópia do vetor adquirida com a função descrita no início deste tópico e opção é recomendada ser 0 (zero), isto é, copia o conteúdo de copia e libera o espaço alocado para copia. Para tratar vetores de objetos, é necessário adquirir cada elemento por vez com as funções de acesso a vetores de objetos: jobject GetObjectArrayElement( JNIEnv* pEnv, jobjectArray vetor, jsize indice ); void SetObjectArrayElement( JNIEnv* pEnv, jobjectArray vetor, jsize indice, jobject valor ); onde: indice é o índice do elemento a ser manipulado e valor o valor a ser atribuído a ele. Para adquirir o número de elementos de um vetor Java, utiliza-se a função abaixo que serve para qualquer vetor Java, seja ele primitivo ou de objetos: jsize GetArrayLength( JNIEnv* pEnv, jarray vetor ); Para criar um vetor Java pelo lado nativo utiliza-se uma das funções: tipoArray NewTipoArray( JNIEnvv* pEnv, jsize tamanho ); jarray NewObjectArray( JNIEnv* pEnv, jsize tamanho, jclass classeElementos, jobject valorInicial ); onde tamanho é o número de elementos do vetor, classeElementos é a classe a que os elementos pertencem e valorInicial é o valor inicial dos elementos até que sejam alterados pelo programa. 3. Resultados e Discussão O diagrama de classes em C++ da biblioteca TerraLib é mostrado na figura abaixo: Figura 5 – Diagrama de classes C++ da TerraLib. 3.1 Diagrama das classes Java para TerraLib O diagrama das classes Java que acessam a biblioteca TerraLib está descrito na figura 6. As classes Java implementam métodos utilizados pela interface JNI para adicionar implementação nativa da biblioteca TerraLib. Conforme já explicado anteriormente estes métodos em Java devem ser declarados como sendo private native. Para cada classe Java com método nativo foi criada uma classe C++ correspondente com a implementação nativa para a biblioteca TerraLib. Figura 6 – Diagrama de classes Java com implementação para objetos TerraLib. Para as classes Java foram escolhidos nomes análogos aos componentes da plataforma TerraLib de maneira a facilitar a compreensão do programador para a implementação. Na figura 7 podemos observar o método stop da classe Plataform no arquivo Plataform.java com o método nativo e na figura 8 a estrutura do arquivo Platform.h gerado por javah seguindo as regras da interface JNI. A figura 9 apresenta a implementação correspondente do método stop na classe Platform em C++ . package te; public final class Platform { private Platform() { } public static native void start(); public static native void stop(); static { System.load("TerraLib_binding_java_d.dll"); } } Figura 7 – Arquivo Platform.java: Método stop da classe Plataform em Java /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class te_platform_Platform */ #ifndef _Included_te_platform_Platform #define _Included_te_platform_Platform #ifdef __cplusplus extern "C" { #endif /* * Class: te_Platform * Method: stop * Signature: ()V */ JNIEXPORT void JNICALL Java_te_Platform_stop (JNIEnv *, jclass); JNIEXPORT jint JNICALL JNI_OnLoad (JavaVM*, void*); JNIEXPORT void JNICALL JNI_OnUnLoad (JavaVM*, void*); #ifdef __cplusplus } #endif #endif Figura 8 – Arquivo Plataform.h: Estrutura gerada por javah void JNICALL Java_te_Platform_stop(JNIEnv* env, jclass /*clazz*/) { env->DeleteWeakGlobalRef(te::java::ClassCache::sm_envelopeClazz); env->DeleteWeakGlobalRef(te::java::ClassCache::sm_geomClazz); env->DeleteWeakGlobalRef(te::java::ClassCache::sm_ptClazz); env->DeleteWeakGlobalRef(te::java::ClassCache::sm_transactorClazz); env->DeleteWeakGlobalRef(te::java::ClassCache::sm_datasetClazz); env->DeleteWeakGlobalRef(te::java::ClassCache::sm_catalogClazz); env->DeleteWeakGlobalRef(te::java::ClassCache::sm_datasourceClazz); env->DeleteWeakGlobalRef(te::java::ClassCache::sm_propertyClazz); env->DeleteWeakGlobalRef(te::java::ClassCache::sm_complexClazz); env->DeleteWeakGlobalRef(te::java::ClassCache::sm_datasettypeClazz); env->DeleteWeakGlobalRef(te::java::ClassCache::sm_canvasClazz); te::plugin::Platform::finalize(); te::qt::widgets::Platform::finalize(); te::da::Platform::finalize(); te::gm::Platform::finalize(); te::common::Platform::finalize(); delete qtapp; qtapp = 0; } Figura 9 – Método stop da classe Plataform em C++ 3.2 Classes genéricas Plataform e TerraLib4J O diagrama das classes Platform e TerraLib4J é apresentado na Figura 10. A classe TerraLib4J é uma classe genérica que possui um atributo para armazenar um descritor, um handle JNI passado pelas classes filhas. A classe Platform é utilizada para iniciar e encerrar a biblioteca TerraLib com os respectivos métodos start e stop. Figura 10 – Métodos das classes Platform e TerraLib4A 3.3 Grupo de classes acesso aos dados O diagrama das classes pertencentes ao grupo de classes de acesso aos dados é apresentado na Figura 11, na Figura 12 e na Figura 13. Na biblioteca TerraLib, uma fonte de dados (data source) pode ser um provedor de dados geográficos tanto quanto de dados descritivos. É caracterizdo por um conjunto de parâmetros que podem ser usados para configurar um canal de acesso para o repositório (DataSourceInfo). Cada fonte de dado (data source) tem um catálogo de dados, representado por um objeto DataSourceCatalog. Este catálogo contem informações sobre os conjuntos de dados (data sets) armazenados na fonte de dados e de como estão organizados. A lista das fontes de dados suportadas: PostGIS Oracle Spatial SQLite GDAL (driver para acesso a diversos formatos de arquivos de imagem) OGR driver para acesso a diversos formatos de arquivos vetoriais) Figura 11 – Métodos das classes DataSourceTransactor e DataSetType Figura 12 – Métodos das classes DataSource e DataSet Figura 13 – Métodos das classes DataSourceCatalog, DataSourceFactory, PropertyType e ComplexType 3.4 Modelo de Geometrias O diagrama das classes pertencentes ao grupo de classes filhas de Geometry é apresentado na Figura 14. Na TerraLib os dados geográficos são agregados em layers. Layers são formados por conjuntos de objetos, onde cada objeto possui uma identificação única, um conjunto de atributos descritivos e uma representação geométrica. A classe Geometry representa a classe base análoga em TerraLib da qual derivam todas as geometrias de TerraLib. Cada geometria possui uma identificação única, a referência ao seu menor retângulo envolvente e a identificação do objeto geográfico que representa. Um determinado tipo de geometria é a classe Points que possui sua classe análoga em TerraLib referente a pontos. Figura 14 – Métodos do Grupo de classes Geometry 3.5 Exemplo Java A figura 15 apresenta um exemplo escrito em Java de utilização das classes Java para acessar código nativo da biblioteca TerraLib. A biblioteca é carregada dinamicamente pela classe Platform através do comando System.load(“...TerraLib_binding_java_d.dll”) visto na Figura 7. No exemplo abaixo a biblioteca TerraLib é iniciada pelo comando te.Platform.start () e ao final é encerrada com te.Platform.stop (). A partir de sua iniciação é possível utilizar as classes Java para abrir e manipular fontes de dados vetoriais como feito no exemplo abaixo através dos comandos para instanciar objetos DataSource, DataSourceTransactor e DataSet ou realizar comandos estáticos a partir das classes como DataSourceFactory. import te.gm.Envelope; import te.gm.GeometryFactory; import te.gm.Point; public final class DataAccessExample { public static void main(String args[]) throws java.io.IOException { // Iniciacao da biblioteca TerraLib te.Platform.start(); // Construcao e abertura de um DataSource te.da.DataSource datasource = te.da.DataSourceFactory.make("DataSource=OGR&C:\\ProjetoInpe\\TerraLib_fernando_b\\data\\shp\\coun try.shp"); datasource.open(); // Manipulacao de um datset do DataSource te.da.DataSourceTransactor transactor = datasource.getTransactor(); te.da.DataSet dataset = transactor.getDataSet("country"); while(dataset.moveNext()){ System.out.println("Field 0:" + dataset.getAsString(0)); } // Encerramento da biblioteca TerraLib te.Platform.stop(); } } Figura 15 – Programa exemplo das classes em Java 3.6 Exemplo de aplicativo para leitura e apresentação gráfica de arquivo vetorial O aplicativo mostrado na Figura 16 foi desenvolvido como exemplo de leitura de datasets vetoriais a partir das classes Java que utilizam código C++ nativo da biblioteca TerraLib. A seguinte seqüência é possível realizar: 1. Abrir uma fonte de dados (“DataSource”), no caso um arquivo “shape”; 2. Carregar “DataSourceCatalog” para obter informações sobre o conteúdo do “DataSource”, como o número de “DataSet”s disponíveis e qual deles contém dados geométricos; 3. Abrir o “DataSet” que contém dados geométricos; 4. Ler os dados geométircos do “DataSet”; 5. Criar um objeto “Canvas”, uma tela para criação de imagem, considerando largura, altura e escala mais adequadas para o desenho; 6. Desenhar as geometrias sobre o “Canvas” 7. Gerar uma imagem a partir da tela criada e apresentar. Figura 16 – Aplicativo para abertura de datasets vetoriais 4. Conclusões Este artigo apresentou os conceitos e técnicas básicas para o desenvolvimento de uma interface Java para a TerraLib5 que são aplicações híbridas envolvendo Java a interface nativa Java (JNI) e biblioteca dinâmica nativa criadas a partir de código C++ para acesso a biblioteca TerraLib. Além de apresentarmos as técnicas básicas para implementar tais aplicações, foram apresentados, ainda alguns exemplos que ilustram as diferentes modalidades de interface. Agradecimentos Gostaríamos de agradecer a Gilberto Ribeiro de Queiroz e Karine Reis Ferreira, profissionais do Inpe que participam ativamente no desenvolvimento do projeto da biblioteca TerraLib e que nos orientaram neste trabalho. Referências Bibliográficas Felipe Carasso, Arndt von Staa. Utilizando JNI para Adicionar Implementação Nativa C ou C++ a Programas JAVA. 2002. 35 p. (ISSN 0103-9741). Dissertação (Monografias em Ciência da Computação) – Pontifíca Universidade Católica do Rio de Janeiro – Rio de Janeiro .2002. INPE-DPI. O aplicativo TerraView. Disponível em: <http://www.dpi.inpe.br/terraview>. Acesso em: Junho 2010.