Astuce Java 17: Intégration de Java avec C ++

Dans cet article, je discuterai de certains des problèmes liés à l'intégration de code C ++ avec une application Java. Après un mot sur les raisons pour lesquelles on voudrait faire cela et quels sont certains des obstacles, je vais créer un programme Java fonctionnel qui utilise des objets écrits en C ++. En cours de route, je discuterai de certaines des implications de cette opération (comme l'interaction avec le ramasse-miettes) et je présenterai un aperçu de ce à quoi nous pouvons nous attendre dans ce domaine à l'avenir.

Pourquoi intégrer C ++ et Java?

Pourquoi voudriez-vous intégrer du code C ++ dans un programme Java en premier lieu? Après tout, le langage Java a été créé, en partie, pour remédier à certaines des lacunes du C ++. En fait, il y a plusieurs raisons pour lesquelles vous pourriez vouloir intégrer C ++ avec Java:

  • Performance. Même si vous développez pour une plate-forme avec un compilateur juste à temps (JIT), il y a de fortes chances que le code généré par le runtime JIT soit considérablement plus lent que le code C ++ équivalent. À mesure que la technologie JIT s'améliore, cela devrait devenir un facteur moins important. (En fait, dans un proche avenir, une bonne technologie JIT pourrait bien signifier que Java s'exécute plus rapidement que le code C ++ équivalent.)
  • Pour la réutilisation du code hérité et l'intégration dans les systèmes hérités.
  • Pour accéder directement au matériel ou effectuer d'autres activités de bas niveau.
  • Pour tirer parti des outils qui ne sont pas encore disponibles pour Java (OODBMS matures, ANTLR, etc.).

Si vous franchissez le pas et décidez d'intégrer Java et C ++, vous renoncez à certains des avantages importants d'une application Java uniquement. Voici les inconvénients:

  • Une application mixte C ++ / Java ne peut pas s'exécuter en tant qu'applet.
  • Vous abandonnez la sécurité du pointeur. Votre code C ++ est libre de mal diffuser des objets, d'accéder à un objet supprimé ou de corrompre la mémoire de l'une des autres manières qui sont si simples en C ++.
  • Votre code n'est peut-être pas portable.
  • Votre environnement construit ne sera certainement pas portable - vous devrez trouver comment mettre du code C ++ dans une bibliothèque partagée sur toutes les plates-formes d'intérêt.
  • Les API pour intégrer C et Java sont en cours de développement et changeront très probablement avec le passage du JDK 1.0.2 au JDK 1.1.

Comme vous pouvez le voir, l'intégration de Java et C ++ n'est pas pour les âmes sensibles! Cependant, si vous souhaitez continuer, lisez la suite.

Nous commencerons par un exemple simple montrant comment appeler des méthodes C ++ à partir de Java. Nous allons ensuite étendre cet exemple pour montrer comment prendre en charge le modèle d'observateur. Le modèle d'observateur, en plus d'être l'une des pierres angulaires de la programmation orientée objet, constitue un bel exemple des aspects les plus complexes de l'intégration du code C ++ et Java. Nous allons ensuite créer un petit programme pour tester notre objet C ++ enveloppé de Java, et nous terminerons par une discussion sur les orientations futures de Java.

Appeler C ++ depuis Java

Qu'y a-t-il de si difficile à intégrer Java et C ++, demandez-vous? Après tout, le didacticiel Java de SunSoft a une section sur «Intégration de méthodes natives dans les programmes Java» (voir Ressources). Comme nous le verrons, cela convient pour appeler des méthodes C ++ à partir de Java, mais cela ne nous donne pas assez pour appeler des méthodes Java à partir de C ++. Pour ce faire, nous devrons faire un peu plus de travail.

À titre d'exemple, nous prendrons une classe C ++ simple que nous aimerions utiliser à partir de Java. Nous supposerons que cette classe existe déjà et que nous ne sommes pas autorisés à la modifier. Cette classe s'appelle "C ++ :: NumberList" (pour plus de clarté, je préfixerai tous les noms de classe C ++ par "C ++ ::"). Cette classe implémente une simple liste de nombres, avec des méthodes pour ajouter un nombre à la liste, interroger la taille de la liste et obtenir un élément de la liste. Nous allons créer une classe Java dont le travail est de représenter la classe C ++. Cette classe Java, que nous appellerons NumberListProxy, aura les trois mêmes méthodes, mais l'implémentation de ces méthodes consistera à appeler les équivalents C ++. Ceci est illustré dans le diagramme de technique de modélisation d'objet (OMT) suivant:

Une instance Java de NumberListProxy doit conserver une référence à l'instance C ++ correspondante de NumberList. C'est assez simple, bien que légèrement non portable: si nous sommes sur une plateforme avec des pointeurs 32 bits, nous pouvons simplement stocker ce pointeur dans un int; si nous sommes sur une plate-forme qui utilise des pointeurs 64 bits (ou que nous pensons que nous pourrions l'être dans un proche avenir), nous pouvons le stocker dans un long. Le code réel de NumberListProxy est simple, bien que quelque peu désordonné. Il utilise les mécanismes de la section «Intégration de méthodes natives dans les programmes Java» du didacticiel Java de SunSoft.

Une première coupe à la classe Java ressemble à ceci:

classe publique NumberListProxy {statique {System.loadLibrary ("NumberList"); } NumberListProxy () {initCppSide (); } public native void addNumber (int n); taille publique native int (); public natif int getNumber (int i); privé natif void initCppSide (); private int numberListPtr_; // NumberList *}

La section statique est exécutée lorsque la classe est chargée. System.loadLibrary () charge la bibliothèque partagée nommée, qui dans notre cas contient la version compilée de C ++ :: NumberList. Sous Solaris, il s'attend à trouver la bibliothèque partagée "libNumberList.so" quelque part dans $ LD_LIBRARY_PATH. Les conventions de dénomination des bibliothèques partagées peuvent différer dans d'autres systèmes d'exploitation.

La plupart des méthodes de cette classe sont déclarées comme «natives». Cela signifie que nous fournirons une fonction C pour les implémenter. Pour écrire les fonctions C, nous exécutons javah deux fois, d'abord en tant que «javah NumberListProxy», puis en tant que «javah -stubs NumberListProxy». Cela génère automatiquement du code "glue" nécessaire à l'exécution Java (qu'il met dans NumberListProxy.c) et génère des déclarations pour les fonctions C que nous devons implémenter (dans NumberListProxy.h).

J'ai choisi d'implémenter ces fonctions dans un fichier appelé NumberListProxyImpl.cc. Cela commence par quelques directives #include typiques:

// // NumberListProxyImpl.cc // // // Ce fichier contient le code C ++ qui implémente les stubs générés // par "javah -stubs NumberListProxy". cf. NumberListProxy.c. #include #include "NumberListProxy.h" #include "NumberList.h"

fait partie du JDK et comprend un certain nombre de déclarations système importantes. NumberListProxy.h a été généré pour nous par javah, et inclut les déclarations des fonctions C que nous sommes sur le point d'écrire. NumberList.h contient la déclaration de la classe C ++ NumberList.

Dans le constructeur NumberListProxy, nous appelons la méthode native initCppSide (). Cette méthode doit trouver ou créer l'objet C ++ que nous voulons représenter. Pour les besoins de cet article, je vais simplement allouer en tas un nouvel objet C ++, bien qu'en général, nous souhaitons plutôt lier notre proxy à un objet C ++ qui a été créé ailleurs. L'implémentation de notre méthode native ressemble à ceci:

void NumberListProxy_initCppSide (struct HNumberListProxy * javaObj) {NumberList * list = new NumberList (); unhand (javaObj) -> numberListPtr_ = (longue) liste; }

Comme décrit dans le didacticiel Java , nous avons passé un "handle" à l'objet Java NumberListProxy. Notre méthode crée un nouvel objet C ++, puis l'attache au membre de données numberListPtr_ de l'objet Java.

Passons maintenant aux méthodes intéressantes. Ces méthodes récupèrent un pointeur vers l'objet C ++ (à partir du membre de données numberListPtr_), puis invoquent la fonction C ++ souhaitée:

void NumberListProxy_addNumber (struct HNumberListProxy * javaObj, long v) {NumberList * list = (NumberList *) unhand (javaObj) -> numberListPtr_; list-> addNumber (v); } long NumberListProxy_size (struct HNumberListProxy * javaObj) {NumberList * list = (NumberList *) unhand (javaObj) -> numberListPtr_; liste de retour-> taille (); } long NumberListProxy_getNumber (struct HNumberListProxy * javaObj, long i) {NumberList * list = (NumberList *) unhand (javaObj) -> numberListPtr_; return list-> getNumber (i); }

Les noms des fonctions (NumberListProxy_addNumber, et le reste) sont déterminés pour nous par javah. Pour plus d'informations à ce sujet, les types d'arguments envoyés à la fonction, la macro unhand () et d'autres détails sur la prise en charge par Java des fonctions C natives, veuillez consulter le didacticiel Java .

Bien que cette «colle» soit quelque peu fastidieuse à écrire, elle est assez simple et fonctionne bien. Mais que se passe-t-il lorsque nous voulons appeler Java à partir de C ++?

Appel de Java à partir de C ++

Avant d' expliquer comment appeler des méthodes Java à partir de C ++, laissez-moi vous expliquer pourquoi cela peut être nécessaire. Dans le diagramme que j'ai montré plus tôt, je n'ai pas présenté toute l'histoire de la classe C ++. Une image plus complète de la classe C ++ est présentée ci-dessous:

As you can see, we're dealing with an observable number list. This number list might be modified from many places (from NumberListProxy, or from any C++ object that has a reference to our C++::NumberList object). NumberListProxy is supposed to faithfully represent all of the behavior of C++::NumberList; this should include notifying Java observers when the number list changes. In other words, NumberListProxy needs to be a subclass of java.util.Observable, as pictured here:

It's easy enough to make NumberListProxy a subclass of java.util.Observable, but how does it get notified? Who will call setChanged() and notifyObservers() when C++::NumberList changes? To do this, we'll need a helper class on the C++ side. Luckily, this one helper class will work with any Java observable. This helper class needs to be a subclass of C++::Observer, so it can register with C++::NumberList. When the number list changes, our helper class' update() method will be called. The implementation of our update() method will be to call setChanged() and notifyObservers() on the Java proxy object. This is pictured in OMT:

Before going into the implementation of C++::JavaObservableProxy, let me mention some of the other changes.

NumberListProxy has a new data member: javaProxyPtr_. This is a pointer to the instance of C++JavaObservableProxy. We'll need this later when we discuss object destruction. The only other change to our existing code is a change to our C function NumberListProxy_initCppSide(). It now looks like this:

 void NumberListProxy_initCppSide(struct HNumberListProxy *javaObj) { NumberList* list = new NumberList(); struct HObservable* observable = (struct HObservable*) javaObj; JavaObservableProxy* proxy = new JavaObservableProxy(observable, list); unhand(javaObj)->numberListPtr_ = (long) list; unhand(javaObj)->javaProxyPtr_ = (long) proxy; } 

Note that we cast javaObj to a pointer to an HObservable. This is OK, because we know that NumberListProxy is a subclass of Observable. The only other change is that we now create a C++::JavaObservableProxy instance and maintain a reference to it. C++::JavaObservableProxy will be written so that it notifies any Java Observable when it detects an update, which is why we needed to cast HNumberListProxy* to HObservable*.

Given the background so far, it may seem that we just need to implement C++::JavaObservableProxy:update() such that it notifies a Java observable. That solution seems conceptually simple, but there is a snag: How do we hold onto a reference to a Java object from within a C++ object?

Maintaining a Java reference in a C++ object

It might seem like we could simply store a handle to a Java object within a C++ object. If this were so, we might code C++::JavaObservableProxy like this:

 class JavaObservableProxy public Observer { public: JavaObservableProxy(struct HObservable* javaObj, Observable* obs) { javaObj_ = javaObj; observedOne_ = obs; observedOne_->addObserver(this); } ~JavaObservableProxy() { observedOne_->deleteObserver(this); } void update() { execute_java_dynamic_method(0, javaObj_, "setChanged", "()V"); } private: struct HObservable* javaObj_; Observable* observedOne_; }; 

Unfortunately, the solution to our dilemma is not so simple. When Java passes you a handle to a Java object, the handle] will remain valid for the duration of the call. It will not necessarily remain valid if you store it on the heap and try to use it later. Why is this so? Because of Java's garbage collection.

First of all, we're trying to maintain a reference to a Java object, but how does the Java runtime know we're maintaining that reference? It doesn't. If no Java object has a reference to the object, the garbage collector might destroy it. In this case, our C++ object would have a dangling reference to an area of memory that used to contain a valid Java object but now might contain something quite different.

Even if we're confident that our Java object won't get garbage collected, we still can't trust a handle to a Java object after a time. The garbage collector might not remove the Java object, but it could very well move it to a different location in memory! The Java spec contains no guarantee against this occurrence. Sun's JDK 1.0.2 (at least under Solaris) won't move Java objects in this way, but there are no guarantees for other runtimes.

Ce dont nous avons vraiment besoin, c'est d'un moyen d'informer le ramasse-miettes que nous prévoyons de maintenir une référence à un objet Java, et de demander une sorte de «référence globale» à l'objet Java dont la validité est garantie. Malheureusement, JDK 1.0.2 n'a pas un tel mécanisme. (L'un sera probablement disponible dans JDK 1.1; voir la fin de cet article pour plus d'informations sur les directions futures.) En attendant, nous pouvons contourner ce problème.