目次 | 前へ | 次へ |
この章は JNI の主な設計の問題に焦点をあてています。このセクションの設計の問題のほとんどはネイティブメソッドと関連があります。呼び出し API の設計については、「第 5 章」に掲載されています。
ネイティブコードは、JNI 関数を呼び出して Java VM 機能にアクセスします。JNI 関数はインタフェースポインタを介して使用できます。インタフェースポインタは、ポインタを指すポインタです。このポインタはポインタの配列を指し、このそれぞれのポインタがインタフェース関数を指します。どのインタフェース関数も配列内の事前に定義されたオフセットにあります。図 2-1 は、インタフェースポインタの構成を図示したものです。
JNI インタフェースは、C++ 仮想関数テーブルまたは COM インタフェースのように構成されています。固定された組み込み関数エントリでなくインタフェーステーブルを使用する利点は、JNI 名前空間がネイティブコードと分離できるようになることです。VM は複数バージョンの JNI 関数テーブルを容易に提供できます。たとえば、VM は次のように 2 つの JNI 関数テーブルをサポートすることもできます。
JNI インタフェースポインタは現在のスレッドの中だけで有効です。したがって、ネイティブメソッドがスレッド間でインタフェースポインタを渡さないようにしてください。JNI を実装している VM は、JNI インタフェースポインタによって指示された領域にスレッドのローカルデータを割り当てて格納することもできます。
ネイティブメソッドは JNI インタフェースポインタを引数として受け取ります。したがって、VM が同じ Java スレッドからネイティブメソッドに複数の呼び出しを行う場合は、ネイティブメソッドに同じインタフェースポインタを渡すことが保証されています。しかし、ネイティブメソッドは、異なる Java スレッドからでも呼び出すことができるので、異なる JNI インタフェースポインタを受け取ることもあります。
Java VM はマルチスレッド化されているため、ネイティブライブラリも、マルチスレッドに対応したネイティブコンパイラでコンパイルおよびリンクするべきです。たとえば、Sun Studio コンパイラでコンパイルされる C++ コードには -mt
フラグを使用するべきです。GNU gcc コンパイラでコンパイルされるコードには、フラグ -D_REENTRANT
または -D_POSIX_C_SOURCE
を使用するべきです。詳細は、ネイティブコンパイラのドキュメントを参照してください。
ネイティブメソッドは、System.loadLibrary
メソッドを使用してロードされます。次の例では、クラス初期化メソッドが、ネイティブメソッド f
が定義されているプラットフォーム固有のネイティブライブラリをロードしています。
package pkg; class Cls { native double f(int i, String s); static { System.loadLibrary(“pkg_Cls”); } }
System.loadLibrary
の引数は、プログラマによって任意に選択されたライブラリ名です。このシステムは、標準であってもプラットフォーム固有の方式に従ってライブラリ名をネイティブライブラリ名に変換します。たとえば、Solaris システムは pkg_Cls
という名前を libpkg_Cls.so
に変換するのに対して、Win32 システムは同じ pkg_Cls
という名前を pkg_Cls.dll
に変換します。
プログラマは、同じローダーでクラスがロードされるかぎり、必要とするクラスがいくらあっても、その必要なすべてのネイティブメソッドを、単一ライブラリを使用して格納できます。VM はクラスローダーごとのロードされたネイティブライブラリのリストを内部的に維持します。ベンダーは、名前ができるだけ競合しないネイティブライブラリ名を選択する必要があります。
基盤のオペレーティングシステムが動的リンクをサポートしない場合は、すべてのネイティブメソッドが VM と事前にリンク済みでなければなりません。この場合、VM は実際にはこのライブラリをロードすることなく System.loadLibrary
の呼び出しを完了します。
プログラマは JNI 関数 RegisterNatives()
を呼び出して、クラスと関連付けられたネイティブメソッドを登録することもできます。RegisterNatives()
関数は、静的にリンクされた関数を使用する場合に特に有用です。
動的リンカはネイティブメソッドの名前に基づいてエントリを解決します。ネイティブメソッド名は、次のコンポーネントを連結して作られます。
Java_
VM は、ネイティブライブラリに常駐するメソッドについてメソッド名の一致を調べます。VM は、最初にショート名 (引数のシグニチャーのない名前) を探します。次にロング名 (引数のシグニチャーが付いた名前) を探します。プログラマがロング名を使用する必要があるのは、ネイティブメソッドが別のネイティブメソッドによりオーバーロードされたときだけです。しかし、ネイティブメソッドが非ネイティブメソッドと同じ名前を持っている場合、これは問題ではありません。非ネイティブメソッド (Java メソッド) は、ネイティブライブラリに常駐していません。
次の例では、ネイティブメソッド g
はロング名を使ってリンクする必要はありません。もう一方のメソッド g
がネイティブメソッドでないため、ネイティブライブラリにないからです。
単純な名前分解スキームですが、すべての Unicode 文字が有効な C 関数名に確実に変換されるようになっています。完全修飾クラス名の中で斜線 (「/」) の代わりに下線 (「_」) 文字を使用します。名前または型記述子が数字で始まることはないので、表 2-1 の例のように、_0
、...、_9
をエスケープシーケンスに使用できます。
エスケープシーケンス
|
表示
|
---|---|
_0XXXX |
Unicode 文字
XXXX 。小文字は ASCII Unicode 文字 以外の文字を表す場合に使用 されます。 たとえば _0abcd は_0ABCD と区別されます。 |
_1 |
文字「_」
|
_2 |
シグニチャーの中の文字「;」
|
_3 |
シグニチャーの中の文字「[」
|
ネイティブメソッドとインタフェース API の両方とも、所定のプラットフォーム上での標準ライブラリ呼び出し規則に従っています。たとえば、UNIX システムは C 呼び出し規則を使用するのに対して、Win32 システムは __stdcall を使用します。
JNI インタフェースポインタは、ネイティブメソッドの最初の引数です。JNI インタフェースポインタは JNIEnv 型です。2 番目の引数は、ネイティブメソッドが static であるか static でないかによって異なります。static でないネイティブメソッドの 2 番目の引数は、オブジェクトの参照です。static なネイティブメソッドの 2 番目の引数は、Java クラスの参照です。
残りの引数は、通常の Java メソッド引数に対応しています。ネイティブメソッド呼び出しは、呼び出し側ルーチンに結果を値で渡して戻します。第 3 章で、Java 型と C 型とのマッピングについて説明しています。
コード例 2-1 に、C 関数を使用してネイティブメソッド f
を実装する例を示します。ネイティブメソッド f
は、次のように宣言されます。
長い分解名 Java_pkg_Cls_f_ILjava_lang_String_2
を持つ C 関数は、ネイティブメソッド f
を実装します。
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 ( JNIEnv *env, /* interface pointer */ jobject obj, /* "this" pointer */ jint i, /* argument #1 */ jstring s) /* argument #2 */ { /* Obtain a C-copy of the Java string */ const char *str = (*env)->GetStringUTFChars(env, s, 0); /* process the string */ ... /* Now we are done with str */ (*env)->ReleaseStringUTFChars(env, s, str); return ... }
Java オブジェクトは、常にインタフェースポインタ env を使用して操作します。C++ を使用すると、コード例 2-2 に示すように、多少すっきりしたコードを書くことができます。
extern "C" /* specify the C calling convention */ jdouble Java_pkg_Cls_f__ILjava_lang_String_2 ( JNIEnv *env, /* interface pointer */ jobject obj, /* "this" pointer */ jint i, /* argument #1 */ jstring s) /* argument #2 */ { const char *str = env->GetStringUTFChars(s, 0); ... env->ReleaseStringUTFChars(s, str); return ... }
C++ では、余分な間接参照およびインタフェースポインタ引数がソースコードから消えています。しかし、基盤となるメカニズムは C による場合とまったく同じです。C++ では、JNI 関数が、インラインメンバー関数として定義されますが、これらは展開されて、C の対応部分になります。
整数、文字などのプリミティブ型は、Java とネイティブメソッド間でコピーされます。他方、任意の Java オブジェクトは参照渡しです。VM はネイティブコードに渡されたすべてのオブジェクトがガベージコレクタによって解放されないよう、これらのオブジェクトを追跡しなければなりません。ネイティブコードは、逆に、オブジェクトがもう必要ないことを VM に通知する手段を持たなければなりません。さらに、ガベージコレクタは、ネイティブコードによって参照されるオブジェクトを移動することもできなければなりません。
JNI は、ネイティブコードによって使用されるオブジェクト参照をローカル参照とグローバル参照の 2 つのカテゴリに分けます。ローカル参照は、ネイティブメソッド呼び出しの間だけ有効で、ネイティブメソッドが復帰すると自動的に解放されます。グローバル参照は、明示的に解放されるまで有効になっています。
オブジェクトは、ローカル参照としてネイティブメソッドに渡されます。JNI 関数によって返される Java オブジェクトはすべてローカル参照です。JNI では、プログラマがローカル参照からグローバル参照を作成できます。Java オブジェクトを扱う JNI 関数は、グローバルとローカルの両方の参照を受け入れます。ネイティブメソッドは、その結果、グローバルまたはローカルのどちらかの参照を VM に返すことになります。
ほとんどの場合、プログラマは、ネイティブメソッドが戻ったあと、VM に基づいてすべてのローカル参照を解放すべきです。しかし、プログラマが明示的にローカル参照を解放する必要がある場合もあります。たとえば、次のような状況があります。
JNI では、プログラマがネイティブメソッド内の任意の点でローカル参照を手動で削除できます。プログラマが手動でローカル参照を解放できることを保証するため、JNI 関数では、これら関数が結果として返す参照を除いて、余分なローカル参照を作成できないようになっています。
ローカル参照は、これらが作成されたスレッドの中だけで有効です。ネイティブコードは、スレッド間でローカル参照を受渡ししてはいけません。
ローカル参照子を実装するため、Java VM は Java からネイティブメソッドに制御が移行するたびにレジストリを作成します。レジストリは、移動できないローカル参照を Java オブジェクトにマッピングし、オブジェクトがガベージコレクトされないよう守ります。ネイティブメソッドに渡されるすべての Java オブジェクト (JNI 関数呼び出しの結果として返されるものも含む) は、自動的にレジストリに追加されます。このレジストリは、ネイティブメソッドが返ったあとに削除され、そのすべての項目をガベージコレクトできるようにします。
レジストリを実装するには、テーブル、連結リスト、またはハッシュテーブルを使用するなど、さまざまな方法があります。レジストリの中の項目の重複を避けるため参照のカウントが使用されることがありますが、JNI の実装では重複項目を検出し重複をなくす必要はありません。
ローカル参照は、厳密にネイティブスタックをスキャンしても、忠実に実装することはできません。ネイティブコードは、ローカル参照をグローバルまたはヒープデータ構造に格納することもあります。
JNI は、グローバル参照およびローカル参照への豊富なアクセス機能のセットを提供します。これは、VM が内部的にどのように Java オブジェクトを表現していても、同じネイティブメソッド実装が作動することを意味します。これが決定的な理由となって、JNI は多様な VM 実装でサポートされています。
不透明な参照を介してアクセス用関数を使用するオーバーヘッドは、C データ構造体へ直接アクセスする場合より高くなります。ほとんどの場合に Java プログラマはネイティブメソッドを使用して、このインタフェースのオーバーヘッドが目立たなくなるような重要な (自明的でない) タスクを実行していると考えられます。
このオーバーヘッドは、整数列や文字列のような多くのプリミティブデータ型を含んでいる大きな Java オブジェクトでは受け入れられません。ベクトルおよび行列の計算を実行するために使用されるネイティブメソッドを考えてください。Java 配列を反復演算し、各要素をすべて関数呼び出しによって取り出すことは、非効率です。
ネイティブメソッドが VM に配列の内容の確認を要求できるように、「ピニング」の概念を導入する解決策もあります。そのあとネイティブメソッドは、その要素を指すダイレクトポインタを受け取ります。しかし、このアプローチは次の 2 つのことを意味します。
上記の両方の問題を克服する折衷案を採用しています。
第一に、Java 配列のセグメントとネイティブメモリーバッファーの間でプリミティブ配列要素をコピーするための関数のセットを提供します。ネイティブメソッドが大きな配列の中の少数要素だけにアクセスする必要しかない場合は、これらの関数を使用してください。
第二に、プログラマは別の関数のセットを使用して、ピニングされたバージョンの配列要素を検索できます。これらの関数がストレージの割り当ておよびコピーを実行するには Java VM が必要なことを覚えておいてください。これらの関数が実際に配列をコピーできるかどうかは、次のように VM の実装によって決まります。
このインタフェースは、ネイティブコードが配列要素にアクセスする必要がなくなったことを VM に通知するための関数を備えています。これらの関数を呼び出すと、システムは配列のピンを外すか、または元の配列を移動できないコピーと適合させ、そのコピーを解放します。
これによって、柔軟性が高くなります。ガベージコレクタアルゴリズムにより、指定配列ごとのコピーまたはピニングについて個別に判断できます。たとえば、ガベージコレクタが小さなオブジェクトをコピーし、大きなオブジェクトをピニングすることもできます。
JNI の実装では、複数のスレッドで実行されているネイティブメソッドが、同時に同じ配列に確実にアクセスできるようにしなければなりません。たとえば、JNI はピニングされた配列ごとに内部カウンタを備えて、あるスレッドが、別のスレッドもピニングしている配列のピンを外すことがないようにしています。JNI はネイティブメソッドによる排他アクセスのためにプリミティブ配列をロックする必要はありません。異なるスレッドから同時に Java 配列を更新すると、不測の結果を招きます。
JNI では、ネイティブコードでフィールドにアクセスし、Java オブジェクトのメソッドを呼び出すことができます。JNI は、シンボリック名および型のシグニチャーによってメソッドおよびフィールドを識別します。2 段階のプロセスにより、名前およびシグニチャーからフィールドまたはメソッドを探し出す手間を分けています。たとえば、cls クラスでメソッド f
を呼び出す場合、ネイティブコードはまず次のようにメソッド ID を取得します。
続いてネイティブコードは、次のようにメソッド探索の手間をかけずにメソッド ID を繰り返し使用できます。
フィールドまたはメソッド ID では、VM がその ID が導き出されたクラスをアンロードしないように防ぐことはできません。クラスがアンロードされると、フィールドまたはメソッド ID は無効になります。そのため、ネイティブコードで次の点を確認する必要があります。
延長された期間中にメソッドまたはフィールド ID を使用するかどうか。
JNI は、フィールドまたはメソッド ID がどのように内部的に実装されているかには何の制約も課しません。
JNI は、null ポインタまたは不正な引数型の受け渡しのようなプログラミングエラーについてチェックを行いません。不正な引数型には、Java クラスオブジェクトの代わりに通常の Java オブジェクトを使用するようなことが含まれます。JNI は、次のような理由からこれらのプログラミングエラーについてのチェックを行いません。
ほとんどの C ライブラリ関数は、プログラムエラーに対して保護されていません。たとえば、printf()
関数は、無効アドレスを受け取ると、通常は実行時エラーを起し、エラーコードを返しません。すべての起こり得るエラー条件についてチェックするように C ライブラリ関数に強制すると、ユーザーコードで 1 回チェックしてまたライブラリでも行うというように、チェックが重複する可能性があります。
プログラマは不正なポインタや間違った型の引数を JNI 関数に渡してはいけません。これを行うと、システムの破壊状態または VM のクラッシュを含む、不測の結果に至ることがあります。
JNI では、ネイティブメソッドは任意の Java の例外を発生させることが可能です。ネイティブコードでも、未処理の Java の例外を処理できます。未処理のままになっている Java の例外は VM に送り返されます。
JNI 関数によっては、Java の例外メカニズムを使用してエラー条件を報告するものもあります。ほとんどの場合、JNI 関数は、エラーコードを返し、かつ Java の例外をスローすることによって、エラー状態を報告します。通常、このエラーコードは、通常の戻り値の範囲外にある特殊な戻り値 (NULL など) です。したがって、プログラマは次のことを行うことができます。
ExceptionOccurred()
を呼び出して、エラー状態のさらに詳細な記述が含まれている例外オブジェクトを取得する。プログラマが最初にエラーコードをチェックできない状態で、例外をチェックすることが必要になる場合として、次の 2 つのケースがあります。
ExceptionOccurred()
を呼び出して、Java メソッドの実行中に起こり得る例外が起きていないかチェックする必要があります。ArrayIndexOutOfBoundsException
または ArrayStoreException
をスローするものがあります。その他すべての場合は、非エラーの戻り値で、例外がスローされていないことを保証しています。
マルチスレッドの場合、現在のスレッドではないほかのスレッドが非同期な例外を送信することがあります。非同期な例外が、現スレッドのネイティブコードの例外にすぐに影響することはありませんが、次の時点で影響します。
ExceptionOccurred()
を使用して、同期および非同期の例外があるかを明示的にチェックする。同期した例外を発生させる可能性のある JNI 関数だけが非同期な例外をチェックします。
ネイティブメソッドでは、必要な場所 (ほかの例外のチェックがない密なループの中など) に ExceptionOccurred()
のチェックを挿入して、現在のスレッドが非同期の例外に適当な時間の範囲内で応答することを保証する必要があります。
ネイティブコードで例外を処理するには、次の 2 とおりの方法があります。
ExceptionClear()
を呼び出して例外をクリアしてから、自身の例外処理コードを実行できる。例外の発生後、ネイティブコードは、ほかの JNI 呼び出しを行う前にまず例外をクリアする必要があります。未処理の例外があるとき、安全に呼び出せる JNI 関数は次のとおりです。
ExceptionOccurred()
ExceptionDescribe()
ExceptionClear()
ExceptionCheck()
ReleaseStringChars()
ReleaseStringUTFChars()
ReleaseStringCritical()
Release<Type>ArrayElements()
ReleasePrimitiveArrayCritical()
DeleteLocalRef()
DeleteGlobalRef()
DeleteWeakGlobalRef()
MonitorExit()
PushLocalFrame()
PopLocalFrame()
目次 | 前へ | 次へ |