Java Tipp 17: Java in C ++ integrieren

In diesem Artikel werde ich einige Probleme bei der Integration von C ++ - Code in eine Java-Anwendung erläutern. Nach einem Wort darüber, warum man dies tun möchte und welche Hürden es gibt, werde ich ein funktionierendes Java-Programm aufbauen, das in C ++ geschriebene Objekte verwendet. Auf dem Weg werde ich einige der Auswirkungen diskutieren (z. B. die Interaktion mit der Speicherbereinigung) und einen Einblick geben, was wir in diesem Bereich in Zukunft erwarten können.

Warum C ++ und Java integrieren?

Warum sollten Sie C ++ - Code überhaupt in ein Java-Programm integrieren wollen? Schließlich wurde die Java-Sprache teilweise erstellt, um einige der Mängel von C ++ zu beheben. Tatsächlich gibt es mehrere Gründe, warum Sie C ++ in Java integrieren möchten:

  • Performance. Selbst wenn Sie für eine Plattform mit einem Just-in-Time-Compiler (JIT) entwickeln, besteht die Wahrscheinlichkeit, dass der von der JIT-Laufzeit generierte Code erheblich langsamer ist als der entsprechende C ++ - Code. Wenn sich die JIT-Technologie verbessert, sollte dies weniger ein Faktor sein. (In naher Zukunft kann eine gute JIT-Technologie durchaus bedeuten, dass Java schneller ausgeführt wird als der entsprechende C ++ - Code.)
  • Zur Wiederverwendung von Legacy-Code und zur Integration in Legacy-Systeme.
  • Direkter Zugriff auf Hardware oder andere Aktivitäten auf niedriger Ebene.
  • Nutzung von Tools, die für Java noch nicht verfügbar sind (ausgereifte OODBMS, ANTLR usw.).

Wenn Sie den Sprung wagen und sich für die Integration von Java und C ++ entscheiden, geben Sie einige der wichtigen Vorteile einer Nur-Java-Anwendung auf. Hier sind die Nachteile:

  • Eine gemischte C ++ / Java-Anwendung kann nicht als Applet ausgeführt werden.
  • Sie geben die Zeigersicherheit auf. Ihr C ++ - Code kann Objekte falsch fehlschlagen, auf ein gelöschtes Objekt zugreifen oder den Speicher auf eine andere Weise beschädigen, die in C ++ so einfach ist.
  • Ihr Code ist möglicherweise nicht portierbar.
  • Ihre erstellte Umgebung ist definitiv nicht portierbar - Sie müssen herausfinden, wie Sie C ++ - Code auf allen Plattformen von Interesse in eine gemeinsam genutzte Bibliothek einfügen.
  • Die APIs für die Integration von C und Java sind in Arbeit und werden sich sehr wahrscheinlich mit dem Wechsel von JDK 1.0.2 zu JDK 1.1 ändern.

Wie Sie sehen können, ist die Integration von Java und C ++ nichts für schwache Nerven! Wenn Sie jedoch fortfahren möchten, lesen Sie weiter.

Wir beginnen mit einem einfachen Beispiel, das zeigt, wie C ++ - Methoden von Java aus aufgerufen werden. Wir werden dieses Beispiel dann erweitern, um zu zeigen, wie das Beobachtermuster unterstützt werden kann. Das Beobachtermuster ist nicht nur einer der Eckpfeiler der objektorientierten Programmierung, sondern auch ein gutes Beispiel für die komplexeren Aspekte der Integration von C ++ - und Java-Code. Wir werden dann ein kleines Programm erstellen, um unser in Java eingeschlossenes C ++ - Objekt zu testen, und wir werden mit einer Diskussion zukünftiger Richtungen für Java enden.

C ++ von Java aus aufrufen

Was ist so schwer an der Integration von Java und C ++? Schließlich enthält das Java-Lernprogramm von SunSoft einen Abschnitt zum Thema "Integrieren nativer Methoden in Java-Programme" (siehe Ressourcen). Wie wir sehen werden, ist dies ausreichend, um C ++ - Methoden von Java aus aufzurufen, aber es gibt uns nicht genug, um Java-Methoden von C ++ aus aufzurufen. Dazu müssen wir etwas mehr arbeiten.

Als Beispiel nehmen wir eine einfache C ++ - Klasse, die wir in Java verwenden möchten. Wir gehen davon aus, dass diese Klasse bereits existiert und wir sie nicht ändern dürfen. Diese Klasse heißt "C ++ :: NumberList" (der Klarheit halber werde ich allen C ++ - Klassennamen "C ++ ::" voranstellen). Diese Klasse implementiert eine einfache Liste von Zahlen mit Methoden, um der Liste eine Zahl hinzuzufügen, die Größe der Liste abzufragen und ein Element aus der Liste abzurufen. Wir werden eine Java-Klasse erstellen, deren Aufgabe es ist, die C ++ - Klasse darzustellen. Diese Java-Klasse, die wir NumberListProxy nennen, hat dieselben drei Methoden, aber die Implementierung dieser Methoden besteht darin, die C ++ - Äquivalente aufzurufen. Dies ist im folgenden Diagramm der Objektmodellierungstechnik (OMT) dargestellt:

Eine Java-Instanz von NumberListProxy muss einen Verweis auf die entsprechende C ++ - Instanz von NumberList beibehalten. Dies ist einfach genug, wenn auch nicht portierbar: Wenn wir uns auf einer Plattform mit 32-Bit-Zeigern befinden, können wir diesen Zeiger einfach in einem int speichern. Wenn wir uns auf einer Plattform befinden, die 64-Bit-Zeiger verwendet (oder wir glauben, dass wir in naher Zukunft sind), können wir sie in einer langen Zeit speichern. Der eigentliche Code für NumberListProxy ist unkompliziert, wenn auch etwas chaotisch. Es verwendet die Mechanismen aus dem Abschnitt "Integrieren nativer Methoden in Java-Programme" des Java-Tutorials von SunSoft.

Ein erster Schnitt in der Java-Klasse sieht folgendermaßen aus:

öffentliche Klasse NumberListProxy {static {System.loadLibrary ("NumberList"); } NumberListProxy () {initCppSide (); } public native void addNumber (int n); public native int size (); public native int getNumber (int i); private native void initCppSide (); private int numberListPtr_; // NumberList *}

Der statische Abschnitt wird ausgeführt, wenn die Klasse geladen wird. System.loadLibrary () lädt die benannte gemeinsam genutzte Bibliothek, die in unserem Fall die kompilierte Version von C ++ :: NumberList enthält. Unter Solaris wird erwartet, dass die gemeinsam genutzte Bibliothek "libNumberList.so" irgendwo im $ LD_LIBRARY_PATH gefunden wird. Namenskonventionen für gemeinsam genutzte Bibliotheken können in anderen Betriebssystemen abweichen.

Die meisten Methoden in dieser Klasse werden als "native" deklariert. Dies bedeutet, dass wir eine C-Funktion bereitstellen, um sie zu implementieren. Um die C-Funktionen zu schreiben, führen wir javah zweimal aus, zuerst als "javah NumberListProxy", dann als "javah -stubs NumberListProxy". Dies generiert automatisch etwas "Kleber" -Code, der für die Java-Laufzeit benötigt wird (die in NumberListProxy.c eingefügt wird) und generiert Deklarationen für die C-Funktionen, die wir implementieren sollen (in NumberListProxy.h).

Ich habe mich entschieden, diese Funktionen in einer Datei namens NumberListProxyImpl.cc zu implementieren. Es beginnt mit einigen typischen # include-Anweisungen:

 // // NumberListProxyImpl.cc // // // This file contains the C++ code that implements the stubs generated // by "javah -stubs NumberListProxy". cf. NumberListProxy.c. #include  #include "NumberListProxy.h" #include "NumberList.h" 

is part of the JDK, and includes a number of important system declarations. NumberListProxy.h was generated for us by javah, and includes declarations of the C functions we're about to write. NumberList.h contains the declaration of the C++ class NumberList.

In the NumberListProxy constructor, we call the native method initCppSide(). This method must find or create the C++ object we want to represent. For the purposes of this article, I'll just heap-allocate a new C++ object, although in general we might instead want to link our proxy to a C++ object that was created elsewhere. The implementation of our native method looks like this:

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

As described in the Java Tutorial, we're passed a "handle" to the Java NumberListProxy object. Our method creates a new C++ object, then attaches it to the numberListPtr_ data member of the Java object.

Now on to the interesting methods. These methods recover a pointer to the C++ object (from the numberListPtr_ data member), then invoke the desired C++ function:

 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_; return list->size(); } long NumberListProxy_getNumber(struct HNumberListProxy* javaObj, long i) { NumberList* list = (NumberList*) unhand(javaObj)->numberListPtr_; return list->getNumber(i); } 

The function names (NumberListProxy_addNumber, and the rest) are determined for us by javah. For more information on this, the types of arguments sent to the function, the unhand() macro, and other details of Java's support for native C functions, please refer to the Java Tutorial.

While this "glue" is somewhat tedious to write, it's fairly straightforward and works well. But what happens when we want to call Java from C++?

Calling Java from C++

Before delving into how to call Java methods from C++, let me explain why this can be necessary. In the diagram I showed earlier, I didn't present the whole story of the C++ class. A more complete picture of the C++ class is shown below:

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.

Was wir wirklich brauchen, ist eine Möglichkeit, den Garbage Collector darüber zu informieren, dass wir einen Verweis auf ein Java-Objekt beibehalten möchten, und eine Art "globalen Verweis" auf das Java-Objekt anzufordern, der garantiert gültig bleibt. Leider hat JDK 1.0.2 keinen solchen Mechanismus. (Eine wird wahrscheinlich in JDK 1.1 verfügbar sein. Weitere Informationen zu zukünftigen Anweisungen finden Sie am Ende dieses Artikels.) Während wir warten, können wir uns um dieses Problem kümmern.