目次|前|次 |
この章はJNIの主な設計の問題に焦点をあてています。このセクションの設計の問題のほとんどはネイティブ・メソッドと関連があります。呼出しAPIの設計については、「第5章: 呼出しAPI」に掲載されています。
この章では次のトピックについて説明します。
ネイティブ・コードは、JNI関数を呼び出してJava VM機能にアクセスします。JNI関数はインタフェース・ポインタを介して使用できます。インタフェース・ポインタは、ポインタを指すポインタです。このポインタはポインタの配列を指し、このそれぞれのポインタがインタフェース関数を指します。どのインタフェース関数も配列内の事前に定義されたオフセットにあります。次の「インタフェース・ポインタ」図は、インタフェース・ポインタの構成を図示したものです。
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
または同等のAPIが正常に実行されている必要があります。
ライブラリによってJNI_OnLoad_L
と呼ばれる関数がエクスポートされた場合にかぎり、イメージがVMと組み合されているライブラリLが静的にリンクされたとして定義されます。
静的にリンクされているライブラリLによってJNI_OnLoad_L
と呼ばれる関数とJNI_OnLoad
と呼ばれる関数がエクスポートされた場合は、JNI_OnLoad
関数が無視されます。
ライブラリLが静的にリンクされている場合、System.loadLibrary("L")
または同等のAPIの最初の呼出し時に、JNI_OnLoad
関数に指定されたものと同じ引数および期待戻り値で、JNI_OnLoad_L
関数が呼び出されます。
静的にリンクされているライブラリLでは、同じ名前のライブラリが動的にロードされることが禁止されます。
このような関数がエクスポートされた場合は、静的にリンクされているネイティブ・ライブラリLを含むクラス・ローダーでガベージ・コレクションが実行されると、VMによってライブラリのJNI_OnUnload_L
関数が呼び出されます。
静的にリンクされているライブラリLによってNI_OnUnLoad_L
と呼ばれる関数とJNI_OnUnLoad
と呼ばれる関数がエクスポートされた場合は、JNI_OnUnLoad
関数が無視されます。
プログラマはJNI関数RegisterNatives()
を呼び出して、クラスと関連付けられたネイティブ・メソッドを登録することもできます。RegisterNatives()
関数は、静的にリンクされた関数を使用する場合に特に有用です。
動的リンカーはネイティブ・メソッドの名前に基づいてエントリを解決します。ネイティブ・メソッド名は、次のコンポーネントを連結して作られます。
Java_
VMは、ネイティブ・ライブラリに常駐するメソッドについてメソッド名の一致を調べます。VMは、最初にショート名(引数のシグニチャのない名前)を探します。次にロング名(引数のシグニチャが付いた名前)を探します。プログラマがロング名を使用する必要があるのは、ネイティブ・メソッドが別のネイティブ・メソッドによりオーバーロードされたときだけです。しかし、ネイティブ・メソッドが非ネイティブ・メソッドと同じ名前を持っている場合、これは問題ではありません。非ネイティブ・メソッド(Javaメソッド)は、ネイティブ・ライブラリに常駐していません。
次の例では、ネイティブ・メソッドg
はロング名を使ってリンクする必要はありません。もう一方のメソッドg
がネイティブ・メソッドでないため、ネイティブ・ライブラリにないからです。
class Cls1 { int g(int i); native int g(double d); }
単純な名前分解スキームですが、すべてのUnicode文字が有効なC関数名に確実に変換されるようになっています。完全修飾クラス名の中で斜線(「/」)の代わりにアンダースコア(「_」)文字を使用します。名前または型記述子が数字で始まることはないので、次の表に示すように、_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章: JNIの型とデータ構造で、Java型とC型とのマッピングについて説明しています。
次のコード例に、C関数を使用してネイティブ・メソッドf
を実装する方法を示します。ネイティブ・メソッドf
は、次のように宣言されます。
package pkg; class Cls { native double f(int i, String s); // ... }
長い分解名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++を使用すると、次のコード例に示すように、多少すっきりしたコードを書くことができます。
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を取得します。
jmethodID mid = env->GetMethodID(cls, “f”, “(ILjava/lang/String;)D”);
続いてネイティブ・コードは、次のようにメソッド探索の手間をかけずにメソッドIDを繰返し使用できます。
jdouble result = env->CallDoubleMethod(obj, mid, 10, str);
フィールドまたはメソッド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()
目次|前|次 |