Introduction au dévellopement de Methodes Natives Partie II : Echange de données entre java et C++. Auteur: Frank Sauvage. 1 \ Correspondance des données entre Java et C++ Dans le chapitre precedent, nous avons vu comment créer des fonctions natives simples en C++. Cependant dans le cadre d'une utilisation réelle, il est neccessaire de pouvoir echanger des données entre les 2 langages utilisés. Pour cela, la JNI fournit des types prédéfinis pouvant etre directement utilisés dans le langage C++. Le tableau suivant donne la correspondance de types alors utilisables. Type java Type natif Equivalent C++ boolean jboolean 8, unsigned byte jbyte 8 char jchar 16, unsigned short jshort 16 int jint 32 long jlong 64 float jfloat 32 double jdouble 64 void void n/a Les types ainsi presentés sont en fait des typedef definis dans le fichier d'include jni.h, ce qui à pour effet de pouvoir etre utilisés directement sans conversions de format. Il est alors juste neccessaire de garder à l'esprit que certains types sont equivalent à d'autres en C++, mais que la correspondance n'est pas forcement la plus évidente comme avec les Char codés sur 16bits en java. Voici un extrait du jni.h : typedef unsigned char jboolean; typedef unsigned short jchar; typedef short jshort; typedef float jfloat; typedef double jdouble; Note: le type jint est lui defini dans un fichier séparé dépendant du systéme d'exploitation. En effet, la taille des registres n'étant pas toujours la même, il est neccessaire de s'assurer de la compatibilité pour chaque plateforme cible où se trouve une machine virtuelle.
2 \ Premier exemple : une fonction pour additionner deux nombres. Notre premier exemple va nous permettre de voir comment echanger des données entre java et C++. pour cela, nous allons tacher d'implementer une methode native qui acceptera en parametre deux int et qui rendra une autre int contenant la somme des deux. Etape 1 : ecriture du code java : package testjni; public class FonctionAddition // chargement de la librairie associée static System.loadLibrary("testJNI_FonctionAddition"); //declaration de la fonction native. ne rends pas de resultat. public static native int fonctionaddition(int nb1, int nb2); public static void main(string[] args) int nb1 = 2; int nb2 = 4; System.out.println("l'addition de "+nb1+" et de "+nb2); System.out.println(" a pour resultat:"+fonctionaddition(nb1,nb2)); Etape 2 : generation du header C++ : comme dans le premier chapitre, on genere le resultat suivant a partir de la classe compilée. /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class testjni_fonctionaddition */ #ifndef _Included_testJNI_FonctionAddition #define _Included_testJNI_FonctionAddition extern "C" /* * Class: testjni_fonctionaddition * Method: fonctionaddition * Signature: (II)I */ JNIEXPORT jint JNICALL Java_testJNI_FonctionAddition_fonctionAddition (JNIEnv *, jclass, jint, jint); comme on peut le voir ici, on obtient alors le prototype de la fonction suivante : JNIEXPORT jint JNICALL Java_testJNI_FonctionAddition_fonctionAddition (JNIEnv *, jclass, jint, jint); dans ce cas, précis on constate la présence de deux jint supplementaire vis à vis des parametres spéciaux. Ce sont donc bien les int que nous avons tenté de passer à la methode native.
Etape 3 : écriture du code C++ : pour écrire le code des fonctions en C++, il suffit de recuperer les prototypes de fonctions generés par javah et ensuite de les modifier afin de nommer les variables dont on devra se servir. Note : il n'est pas utile de renommer ces memes variables dans le prototype du fichier d'entete, cela est meme deconseillé afin de garder intacte l'integrité dudit fichier. #include "testjni_fonctionaddition.h" // on recupere le prototype depuis le fichier d'entete // et on nomme les parametres dont on aura besoin JNIEXPORT jint JNICALL Java_testJNI_FonctionAddition_fonctionAddition (JNIEnv *, jclass, jint nb1, jint nb2) // on recupere les valeurs de nb1 et de nb2 dans des variables locales. // cela est inutile, mais on le fait ici pour montrer l'equivalance des types. signed int a = nb1; signed int b = nb2; signed int res = a + b; return res; les types natifs sont utilisables directement, mais l'exemple ci dessus montre qu'il est possible sans aucune difficulté de les affecter à des variables declarées directement en C++. Etape 4 : compilation et execution : maintenant que nous avons ecris notre programme, nous pouvons donc le compiler et l'executer. Rappel sur la compilation : la commande suivante lance la compilation de notre librairie : cl /LD testjni_fonctionaddition.cpp A l'execution, l'exemple sur l'addition affiche le resultat suivant : l'addition de 2 et de 4 a pour resultat:6 Conclusion : l'exemple simple que nous venons de voir demontre la possibilité de passer des données de type intrinséques à Java grâce aux types natifs fournis par l'interface JNI.
3 \ utilisation des strings avec les méthodes natives. En java, une chaine de caractere est nativement un objet de base du langage surveillé par le ramasse miette. Il est donc neccessaire de passer par des fonctions de conversions afin de pouvoir les utiliser dans les methodes natives. l'exemple suivant donne une idée de l'utilisation des String dans les methodes natives. Il defini deux methodes, l'une pour recuperer une string, l'autre pour la passer en parametre. Etape 1 : declaration des methodes en java : De la meme maniere que pour toute methode native, on passe une string comme tout autre type de donnée. La difference au niveau implementation se situera au niveau du code C++. package testjni; public class FonctionsString // chargement de la librairie associée static System.loadLibrary("testJNI_FonctionsString"); // exemple de fonction utilisant une string en paramétre. public static native void ecrisstring(string chaine); // exemple de fonction renvoyant une String. public static native String recuperestring(); public static void main(string[] args) // on recupere la string a partir de la methode getstring(); String chaine = recuperestring(); // on imprime la string recuperée grace a la methode printstring() ecrisstring(chaine);
Etape 2 : Generation du fichier d'entete C++ : La generation de l'entete est maintenant une action familiere. Le fichier suivant est donc obtenu : /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class testjni_fonctionsstring */ #ifndef _Included_testJNI_FonctionsString #define _Included_testJNI_FonctionsString extern "C" /* * Class: testjni_fonctionsstring * Method: ecrisstring * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_testJNI_FonctionsString_ecrisString (JNIEnv *, jclass, jstring); /* * Class: testjni_fonctionsstring * Method: recuperestring * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_testJNI_FonctionsString_recupereString (JNIEnv *, jclass); Etape 3 : Ecriture du code C++ : L'ecriture du code C++ va demander cette fois l'utilisation de methodes fournies pas la JNI. Comme precisé precedement, une String est un objet qui neccessite plusieurs etapes pour etre utilisé. Tout d'abord il est neccessaire cette fois de nommer l'environnement JNI qui nous permettra ainsi d'appeller des fonctions de l'api. Dans notre exemple, nous le nomeront env, ce qui sera son nom par defaut durant le reste de ce document (La signification precise de JNIEnv et de jclass sera etudiée plus tard). Lorsque la fonction recoit la chaine en parametre, il est neccessaire de la convertir en chaine java. Pour cela l'environnement JNI fourni une fonction dediée : la methode GetStringUTFChars() peut ainsi etre utilisée : const char *chainec = env->getstringutfchars(chaine,0); cette methode accepte en parametre la chaine java a convertir ( le second parametre precise si il s'agit d'une copie ou d'un accés à la chaine originale).
On peut donc ecrire les methodes C++ suivantes : #include "testjni_fonctionsstring.h" #include <iostream> JNIEXPORT void JNICALL Java_testJNI_FonctionsString_ecrisString (JNIEnv *env, jclass, jstring chaine) // recuperation de la chaine passée en parametre const char *chainec = env->getstringutfchars(chaine,0); // on ecris a l'ecran la chaine printf(chainec); // on indique que le C n'a plus besoin de la chaine env->releasestringutfchars(chaine,chainec); JNIEXPORT jstring JNICALL Java_testJNI_FonctionsString_recupereString (JNIEnv *env, jclass) // Construction d'une chaîne Java à partir d'une chaîne C++ return env->newstringutf("chaine venant du code C"); comme le montre l'exemple, lorsque l'on utilise une chaine java, il est neccessaire de garder à l'esprit qu'il est obligatoire pour des raisons de gestions memoire de preciser à la fin des traitements que l'objet String n'est plus utilisé. Dans le cas contraire, le ramasse miette ne sera pas en mesure de liberer l'espace memoire ainsi alloué. La creation d'une String java, quand à elle, est assez simple, il suffit d'invoquer la methode NewStingUTF avec comme parametre la chainec a convertir. Il existe diverses autres methodes permettant de travailler sur des chaines dans le cadre des methodes natives. Voici la liste de ces fonctions avec le lien vers la reference SUN concernée. fonctions d'utilisation : NewString GetStringLength GetStringChars ReleaseStringChars NewStringUTF GetStringUTFLength GetStringUTFChars ReleaseStringUTFChars GetStringRegion GetStringUTFRegion GetStringCritical Release StringCritical comme l'indiquent leurs noms, ces methodes permettent de recuperer les tailles, une partie de chaine, de les créer et de les implementer dans un environnment multithread.
4 \ utilisation de tableaux avec les methodes natives. Au meme titre que les chaines de caracteres, les tableaux en java sont en fait des objets. Il existe alors des methodes destinées a leur utilisation dans des methodes natives cependant avant de regarder un exemple, il est neccessaire de comprendre le fonctionnement interne de la machine virtuelle java en ce qui concerne les tableaux. Pour chaque type natif, java defini un tableau, il existe donc huit tableaux natifs plus un pour les tableaux d'objets. L'interface JNI fourni donc un double des fonctions pour chacun de ces types de tableaux. le tableau suivant donne la correspondance entre le type de tableau voulu et la methode correspondante à utiliser pour les créer. NewBooleanArray() NewByteArray() NewCharArray() NewShortArray() NewIntArray() NewLongArray() NewFloatArray() NewDoubleArray() jbooleanarray jbytearray jchararray jshortarray jintarray jlongarray jfloatarray jdoublearray
Etape 1 : declaration des methodes en java : L'exemple suivant montre juste comment ecrire deux fonctions, l'une renvoyant un tableau, l'autre l'utilisant : package testjni; public class FonctionsTableau static System.loadLibrary("testJNI_FonctionsTableau"); private static native void printtableau(int [] tableau); public static native int [] gettableau(); public static void main(string[] args) // recupere un tableau int [] montableau = gettableau(); // imprime le tableau printtableau(montableau); Etape 2 : Generation du fichier d'entete C++ : /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class testjni_fonctionstableau */ #ifndef _Included_testJNI_FonctionsTableau #define _Included_testJNI_FonctionsTableau extern "C" /* * Class: testjni_fonctionstableau * Method: printtableau * Signature: ([I)V */ JNIEXPORT void JNICALL Java_testJNI_FonctionsTableau_printTableau (JNIEnv *, jclass, jintarray); /* * Class: testjni_fonctionstableau * Method: gettableau * Signature: ()[I */ JNIEXPORT jintarray JNICALL Java_testJNI_FonctionsTableau_getTableau (JNIEnv *, jclass);
Etape 3 : Ecriture du code C++ : comme precisé auparavant, il est neccessaire d'utiliser une methode par type de tableau. Dans notre cas, nous utiliserons des tableaux d'int, ce qui nous poussera à utiliser les methodes dediées. Pour changer le type de données contenu dans le tableau, il suffit de changer dans le prototype de la methode int par le type choisi. #include "testjni_fonctionstableau.h" #include <iostream> JNIEXPORT void JNICALL Java_testJNI_FonctionsTableau_printTableau (JNIEnv *env, jclass, jintarray montableau) // longueur du tableau jsize longueur = env->getarraylength(montableau); // Création d'un tableau C++ de valeur entière jint *tableauc = env->getintarrayelements(montableau,0); // impression du tableau for(int i=0;i<longueur;i++) printf("valeur a l'indice %d est egale a :%d\n",i,longueur); // on precise a la VM que le code C++ a fini de travailler sur le tableau env->releaseintarrayelements(montableau,tableauc,0); JNIEXPORT jintarray JNICALL Java_testJNI_FonctionsTableau_getTableau (JNIEnv *env, jclass) // on fixe la taille du tableau int taille = 3; // on crée le tableau jintarray montableau = env->newintarray(taille); // on peut ici travailler sur le tableau // on renvoi le tableau return montableau;
Etape 4 : Compilation et Execution : à l'execution le programme donne le résultat suivant : valeur a l'indice 0 est egale a:3 valeur a l'indice 1 est egale a:3 valeur a l'indice 2 est egale a:3 Note : la methode newintarray crée un tableau dont chacune des valeurs est initialisée pour contenir la taille, ou dans le cas d'un tableau d'objet, des pointeurs nulls. Les fonctions suivantes permettent d'utiliser les tableaux java. Chacune des methodes ici, est un lien vers la reference SUN correspondante. GetArrayLength NewObjectArray GetObjectArrayElement SetObjectArrayElement New<PrimitiveType>Array Routines Get<PrimitiveType>ArrayElements Routines Release<PrimitiveType>ArrayElements Routines Get<PrimitiveType>ArrayRegion Routines Set<PrimitiveType>ArrayRegion Routines GetPrimitiveArrayCritical ReleasePrimitiveArrayCritical 5 \ conclusion: ce document nous a permis de voir comment cette fois de passer des variables simples entre java et ses methodes natives. Il est donc maintenant possible de commencer à ecrire des methodes reellement utilisables, mais laisse cependant un trou pour le prochain document : l'utilisation de methodes d'instance d'objets java à partir du code natif C++ ouvrant ainsi la voix au debut de la programmation objet.