Interfacer du Java avec du C (JDK 1.1)

Demarche a suivre

Il y a du progres par rapport a la version 1.0 grace entre autres a l'apparition de la Java Native Interface (JNI) qui a deux grands buts : elle permet l'appel de fonctions natives depuis JAVA et elle offre un ensemble de fonctions standards d'inferfacage. On peut alors creer, detruire des objets JAVA et bien sur avoir acces a leurs methodes.

L'interet d'un tel interfacage apparait si l'on dispose d'un code natif tres performant ou trop gros pour que tout soit recrit. L'exemple ci-dessous montre comment envoyer des ordres UNIX en passant par la commande system du C.
Le principe est d'ecrire un fichier qui contient un main appelant une classe dont l'implementation est en C. 

Un fichier distinct du main (mais pas necessairement) va decrire cette classe (appelee sur la figure Toto et dans l'exemple ci-dessous Interface2) qui inclut les fonctions ou procedures C . Dans cette classe, on declarera le profil des fonctions C avec un status native. Puis on chargera la librairie dans un bloc static qui n'est pas une methode.

On verifiera tout d'abord que les fichiers sont accessibles grace aux variables d'environnement :
CLASSPATH et LD_LIBRARY_PATH
Les fichiers seront compiles separement et un fichier souche special (stub) sera genere automatiquement (contrairement a la version 1.0) pour finir l'interfacage. On ecrit donc directement trois fichiers (deux .java et un .c).

Programme (2 fichiers differents) :
// fichier system2.java

public class system2

{

    static Interface2 unix ;

    public static void main(String args[])

      unix = new Interface2() ; 

      unix.hello() ;

      System.out.println("Je demande un <ls>.") ; 

      unix.ls() ;

      System.out.println("Je demande un <pwd>.") ; 

      unix.pwd(); 

      System.out.println("Une commande plus complexe.") ; 

      unix.passe_parametre() ; // methode java qui va appeler du C

    }
}


// fichier Interface.java

class Interface2

    // procedure C 

    public native void hello() ;

    public native void ls() ;

    public native void pwd() ;

    public native void passe_param(String java_string, int entier, float flottant, char caractere) ;

    static

      // charge la librairie libma_lib.so

      System.loadLibrary("ma_lib") ; 

    }

    // variables utilisees pour l'echange java/c

    public String java_string ;

    public int entier ;

    public float flottant ;

    public char caractere ;

    public void passe_parametre()

    {

      java_string = new String("Voici une chaine") ;

      entier = 10 ;

      flottant = (float) 3.14159 ;

      caractere = 'q' ;

      passe_param (java_string, entier,  flottant, caractere) ; // fonction C

    }
}
Il faut ensuite compiler ce programme puis generer un .h par :
javac system2.java
javac Interface2.java
javah -jni Interface2
On ecrit ensuite le fichier.c (la lecture du fichier .h genere par javah donne le nom des fonctions) :
/* Fichier Interface2Imp.c dont la compilation va donner un fichier appele libma_lib.so */

#include <jni.h>

#include <stdio.h> 

#include "Interface2.h"

/* voir le fichier Interface.h pour le profile des fonctions C a ecrire */

JNIEXPORT void JNICALL Interface2_hello(JNIEnv *env, jobject obj)

{

    printf("Bonjour le monde\n");
}

JNIEXPORT void JNICALL Interface2_ls(JNIEnv *env, jobject obj) 

    system("ls -l"); return; 
}

JNIEXPORT void JNICALL Interface2_pwd(JNIEnv *env, jobject obj) 

    system("pwd"); return; 
}

JNIEXPORT void JNICALL void Interface2_passe_1param(JNIEnv *env, jobject obj, jstring j_chne, jint j_entier, jfloat j_reel, jchar j_car) 

    const char *c_chne = (*env)->GetStringUTFChars(env, j_chne, 0); 

    fprintf(stdout,"$s $d $f $c\n",c_chne, j_entier, j_reel, j_car);

    (*env)->ReleaseStringUTFChars(env, j_chne, c_chne);

    return ;

}

// NB : j'aimerai bien savoir pourquoi javah m'oblige a appeler ma fonction Interface2_passe_1param au lieu de Interface2_passe_param !

Avec le JDK 1.1, un fichier souche qui est ecrit en C (stub file) est genere automatiquement. Il va finir de realiser l'interface entre la classe Java et le programme natif : 

On dispose maintenant de deux fichiers .c qu'il faut compiler et mettre dans une librairie qui s'appelera ici libma_lib.so (soit sous Solaris) :
gcc -shared -I/usr3/Java/jdk1.1.4/include -I/usr3/Java/jdk1.1.4/include/solaris Interface2Imp.c -o libma_lib.so
Attention :

En resume :
Fichiers sources a ecrire
Fichiers generes
librairie
system2.java
Interface2.java
Interface2Imp.c
system2.class
Interface2.class
Interface2.h
libma_lib.so

Correspondance des types JAVA/C

Pour les types primitifs on a la correspondance (pour la declaration des parametres dans le code natif) :
Type C
Type JAVA
Taille (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 
Le passage de parametres, dans le cas des types simples, ne pose pas de probleme de conversion et l'affectation peut ensuite se faire directement (i.e. var_c = (type_C) var_java_de_type_equivalent).

De meme, les objets JAVA sont designes en C par :

Le passage d'une variable d'un objet JAVA (non "simple") a son equivalent C n'est pas immediat car le codage n'est pas le meme (i.e. Unicode et UTF-8). Il est necessaire d'utiliser des fonctions de conversion. Ces fonctions sont des methodes issues de l'objet d'environement (JNIEnv).

Appel de methodes JAVA depuis le C

L'operation se deroule en plusieurs phases :
  1. on recupere un pointeur de classe sur l'objet JAVA :
  2. (*env)->GetObjectClass(env, obj)
  3. on recupere un pointeur sur la classe que l'on veut utiliser (il faut connaitre son nom) :
  4. (*env)->GetMethodID(env, pointeur_sur_l_objet, "nom_fonction", "profile_des_parametres");

    Le profile des parametres suit la syntaxe suivante :

  5. on appelle la methode :
(*env)->CallVoidMethod(env, obj, pointeur_sur_la_methode, parametre);

Exemple type d'une procedure C utilisant une methode d'un objet JAVA :
JNIEXPORT void JNICALL Java_Callbacks_nativeMethod(JNIEnv *env, jobjectobj, jint parametre) 

    jclass obj_a_appeler = (*env)->GetObjectClass(env, obj); 

    jmethodID methode = (*env)->GetMethodID(env, obj_a_appeler, "methode_a_appeler", "(I)V"); 

    (*env)->CallVoidMethod(env, obj, methode, parametre); 

}
Le programme complet :

Conclusion

JNI offre encore bien d'autres possibilites (acces aux variables de classe, exceptions, multi-threading, ...). On pourra se reporter au tutorial JAVA de Sun pour ces points tres particuliers mais cette page donne l'essentiel des mecanismes de base de l'interfacage.

La JNI offre un reel progres par rapport a la version 1.0 du JKD et permet d'interfacer proprement et relativement facilement JAVA avec C (C++ est bien sur egalement supporte).


Ph. RIS 1997