Appeler du Java depuis .Net

Deux mondes s’affrontent: Java et .Net. Chacun choisi son camp, ou choisi les deux… moi j’ai la double nationalité 🙂 Mais quand les deux mondes doivent alors communiquer? Je fais l’interprète. Voila le topo:
J’ai une application .Net qui a besoin de manipuler des classes Java, et pour se faire je passe par C++/CLI: comment avoir un pied dans du .Net et un autre dans du natif C++.

L’avantage de C++/CLI (Common Language Infrastructure) c’est qu’on peut mixer du code managé et non-managé. Je peux donc compiler un assembly .Net en C++, qui lui fait appelle à du pure code C++ natif. Ici en l’occurrence j’utilise "jni.h" pour communiquer avec la JVM à l’aide de jvm.dll.

Rentrons dans le vif du sujet: qu’est-ce que ça donne du coté de mon application C#:

// on démarre la JVM avec mes bons arguments
MathiasJniCpp.JVMWrapper.InitJvm(new String[] { "-Djava.class.path=Mathias.Jni.Java.jar" });

// on créer un objet .Net qui wrappe l'objet Java
// en C++/CLI, le destructeur des classes CLI sert de méthode "Dispose"
using (MathiasJniCpp.MyJavaWrapper javaObject = new MathiasJniCpp.MyJavaWrapper())
{
// faire mumuse avec...
javaObject.People = "Mathias";
Console.WriteLine(javaObject.SayHello());
}

// on libère la JVM
JVMWrapper.ReleaseJvm();

MathiasJniCpp c’est le namespace de mon assembly c++/cli, j’en reparlerai plus tard.
On voit que je manipule une classe JVMWrapper qui me permet de charger une JVM (et de la libérer). J’utilise aussi une classe .Net codé en C++/CLI. En fait, je l’utilise comme un classe C#, ou VB.net etc. C’est une classe « classique » .Net avec des méthodes et des propriétés.

Point intéressant à souligner: pourquoi utiliser using?
Pour rappelle, il y a des mots-clefs en C# très lié au Framework (comme foreach) et using en fait partie. Il prend les objets qui implémente IDisposable et fait appelle à la méthode Dispose() à la fin du bloque. Comme ça, je suis sûr de libérer la classe du coté JVM quand j’en ai plus besoin.

Je ne vais pas montrer tout le code, car trop long et disponible ici.
Mais voici un aperçu de la classe C++:

#include "jni.h"
public ref class MyJavaWrapper
{
private:
  static jmethodID initMethodId;
  static jclass clazz;
  jobject obj;
public:
  /* initialisation des métadata Java/JNI, à voir plus tard */
  static void initJavaMetadata()
  { ... }

  /* une méthode de notre classe Java que l'on wrappe, expliqué aussi plus tard */
  String^ SayHello(String^ people)
  { ... }

  /* constructeur */
  MyJavaWrapper(void)
  {
    // histoire de récupérer toutes les métadatas nécessaires du coté Java
    MyJavaWrapper::initJavaMetadata();

    // construction d'une instance avec le constructeur par défaut
    this->obj = JVMWrapper::env->NewObject(MyJavaWrapper::clazz, MyJavaWrapper::initMethodId);
  }

  /* "destructeur" */
  virtual ~MyJavaWrapper(void)
  {
    JVMWrapper::env->DeleteLocalRef(this->obj);
  }
};

ref class veut dire « c’est une classe .Net ». Mais… ma classe n’implémente pas IDisposable!! Et c’est quoi ce destructeur??
Et oui: le destructeur C++ pour un objet .Net est transformé en la méthode Dispose() et la classe devient alors forcement IDisposable. Pour gérer le finalize allez voir ici.

Je manipule le membre this->obj qui est tout simplement un « pointeur » sur notre objet java. En fait, c’est un jobject qui est un type définie dans "jni.h".
J’utilise la classe JVMWrapper qui me permet de communiquer avec la JVM, et je lui demande de créer un nouvel objet d’une certaine classe jclass avec un certain constructeur jmethodID et j’obtiens ainsi mon jobject.
Étant donnée que la classe et la méthode ne change pas, j’ai rendu ces données static.

Voyons maintenant ce que fait MyJavaWrapper::initJavaMetadata();:

/* initialise les metadata du coté Java */
static void initJavaMetadata()
{
  // si les métadata ne sont pas déjà récupérées...
  if(MyJavaWrapper::clazz == NULL)
  {
    MyJavaWrapper::clazz = JVMWrapper::env->FindClass("mathias/jni/java/MyJavaClass");
    MyJavaWrapper::initMethodId = JVMWrapper::env->GetMethodID(MyJavaWrapper::clazz, "<init>", "()V");
  }
}

C’est la dedans que j’obtiens une fois pour toute la représentation de la classe Java mathias.jni.java.MyJavaClass et la représentation de la méthode <init> avec en paramètre ()V.
Pour comprend le lien avec la classe, il n’y a pas trop de problème: c’est le fully qualified name avec des ‘/’ au lieu des ‘.’.
Mais en ce qui concerne la recherche d’une méthode, ça devient du charabia!!
En fait, <init> est une méthode un peu spéciale: c’est un constructeur.
Ensuite, on spécifie les arguments du constructeur que l’on cherche, et la on tombe sur une syntaxe barbare. Dans notre cas, on cherche le constructeur par défaut c’est à dire qui ne prend pas d’argument.
Mais pour mieux comprendre la syntaxe barbare, voyons d’autres exemples de méthodes:

JVMWrapper::env->GetMethodID(MyJavaWrapper::clazz, "setPeople", "(Ljava/lang/String;)V");

Traduction: je cherche la méthode setPeople qui prend un argument de type java.lang.String et qui retourne void.
Un autre exemple:

JVMWrapper::env->GetMethodID(MyJavaWrapper::clazz, "sayHello", "([Ljava/lang/String;Z;)I");

Traduction: je cherche la méthode sayHello qui prend un argument de type java.lang.String[] et un autre de type boolean et qui retourne un type int.

On retrouve cette syntaxe à beaucoup d’endroits, comme sous Eclipse par exemple. Pour plus d’explication voir la documentation officielle.

Si l’on veut maintenant appeler une méthode Java, on récupère sa représentation tout comme on le fait avec le constructeur, puis on l’invoque sur notre instance:

public ref class MyJavaWrapper
{
private:
  static jmethodID initMethodId;
  static jclass clazz;

  // notre représentation JNI de la méthode "sayHello"
  static jmethodID sayHelloMethodId;

  jobject obj;
public:
  /* initialisation des métadata Java/JNI */
  static void initJavaMetadata()
  { 
    /* initialisation de la classe et du constructeur, comme vu précédemment 
    [...]  */

    // on récupère la représentation de "sayHello"
    JVMWrapper::env->GetMethodID(MyJavaWrapper::clazz, "sayHello", "(Ljava/lang/String;)Ljava/lang/String");
  }

  /* sur l'appelle de cette méthode .Net, on fait appelle à la méthode Java */
  String^ SayHello(String^ people)
  {
    // j'utilise une classe spéciale pour convertir ma String^ .net en natif ou Java
    StringConverter peopleStringConverter(people);
    jstring jPeople = peopleStringConvert.toJava();

    // appelle de la méthode Java, j'ai le droit de caster en jstring car c'est un sous-type de jobject
    jstring jResult = (jstring)JVMWrapper::env->CallObjectMethod(this->obj, sayHelloMethodId, jPeople);

    // conversion du type Java en .Net
    StringConverter resultStringConvert(jResult);
    return resultStringConvert.toDotnet();
  }
};

Voila, maintenant vous savez:

  • obtenir la représentation d’une classe Java
  • obtenir la représentation d’une méthode d’une classe
  • créer une instance d’une classe Java
  • invoker des méthodes sur une instance

En conclusion:
l’API JNI c’est un peux comme utiliser la réflection. Ça a donc des conséquences en termes de performance. Pour information, créer 10000 objet en java prend 625ms, en pure .net ça donne 46ms et en .Net->JNI->Java ça donne 2.687s.
L’API JNI peut sembler barbare au début, mais on s’y fait 🙂 et puis il y a la doc, alors RTFM ;).

C++/CLI c’est de la bombe en termes d’interop. C’est le pont parfait entre le monde .Net et le natif.
L’inconvénient c’est que la syntaxe C++ est lourde. Et elle l’est d’autant plus en C++/CLI car il faut y ajouter les spécificités .Net, et il faut aussi distinguer une instance managée et non-managée, et tout ça passe par de nouveau symboles/mots-clefs.
Les conversions de types entre les deux mondes ne sont pas faites implicitement, et il faut souvent jongler pour avoir le bon type. J’ai par exemple eu des problèmes lors des conversions de String avec JNI: il faut convertir la String^ .net en char* natif pour enfin construire une jstring. Les conversions ont été le plus pénible dans l’histoire.

Enfin, voici le projet complet: Mathias.Jni.CSharp.zip
Un petit rappel des liens utiles:

Publicités

11 réflexions sur “Appeler du Java depuis .Net

  1. Hello j’ai utilise ton programme car j’ai aussi besoin d’appeller du java dans du .Net C#. Je n’arrive pas à le faire marcher. c’est à dire que à l’exécution j’ai ce message d’erreur « Module spécifié introuvable ». Si tu as une idée je te serais reconnaissant de m’aider

    1. Salut! Je vais jeter un œil dès que j’ai le temps. Mais en « googlant » rapidement le code d’erreur (HResult) je « devine » que ton programme ne trouve pas la DLL de Java (jvm.dll). En C++ sous Windows, pour qu’une DLL soit visible, il faut qu’elle se trouve dans le PATH. Donc fait bien attention que le PATH pointe aussi sur JAVA_HOME/bin.
      Si ça corrige ton problème, je vais alors corriger mon post 🙂
      Sinon, ma solution est « tricky » et très bas niveau. Tu peux aussi utiliser des solutions « pro » comme http://www.jnbridge.com/jnbpro.htm ou http://dev.mainsoft.com/

  2. salut merci pour ta reponse
    J’ai resolu le probleme maintenant c’est au niveau de l’appelle de FindClass() pour récupérer L’ID de la classe qui ne marche pas du coup c’est l’apelle de this->obj = JVMWrapper::env->NewObject(MyJavaWrapper::clazz,initMethodId,Converter.java); plante. Pourtant je passe bien le repertoire projet contenanr mon mi fichier .jar à l’initialisation de la JVM

  3. Voici la class qui marche

    public ref class JVMWrapper abstract
    {
    public:

    static JNIEnv *env;
    static JavaVM *jvm;

    private :

    //[System::Runtime::InteropServices::DllImportAttribute(« jvm.dll »)]
    //static jint JNI_CreateJavaVM(JavaVM **p_vm, void** p_env, void *vm_args);
    typedef jint (JNICALL *CreateJavaVM_t)(JavaVM **, void **, JavaVMInitArgs *);

    static void *JNU_FindCreateJavaVM(char *vmlibpath)
    {
    HINSTANCE hVM = LoadLibrary((LPCWSTR)vmlibpath);
    if (hVM == NULL) {
    return NULL;
    }
    return GetProcAddress(hVM, « JNI_CreateJavaVM »);
    }

    public:
    /* Permet d’initialiser une JVM */
    static void InitJvm(cli::array^ options)
    {
    //CreateJavaVM_t * CreateJavaVM;
    CreateJavaVM_t createJavaVM;

    LPCWSTR str= L »C:\\Program Files\\Java\\jdk1.6.0_12\\jre\\bin\\client\\jvm.dll »;

    HINSTANCE hinst = ::LoadLibrary(str);

    // CreateJavaVM_t * pfnCreateJavaVM;
    //createJavaVM = (CreateJavaVM_t) GetProcAddress(hinst, « JNI_CreateJavaVM »);

    JavaVMInitArgs * vm_args;
    vm_args = new JavaVMInitArgs();
    JavaVMOption* jvmOptions = new JavaVMOption[options->Length];

    for(int i = 0; i Length; i++)
    {
    jvmOptions[i] = JavaVMOption();

    pin_ptr wch = PtrToStringChars(options[i]);

    size_t convertedChars = 0;
    size_t sizeInBytes = ((options[i]->Length + 1) * 2);
    errno_t err = 0;
    jvmOptions[i].optionString = (char *)malloc(sizeInBytes);

    wcstombs_s(&convertedChars, jvmOptions[i].optionString, sizeInBytes, wch, sizeInBytes);
    }

    vm_args->version = JNI_VERSION_1_4;
    vm_args->options = jvmOptions;
    vm_args->nOptions = options->Length;
    vm_args->ignoreUnrecognized = JNI_TRUE;

    JavaVM * nativeJvm;
    JNIEnv * nativeEnv;
    //jint vmError = JNI_CreateJavaVM(&nativeJvm,(void **)&nativeEnv, &vm_args);
    //jint vmError=pfnCreateJavaVM(&nativeJvm,(void **)&nativeEnv, &vm_args);
    createJavaVM = (CreateJavaVM_t) GetProcAddress(hinst, « JNI_CreateJavaVM »);

    jint vmError = (*createJavaVM)(&nativeJvm, (void **)&nativeEnv, vm_args);

    delete [] jvmOptions;
    jvmOptions = NULL;

    JVMWrapper::jvm = nativeJvm;
    JVMWrapper::env = nativeEnv;

    if(vmError) DestroyJavaVM();
    if(vmError < 0) throw gcnew Exception("Failed to destroy JVM, error number: " + vmError); } } [/code]

  4. Bonjour, j’ai tester votre exemple d’utilisation de JNI, mais je rencontre aussi un problem au niveau du FindClass, il ne récupère pas ma classe. Pouvez vous m’aider s’il vous plait?

  5. ca pète dans MyJavaWrapper
    La ligne exacte c’est:
    this->obj = JVMWrapper::env->NewObject(MyJavaWrapper::clazz, MyJavaWrapper::initMethodId);

    1. Salut, Je vais voir ça…
      ça fait longtemps que je n’ai pas touché à du C++/CLI, et je n’arrive même plus à le compiler depuis que je suis sous VS2008 + Windows7 x64 -_- » Je te tiens au courant

    2. Bon, je viens de re-tester et ça marche -_-« .
      Pour la petite histoire, je suis sous Win7x64/VS2008, je force me met en configuration « Win32 », je force le projet C# à du « x86 », et ma variables d’environnement « JAVA_HOME » pointe sur la version « x86 ».
      De plus, j’ai ajouté dans la PATH le chemin vers « jvm.dll » (ATTENTION, pas la version « server », mais celle du dossier « client »).
      Et voila, ça marche…
      Mais j’ai un problème avec les conversions de chaines (AccessViolationException)…

      Concernant ton problème de résolution de classe, est-ce que tu fais bien un « InitJvm » avec l’option « -Djava.class.path=Mathias.Jni.Java.jar » (qui signifie: charge ce .jar pour y trouver des classes) ? ça a l’air d’être ça… je rappelle que tu peux en mettre plusieurs séparés par des des « ; »

  6. Ca marche! J’ai modifier la variable d’environnement ainsi que les chemin d’acces aux fichiers jni.h et jni_md.h et jvm.lib.

    Merci!

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s