Jni
(Java Native Interface)
SOMMAIRE
I Présentation
II Exemple
III Correspondance des types
1 Types primitifs
2 Types objets
IV - Chaînes de caractères
A Exemple
B Quelques fonctions
V - Tableaux
1 Tableau d'éléments de types primitifs
2 Tableau d'objets
VI Objets
A - Exemple
VII - Appels de méthodes
1 Méthodes d'instance
A Exemple
2 Méthodes de classe
3 Méthodes d'instance de la super classe
4 Correspondance des signatures
VIII Accéder aux variables
1 Variables de classe
A Exemple
2 Variables d'instance
IX Levée d'exceptions
A - Exemple
X - Références globales et références locales
1 Références globales
A Exemple
2 Références locales
XI Threads
XII Invoquer une JVM
XIII Conclusion
PRESENTATION
JNI est l'interface qui permet de lier un programme Java à du code natif C et/ou C++.
Un programme Java peut invoquer des fonctions natives et inversement un programme natif peut invoquer des méthodes Java et une JVM.
Une méthode native peut créer des objets Java, elle peut aussi utiliser les objets créés par l'application Java. Les applications Java peuvent traiter les exceptions des méthodes natives.
D'une façon générale JNI permet:
Attention, la portabilité n'est plus assurée.
EXEMPLE
L'exemple présenté affiche, sous Windows, "Hello World !" à l'écran. Le programme Java invoque la méthode native écrite en C.
La procédure à suivre est la suivante :
Programme java :
public class NativeEssai { public native void AfficheHello();
//Chargement de la librairie dynamique hello.dll static { System.loadLibrary("hello"); }
public static void main (String[] args) { new NativeEssai().AfficheHello(); } }
Fichier Header NativeEssai.h :
#include <jni.h>
#ifndef _Included_NativeEssai
#define _Included_NativeEssai
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: NativeEssai
* Method: AfficheHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_NativeEssai_AfficheHello(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
Le format de la fonction du fichier d'en-tête est le suivant :
JNIEXPORT <type retourné> JNICALL <prefix>_<nom de la classe>_<nom de méthode>(JNIEnv *, jobject, <Arguments>,< >,)
Programme Hello.c
#include <jni.h>
#include "NativeEssai.h"
#include <stdio.h>
JNIEXPORT void JNICALL Java_NativeEssai_AfficheHello(JNIEnv *env, jobject obj) {
printf("Hello World !");
}
· JNIEnv *env
Il s agit de l environnement de la machine Java associé au "Thread" courant, (le Thread ayant engendré l appel de la méthode native AfficheHello).
Les méthodes natives manipulent les objets Java à travers le pointeur d'interface env.
C'est par le pointeur env que l'on accéde aux fonctions de JNI.
· jobject j
Il s agit de l objet receveur du message AfficheHello(), ici l instance créée dans la méthode main.
· En résumé
A chaque appel d une méthode native sont transmis par la machine Java un environnement,
l objet receveur ou la classe si c'est une méthode classe et éventuellement les paramètres.
CORRESPONDANCE DES TYPES
1 Types primitifs
Les méthodes natives accédent directement aux types primitifs de Java. Le tableau ci-dessous montre la correspondance.
Type Java |
Type Natif |
Taille en bits |
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 |
2 Types objets
Les objets sont passés par références. Ils héritent tous du type jobject :
CHAÎNES DE CARACTERES
Le type jstring est différent du type (char *), aussi les méthodes natives doivent utiliser des fonctions qui assurent la correspondance entre les types.
Par exemple la fonction GetStringUTFChars permet de passer du type unicode au type UTF8 (celui-ci acceptant 0 en fin de chaîne).
A - Exemple :
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) {
Const char str = (*env)->GetStringUTFChars(env, prompt, 0);
Printf("%s", str);
(*env)->ReleaseStringUTFChars(env, prompt, str);
}
La fonction ReleaseStringUTFChars informe la MV qu'elle peut récupérer la mémoire utiliser par prompt.
B - Quelques fonctions
· GetStringUTFChars : Convertit une chaîne unicode en UTF8.
· ReleaseStringUTFChars : "Libère" la place mémoire.
· NewStringUTF : Construit une chaîne.
TABLEAUX
Comme pour les chaînes de caractères, les tableaux ne sont pas accessibles directement. Il faut utiliser les fonctions de JNI.
1 Tableau d'éléments de types primitifs
A - Exemple
JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintarray arr) {
int i, sum = 0;
jint len = (*env)->GetArrayLength(env, arr);
jsize *body = (*env)->GetIntArrayElements(env, arr, 0);
for (i = 0; i <= len; i++) { sum = sum + body[i]; }
(*env)->ReleaseIntArrayElements(env, arr, body, 0);
return sum;
}
On récupére, dans un premier temps, la longueur du tableau, puis le tableau lui-même et ensuite on autorise la VM à libérer la mémoire.
La syntaxe des fonctions qui permettent d'acceder aux éléments du tableau est de la forme :
Get<type>ArrayElements
Idem pour la libération de la mémoire :
Release<type>ArrayElements
On peut également travailler sur une partie du tableau. Les fonctions sont du type :
Get/Set<type>ArrayRegion
2 Tableau d'objets
Il n'est pas possible de récupérer la totalité du tableau. Les fonctions prévues accédent à un élément à la fois.
GetObjectArrayElement
SetObjectArrayElement
OBJETS
Il est possible de créer des objets Java à partir de JNI à l'aide de la fonction NewObject.
En Java on instancie la classe A de cette façon : ClasseA newObj = new ClasseA(10,"hello");
L'exemple ci-dessous crée un objet de la classe Objet, le renvoi au programme Java qui l'affiche.
A Exemple
Programmes Java :
public class Appel {
private native Objet AppelNative();
public static void main(String[] args) {
Objet newObj = null;
Appel a = new Appel();
newObj = a.AppelNative();
System.out.println(newObj.getS());
}
static { System.loadLibrary("Librairie"); }
}
public class Objet {
private int i;
private String s;
public Objet(int i, String s) {
this.i = i;
this.s = s;
}
public String getS() { return s; }
}
Programme C :
#include <jni.h>
#include "Appel.h"
JNIEXPORT jobject JNICALL Java_Appel_AppelNative (JNIEnv *env, jobject obj) {
jclass classe = (*env)->FindClass(env, "Objet");
jmethodID mid = (*env)->GetMethodID(env, classe, "<init>", "(ILjava/lang/String;)V");
jint valeur = 10;
jstring chaine = (*env)->NewStringUTF(env, "hello");
jobject objet = (*env)->NewObject(env, classe, mid, valeur, chaine);
return objet;
}
APPELS DE METHODES
Il est possible d'appeler des méthodes Java à partir d'un programme natif. La récursivité est autorisée. La méthode Java peut appeler à son tour la méthode native.
1 Méthodes d'instance
La méthode à suivre est la suivante :
1 Récupérer la classe de l'objet par la fonction GetObjectClass
2 Rechercher la méthode par la fonction GetMethodID. Si cette méthode n'existe pas, la fonction renvoie 0.
3 Appeler la méthode par la fonction CallVoidMethod.
La fonction GetMethodID recherche la méthode par son nom et par sa signature.
L'utilitaire javap de Sun donne explicitement le nom et la signature des méthodes.
A Exemple
L'exemple ci-dessous appelle la méthode Affiche().
Appel.java
class Appel {
private native void AppelNative();
private void Affiche() { System.out.println("Méthode Affiche()"); }
static { System.loadLibrary("Librairie"); }
public static void main(String[] args) {
Appel a = new Appel();
a.AppelNative();
}
}
Compiled from Appel.java
class Appel extends java.lang.Object {
static {};
/* ()V */
Appel();
/* ()V */
private void Affiche();
/* ()V */
private native void AppelNative();
/* ()V */
public static void main(java.lang.String[]);
/* ([Ljava/lang/String;)V */ }
Le programme C :
#include <jni.h>
#include <stdio.h>
#include "Appel.h"
JNIEXPORT void JNICALL Java_Appel_AppelNative (JNIEnv *env, jobject obj) {
jclass cls = (*env)->GetObjectClass(env, obj);
jmethodID mid = (*env)->GetMethodID(env, cls, "Affiche", "()V");
(*env)->CallVoidMethod(env, obj, mid); //Provoque l'affichage de "Méthode Affiche()"
printf("Fonction AppelNative()"); //Provoque l'affichage de "Fonction AppelNative()"
}
Pour appeler des méthodes qui retournent des valeurs, il faut utiliser les fonctions adéquates :
Call<type>Method
2 Méthodes de classe
Le principe est le même, il suffit d'utiliser les méthodes :
GetStaticMethodID
CallStatic<type>Method
3 Méthodes d'instance de la super classe
Il est possible d'appeler une méthode d'instance de la super classe en utilisant la famille de fonctions CallNonvirtual<type>Method. Il aura fallut au préalable récupérer l' ID de la méthode par GetMethodID.
4 Correspondance des signatures
Signature |
Type Java |
Z | Boolean |
B | Byte |
C | Char |
S | Short |
I | Int |
J | Long |
F | Float |
D | Double |
L Nom de la classe; | Nom de la classe |
[ type | Type[] |
(types arguments)type retourné | Type de la méthode |
ACCEDER AUX VARIABLES
Il est possible d'acceder, à partir de méthodes natives, aux variables de classe comme aux variables d'instance.
Le principe est le même dans les deux cas seules les fonctions JNI diffèrent.
1 Variables de classe
Procédure :
Récupérer l'ID de la variable par son nom et sa signature. Puis travailler dessus à l'aide des fonctions appropriées.
A Exemple
Cet exemple montre comment récupérer la variable si à partir de la méthode native. Affiche sa valeur, la modifie et enfin l'affiche à partir du programme Java.
Programme Java
class FieldAccess {
static int si;
private native void accessFields();
public static void main(String args[]) {
FieldAccess c = new FieldAccess();
c.si = 100;
c.accessFields();
System.out.println("In Java:");
System.out.println(" FieldAccess.si = " + FieldAccess.si);
}
static { System.loadLibrary("Librairie"); }
}
Programme C
#include <stdio.h>
#include <jni.h>
#include "FieldAccess.h"
JNIEXPORT void JNICALL Java_FieldAccess_accessFields(JNIEnv *env, jobject obj) {
jclass cls = (*env)->GetObjectClass(env, obj);
jfieldID fid;
jint si;
printf("In C:\n");
fid = (*env)->GetStaticFieldID(env, cls, "si", "I");
si = (*env)->GetStaticIntField(env, cls, fid);
printf(" FieldAccess.si = %d\n", si);
(*env)->SetStaticIntField(env, cls, fid, 200);
}
Comme d'habitude, on récupère la classe dans cls. On récupère l'ID de la variable dans fid. On récupère si. On l'affiche puis on la modifie.
On notera que les fonctions nécessaires contiennent le mot Static.
2 Variables d'instance
Le principe est le même. Il suffit d'utiliser les fonctions :
GetFieldID
Get<type>Field
Set<type>Field
Dans les deux cas, on déterminée la signature à l'aide de l'utilitaire javap.
LEVEE D'EXCEPTIONS
Avec JNI on peut lever des exceptions Java. L'exemple suivant lève une exception traitée par java puis revient au programme C.
A Exemple
Programme Java :
class CatchThrow {
private native void catchThrow() throws IllegalArgumentException;
static { System.loadLibrary("Librairie"); }
public static void main(String args[]) {
CatchThrow c = new CatchThrow();
try { c.catchThrow(); }
catch (Exception e) { System.out.println("En Java: "); }
}
}
Programme C :
#include <jni.h>
#include "CatchThrow.h"
JNIEXPORT void JNICALL Java_CatchThrow_catchThrow(JNIEnv *env, jobject obj) {
jclass Exc;
Exc = (*env)->FindClass(env, "java/lang/IllegalArgumentException");
(*env)->ThrowNew(env, Exc, "\nException en C: ");
}
On déclare la méthode native pouvant levée une exception de type IllegalArgumentException
La fonction ThrowNew lève l'exception correspondante dans le programme Java, puis affiche le message indiqué.
La fonction ExceptionOccured détermine si une exception à été levée.
ExceptionDescribe affiche la trace de la pile.
ExceptionClear remet à zéro l'exception en cours.
REFERENCES GLOBALES ET REFERENCES LOCALES
Par défaut JNI crée des références locales. Ces références sont détruites quand la fonction se termine.
JNI propose des fonctions qui permettent de créer des références globales, c'est dire réutilisables après être sorti de la fonction.
1 Références globales
A Exemple
Static jclass cls = 0;
Static jfieldID fid;
JNIEXPORT void JNICALL Java_FieldAccess_accessFields(JNIEnv *env, object obj) {
If (cls == 0) {
Jclass cls1 = (*env)->GetObjectClass(env, obj);
Cls = (*env)->NewGlobalRef(env, cls1);
Fid = (*env)->GetStaticFieldID(env, "si", "I");
}
}
Au deuxième appel de la fonction cls et fid pointe sur les bons objets.
2 Références locales
La fonction DeleteLocalRef(env, <objet>) autorise la récupération de la mémoire par le ramasse miettes de Java.
THREADS
JNI à l'équivalent du mot-clé synchronised de Java.
(*env)->MonitorEnter(env, obj);
. . .
(*env)->MonitorExit(env, obj);
est équivalent à :
synchronised (obj) {
. . .
}
- Le pointeur d'interface (JNIEnv *) n'est valide que dans le thread courant.
Il n'est pas possible de passer ce pointeur d'un thread à un autre.
- Il n'est pas possible non plus de passer une référence locale d'un thread à un autre.
On doit la transformer en référence globale.
INVOQUER UNE JVM
Une caractéristique importante de JNI est d'autoriser le lancement de Java Virtual Machine à partir d'un programme natif.
L'exemple ci-dessous montre comment invoquer une jvm sous Windows à partir d'un programme C/C++ :
La marche à suivre est la suivante :
- Définir les pointeurs nécessaires au lancement de la jvm
- Initialiser les arguments
- Charger la libraire jvm.dll
- Initialiser les pointeurs
- Créer la jvm
- Invoquer la méthode main du programme Java
- Détruire la jvm
Programme C :
#include <jni.h>
#include <windows.h>
#define PATH_SEPARATOR ';' /* platform dependent */
#define USER_CLASSPATH "." /* directory where Sample.class is present */
typedef jint (*P_JNI_GetDefaultJavaVMInitArgs)(void *args);
typedef jint (*P_JNI_CreateJavaVM)(JavaVM **pvm, JNIEnv ** penv, void *args);
void __cdecl main(int argc, char **argv) {
char classpath[1024]; /* Class Path */
JNIEnv *env = NULL; /* Environment */
JavaVM *jvm = NULL; /* Virtual Machine */
JDK1_1InitArgs vm_args; /* Initializing arguments */
Jint res = -1;
jclass cls;
jmethodID mid;
jstring jstr;
jobjectArray args;
HANDLE hLib = NULL;
/* Pointer to required functions */
P_JNI_GetDefaultJavaVMInitArgs pfnGetDefaultJavaVMInitArgs = NULL;
P_JNI_CreateJavaVM pfnCreateJavaVM = NULL;
/* Load the library */
printf("Loading Library .... <%s>\n",argv[1]);
hLib = LoadLibrary(argv[1]);
/* Store the function pointer for getting default arguments */
pfnGetDefaultJavaVMInitArgs = (P_JNI_GetDefaultJavaVMInitArgs)GetProcAddress(hLib,"JNI_GetDefaultJavaVMInitArgs");
/* IMPORTANT: specify vm_args version # if you use JDK1.1.2 and beyond */
vm_args.version = 0x00010001;
/* Get the default arguments */
if(pfnGetDefaultJavaVMInitArgs != NULL)(*pfnGetDefaultJavaVMInitArgs)(&vm_args);
/* Append USER_CLASSPATH to the end of default system class path */
if(vm_args.classpath != NULL) sprintf(classpath, "%s%c%s", vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
else sprintf(classpath, "%s", USER_CLASSPATH);
vm_args.classpath = classpath;
/* Store the function pointer for creating the VM */
pfnCreateJavaVM = (P_JNI_CreateJavaVM) GetProcAddress(hLib, "JNI_CreateJavaVM");
/* Create the Java VM */
if(pfnCreateJavaVM != NULL) res = (*pfnCreateJavaVM)(&jvm,&env,&vm_args);
cls = (*env)->FindClass(env, argv[2]);
mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V");
jstr = (*env)->NewStringUTF(env, "Hello World!!!");
args = (*env)->NewObjectArray(env, 1, (*env)->FindClass(env, "java/lang/String"), jstr);
(*env)->CallStaticVoidMethod(env, cls, mid, args);
(*jvm)->DestroyJavaVM(jvm);
}
Programme Java :
import java.lang.*;
import java.io.*;
public class Sample {
static public void main (String argv[]) {
System.out.println("Version : " + System.getProperty("java.version"));
System.out.println("Vendeur : " + System.getProperty("java.vendor"));
}
}
Programme Batch pour le jdk 1.2 :
@ECHO OFF
IF "%OLDPATH%" == "" SET OLDPATH=%PATH%
SET PATH=E:\JAVA1~1.2\BIN;E:\JAVA1~1.2\JRE\BIN\CLASSIC;%OLDPATH%
SET CLASSPATH=.;
SET JVMDLL=jvm
SET PATH
SET CLASSPATH
INVOKEJVM %JVMDLL% Sample
CONCLUSION
JNI propose toutes les fonctions et les types nécessaires permettant le passage de Java vers C/C++ et C/C++ vers Java.
Ces fonctions font parties d'une structure accessible via le pointeur d'environnement env.
Bien que les méthodes natives soient capables de faire plein de choses que Java ne fait pas. Leurs utilisations rend les programmes moins portables, moins sécurisés et surtout plus compliqués à développer et à maintenir.
D'autre part, chaque vendeur de jvm doit implémenter les fonctions de JNI, aussi il est tout à fait possible que ces implémentations varient d'un constructeur à l'autre. De même qu'à chaque version du jdk JNI peut changer.
Bibliographie:
La spécification:
http://java.sun.com/products/jdk/1.2/docs/guide/jni/index.html
Le tutorial de Sun:
http://java.sun.com/docs/books/tutorial/native1.1/index.html
Code JVM:
http://codeguru.developer.com/java/articles/216.shtml
Notes de cours de Jean-Michel Douin:
http://java.cnam.fr/public_html/Iagl99/douin/java_jni.zip