Javaのヒント17:JavaとC ++の統合

この記事では、C ++コードとJavaアプリケーションの統合に関連するいくつかの問題について説明します。なぜこれをやりたいのか、そしていくつかのハードルは何かについて一言述べた後、C ++で書かれたオブジェクトを使用する動作するJavaプログラムを構築します。その過程で、これを行うことの影響(ガベージコレクションとの相互作用など)のいくつかについて説明し、この分野で将来期待できることを垣間見ることができます。

なぜC ++とJavaを統合するのですか?

そもそもなぜC ++コードをJavaプログラムに統合したいのですか?結局のところ、Java言語は、C ++のいくつかの欠点に対処するために部分的に作成されました。実際、C ++をJavaと統合する理由はいくつかあります。

  • パフォーマンス。ジャストインタイム(JIT)コンパイラーを備えたプラットフォーム用に開発している場合でも、JITランタイムによって生成されるコードが同等のC ++コードよりも大幅に遅い可能性があります。JITテクノロジーが向上するにつれて、これはそれほど重要ではなくなるはずです。(実際、近い将来、優れたJITテクノロジーは、Javaが同等のC ++コードより高速に実行されることを意味する可能性があります。)
  • レガシーコードの再利用とレガシーシステムへの統合。
  • ハードウェアに直接アクセスする、またはその他の低レベルのアクティビティを実行するため。
  • Javaでまだ利用できないツール(成熟したOODBMS、ANTLRなど)を活用するため。

思い切ってJavaとC ++を統合することにした場合、Javaのみのアプリケーションの重要な利点のいくつかを放棄することになります。欠点は次のとおりです。

  • C ++ / Javaが混在するアプリケーションは、アプレットとして実行できません。
  • あなたはポインタの安全性をあきらめます。C ++コードは、C ++で非常に簡単な他の方法で、オブジェクトのミスキャスト、削除されたオブジェクトへのアクセス、またはメモリの破損を自由に行うことができます。
  • コードは移植できない可能性があります。
  • 構築された環境は間違いなく移植性がありません。関心のあるすべてのプラットフォームの共有ライブラリにC ++コードを配置する方法を理解する必要があります。
  • CとJavaを統合するためのAPIは進行中であり、JDK1.0.2からJDK1.1への移行に伴って変更される可能性が非常に高くなります。

ご覧のとおり、JavaとC ++の統合は、気の弱い人向けではありません。ただし、続行する場合は、読み進めてください。

まず、JavaからC ++メソッドを呼び出す方法を示す簡単な例から始めます。次に、この例を拡張して、オブザーバーパターンをサポートする方法を示します。オブザーバーパターンは、オブジェクト指向プログラミングの基礎の1つであることに加えて、C ++とJavaコードの統合のより複雑な側面の良い例として機能します。次に、JavaでラップされたC ++オブジェクトをテストするための小さなプログラムを作成し、Javaの今後の方向性について説明します。

JavaからC ++を呼び出す

JavaとC ++を統合することの何がそんなに難しいのでしょうか?結局のところ、SunSoftのJavaチュートリアルには、「ネイティブメソッドのJavaプログラムへの統合」に関するセクションがあります(「参考文献」を参照)。後で説明するように、これはJavaからC ++メソッドを呼び出すには十分ですが、C ++からJavaメソッドを呼び出すには十分ではありません。そのためには、もう少し作業を行う必要があります。

例として、Java内から使用したい単純なC ++クラスを取り上げます。このクラスはすでに存在し、変更することは許可されていないと想定します。このクラスは「C ++ :: NumberList」と呼ばれます(わかりやすくするために、すべてのC ++クラス名の前に「C ++ ::」を付けます)。このクラスは、リストに数値を追加し、リストのサイズを照会し、リストから要素を取得するメソッドを使用して、数値の単純なリストを実装します。 C ++クラスを表すことを仕事とするJavaクラスを作成します。 NumberListProxyと呼ぶこのJavaクラスには、同じ3つのメソッドがありますが、これらのメソッドの実装は、同等のC ++を呼び出すことです。これは、次のオブジェクトモデリング技法(OMT)図に示されています。

NumberListProxyのJavaインスタンスは、NumberListの対応するC ++インスタンスへの参照を保持する必要があります。これは、少し移植性がない場合でも、十分に簡単です。32ビットポインタを備えたプラットフォームを使用している場合は、このポインタをintに格納するだけです。64ビットポインタを使用するプラットフォームを使用している場合(または近い将来になる可能性があると思われる場合)、それを長期間保存できます。NumberListProxyの実際のコードは、多少面倒ですが、簡単です。これは、SunSoftのJavaチュートリアルの「ネイティブメソッドのJavaプログラムへの統合」セクションのメカニズムを使用します。

Javaクラスでの最初のカットは次のようになります。

パブリッククラスNumberListProxy {static {System.loadLibrary( "NumberList"); } NumberListProxy(){initCppSide(); } public native void addNumber(int n); public native int size(); public native int getNumber(int i); プライベートネイティブvoidinitCppSide(); private int numberListPtr_; // NumberList *}

静的セクションは、クラスがロードされるときに実行されます。System.loadLibrary()は、名前付き共有ライブラリをロードします。この場合、C ++ :: NumberListのコンパイル済みバージョンが含まれています。Solarisでは、共有ライブラリ「libNumberList.so」が$ LD_LIBRARY_PATHのどこかにあると想定されます。共有ライブラリの命名規則は、他のオペレーティングシステムでは異なる場合があります。

このクラスのほとんどのメソッドは「ネイティブ」として宣言されています。これは、それらを実装するためのC関数を提供することを意味します。C関数を作成するには、javahを2回実行します。最初は「javahNumberListProxy」として、次に「javah-stubsNumberListProxy」として実行します。これにより、Javaランタイムに必要な「グルー」コード(NumberListProxy.cに配置)が自動的に生成され、実装するC関数(NumberListProxy.h)の宣言が生成されます。

これらの関数をNumberListProxyImpl.ccというファイルに実装することにしました。それはいくつかの典型的な#includeディレクティブで始まります:

// // NumberListProxyImpl.cc // // //このファイルには、「javah-stubsNumberListProxy」によって//生成されたスタブを実装するC ++コードが含まれています。cf. NumberListProxy.c。#include #include "NumberListProxy.h" #include "NumberList.h"

はJDKの一部であり、いくつかの重要なシステム宣言が含まれています。NumberListProxy.hはjavahによって生成され、これから作成するC関数の宣言が含まれています。NumberList.hには、C ++クラス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.

本当に必要なのは、Javaオブジェクトへの参照を維持する予定であることをガベージコレクターに通知し、有効であることが保証されているJavaオブジェクトへのある種の「グローバル参照」を要求する方法です。残念ながら、JDK1.0.2にはそのようなメカニズムはありません。(1つはおそらくJDK 1.1で利用可能になります。今後の方向性の詳細については、この記事の最後を参照してください。)待っている間、この問題を回避することができます。