目次 | 前へ | 次へ

設計の概要


第 2 章

この章は JNI の主な設計の問題に焦点をあてています。このセクションの設計の問題のほとんどはネイティブメソッドと関連があります。呼び出し API の設計については、「第 5 章」に掲載されています。

JNI インタフェースの関数とポインタ

ネイティブコードは、JNI 関数を呼び出して Java VM 機能にアクセスします。JNI 関数はインタフェースポインタを介して使用できます。インタフェースポインタは、ポインタを指すポインタです。このポインタはポインタの配列を指し、このそれぞれのポインタがインタフェース関数を指します。どのインタフェース関数も配列内の事前に定義されたオフセットにあります。図 2-1 は、インタフェースポインタの構成を図示したものです。

 

このイメージについては前の文脈で説明しています。

 

図 2-1 インタフェースポインタ

JNI インタフェースは、C++ 仮想関数テーブルまたは COM インタフェースのように構成されています。固定された組み込み関数エントリでなくインタフェーステーブルを使用する利点は、JNI 名前空間がネイティブコードと分離できるようになることです。VM は複数バージョンの JNI 関数テーブルを容易に提供できます。たとえば、VM は次のように 2 つの JNI 関数テーブルをサポートすることもできます。

  • 一方のテーブルは、不正な引数の検査を厳密に実行するので、デバッグに適している
  • もう一方のテーブルは、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_
  • 分解された完全修飾クラス名
  • 下線 (「_」) 区切り文字
  • 分解されたメソッド名
  • オーバーロードされたネイティブメソッドでは、2 個の下線 (「__」) に続いて分解された引数のシグニチャー

VM は、ネイティブライブラリに常駐するメソッドについてメソッド名の一致を調べます。VM は、最初にショート名 (引数のシグニチャーのない名前) を探します。次にロング名 (引数のシグニチャーが付いた名前) を探します。プログラマがロング名を使用する必要があるのは、ネイティブメソッドが別のネイティブメソッドによりオーバーロードされたときだけです。しかし、ネイティブメソッドが非ネイティブメソッドと同じ名前を持っている場合、これは問題ではありません。非ネイティブメソッド (Java メソッド) は、ネイティブライブラリに常駐していません。

次の例では、ネイティブメソッド g はロング名を使ってリンクする必要はありません。もう一方のメソッド g がネイティブメソッドでないため、ネイティブライブラリにないからです。


class Cls1 { 

  int g(int i); 

  native int g(double d); 

} 

単純な名前分解スキームですが、すべての Unicode 文字が有効な C 関数名に確実に変換されるようになっています。完全修飾クラス名の中で斜線 (「/」) の代わりに下線 (「_」) 文字を使用します。名前または型記述子が数字で始まることはないので、表 2-1 の例のように、_0、...、_9 をエスケープシーケンスに使用できます。

 

表 2-1 Unicode 文字変換
エスケープシーケンス
表示
_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 は、次のように宣言されます。


package pkg;  

class Cls { 

     native double f(int i, String s); 

     ... 

} 

長い分解名 Java_pkg_Cls_f_ILjava_lang_String_2 を持つ C 関数は、ネイティブメソッド f を実装します。

 

コード例 2-1 C を使用するネイティブメソッドの実装
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 に示すように、多少すっきりしたコードを書くことができます。

 

コード例 2-2 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 とネイティブメソッド間でコピーされます。他方、任意の Java オブジェクトは参照渡しです。VM はネイティブコードに渡されたすべてのオブジェクトがガベージコレクタによって解放されないよう、これらのオブジェクトを追跡しなければなりません。ネイティブコードは、逆に、オブジェクトがもう必要ないことを VM に通知する手段を持たなければなりません。さらに、ガベージコレクタは、ネイティブコードによって参照されるオブジェクトを移動することもできなければなりません。

グローバル参照およびローカル参照

JNI は、ネイティブコードによって使用されるオブジェクト参照をローカル参照グローバル参照の 2 つのカテゴリに分けます。ローカル参照は、ネイティブメソッド呼び出しの間だけ有効で、ネイティブメソッドが復帰すると自動的に解放されます。グローバル参照は、明示的に解放されるまで有効になっています。

オブジェクトは、ローカル参照としてネイティブメソッドに渡されます。JNI 関数によって返される Java オブジェクトはすべてローカル参照です。JNI では、プログラマがローカル参照からグローバル参照を作成できます。Java オブジェクトを扱う JNI 関数は、グローバルとローカルの両方の参照を受け入れます。ネイティブメソッドは、その結果、グローバルまたはローカルのどちらかの参照を VM に返すことになります。

ほとんどの場合、プログラマは、ネイティブメソッドが戻ったあと、VM に基づいてすべてのローカル参照を解放すべきです。しかし、プログラマが明示的にローカル参照を解放する必要がある場合もあります。たとえば、次のような状況があります。

  • ネイティブメソッドが大きな Java オブジェクトにアクセスし、この Java オブジェクトに対してローカル参照を作成する。次にネイティブメソッドは、呼び出し側に返す前に追加の計算を実行する。大きな Java オブジェクトのローカル参照は、このオブジェクトが残りの計算に使用されなくなった場合でも、ガベージコレクトが妨げられる。
  • ネイティブメソッドは多数のローカル参照を作成するが、これらのすべてが同時に使用されるわけではない。VM はローカル参照を追跡するため一定量のスペースを必要とし、そのため多くのローカル参照を作成すると、システムのメモリーがなくなることがある。たとえば、ネイティブメソッドは、大きな配列のオブジェクトを通してループし、その要素をローカル参照として検索し、反復のたびに 1 つの要素で演算する。各反復の終了後、プログラマはその配列要素のローカル参照をもう必要としない。

JNI では、プログラマがネイティブメソッド内の任意の点でローカル参照を手動で削除できます。プログラマが手動でローカル参照を解放できることを保証するため、JNI 関数では、これら関数が結果として返す参照を除いて、余分なローカル参照を作成できないようになっています。

ローカル参照は、これらが作成されたスレッドの中だけで有効です。ネイティブコードは、スレッド間でローカル参照を受渡ししてはいけません。

ローカル参照の実装

ローカル参照子を実装するため、Java VM は Java からネイティブメソッドに制御が移行するたびにレジストリを作成します。レジストリは、移動できないローカル参照を Java オブジェクトにマッピングし、オブジェクトがガベージコレクトされないよう守ります。ネイティブメソッドに渡されるすべての Java オブジェクト (JNI 関数呼び出しの結果として返されるものも含む) は、自動的にレジストリに追加されます。このレジストリは、ネイティブメソッドが返ったあとに削除され、そのすべての項目をガベージコレクトできるようにします。

レジストリを実装するには、テーブル、連結リスト、またはハッシュテーブルを使用するなど、さまざまな方法があります。レジストリの中の項目の重複を避けるため参照のカウントが使用されることがありますが、JNI の実装では重複項目を検出し重複をなくす必要はありません。

ローカル参照は、厳密にネイティブスタックをスキャンしても、忠実に実装することはできません。ネイティブコードは、ローカル参照をグローバルまたはヒープデータ構造に格納することもあります。

Java オブジェクトへのアクセス

JNI は、グローバル参照およびローカル参照への豊富なアクセス機能のセットを提供します。これは、VM が内部的にどのように Java オブジェクトを表現していても、同じネイティブメソッド実装が作動することを意味します。これが決定的な理由となって、JNI は多様な VM 実装でサポートされています。

不透明な参照を介してアクセス用関数を使用するオーバーヘッドは、C データ構造体へ直接アクセスする場合より高くなります。ほとんどの場合に Java プログラマはネイティブメソッドを使用して、このインタフェースのオーバーヘッドが目立たなくなるような重要な (自明的でない) タスクを実行していると考えられます。

プリミティブ配列へのアクセス

このオーバーヘッドは、整数列や文字列のような多くのプリミティブデータ型を含んでいる大きな Java オブジェクトでは受け入れられません。ベクトルおよび行列の計算を実行するために使用されるネイティブメソッドを考えてください。Java 配列を反復演算し、各要素をすべて関数呼び出しによって取り出すことは、非効率です。

ネイティブメソッドが VM に配列の内容の確認を要求できるように、「ピニング」の概念を導入する解決策もあります。そのあとネイティブメソッドは、その要素を指すダイレクトポインタを受け取ります。しかし、このアプローチは次の 2 つのことを意味します。

  • カベージコレクタはピニングをサポートしなければならない。
  • VM はプリミティブ配列をメモリーに切れ目なく連続して配置しなければならない。これはほとんどのプリミティブ配列にとってもっとも自然な実装だが、ブール配列はパックでもアンパックでも実装できる。したがって、ブール配列の正確な配置に基づくネイティブコードは移植できない。

上記の両方の問題を克服する折衷案を採用しています。

第一に、Java 配列のセグメントとネイティブメモリーバッファーの間でプリミティブ配列要素をコピーするための関数のセットを提供します。ネイティブメソッドが大きな配列の中の少数要素だけにアクセスする必要しかない場合は、これらの関数を使用してください。

第二に、プログラマは別の関数のセットを使用して、ピニングされたバージョンの配列要素を検索できます。これらの関数がストレージの割り当ておよびコピーを実行するには Java VM が必要なことを覚えておいてください。これらの関数が実際に配列をコピーできるかどうかは、次のように VM の実装によって決まります。

  • ガベージコレクタがピニングをサポートする場合、配列の配置はネイティブメソッドが予期するものと同じなので、コピーは必要ない。
  • それ以外の場合は、配列が移動できないメモリーブロック (C ヒープの中など) にコピーされ、必要なフォーマット変換が実行される。コピーへのポインタが返される。

このインタフェースは、ネイティブコードが配列要素にアクセスする必要がなくなったことを 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 を再計算するかどうか

延長された期間中にメソッドまたはフィールド ID を使用するかどうか。

JNI は、フィールドまたはメソッド ID がどのように内部的に実装されているかには何の制約も課しません。

プログラミングエラーの報告

JNI は、null ポインタまたは不正な引数型の受け渡しのようなプログラミングエラーについてチェックを行いません。不正な引数型には、Java クラスオブジェクトの代わりに通常の Java オブジェクトを使用するようなことが含まれます。JNI は、次のような理由からこれらのプログラミングエラーについてのチェックを行いません。

  • JNI 関数に起こり得るすべてのエラー条件についてチェックするよう強制すると、通常の (正常な) ネイティブメソッドのパフォーマンスが低下する。
  • 多くの場合、このようなチェックを実行できるほど十分な実行時の情報がない。

ほとんどの C ライブラリ関数は、プログラムエラーに対して保護されていません。たとえば、printf() 関数は、無効アドレスを受け取ると、通常は実行時エラーを起し、エラーコードを返しません。すべての起こり得るエラー条件についてチェックするように C ライブラリ関数に強制すると、ユーザーコードで 1 回チェックしてまたライブラリでも行うというように、チェックが重複する可能性があります。

プログラマは不正なポインタや間違った型の引数を JNI 関数に渡してはいけません。これを行うと、システムの破壊状態または VM のクラッシュを含む、不測の結果に至ることがあります。

Java の例外

JNI では、ネイティブメソッドは任意の Java の例外を発生させることが可能です。ネイティブコードでも、未処理の Java の例外を処理できます。未処理のままになっている Java の例外は VM に送り返されます。

例外とエラーコード

JNI 関数によっては、Java の例外メカニズムを使用してエラー条件を報告するものもあります。ほとんどの場合、JNI 関数は、エラーコードを返し、かつ Java の例外をスローすることによって、エラー状態を報告します。通常、このエラーコードは、通常の戻り値の範囲外にある特殊な戻り値 (NULL など) です。したがって、プログラマは次のことを行うことができます。

  • 最後の JNI 呼び出しの戻り値をすばやくチェックして、エラーが起きているかどうか判断する
  • 関数 ExceptionOccurred() を呼び出して、エラー状態のさらに詳細な記述が含まれている例外オブジェクトを取得する。

プログラマが最初にエラーコードをチェックできない状態で、例外をチェックすることが必要になる場合として、次の 2 つのケースがあります。

  • Java メソッドを呼び出す JNI 関数が Java メソッドの結果を返す。プログラマは ExceptionOccurred() を呼び出して、Java メソッドの実行中に起こり得る例外が起きていないかチェックする必要があります。
  • JNI 配列アクセス関数の一部には、エラーコードを返さないが、ArrayIndexOutOfBoundsException または ArrayStoreException をスローするものがあります。

その他すべての場合は、非エラーの戻り値で、例外がスローされていないことを保証しています。

非同期な例外

マルチスレッドの場合、現在のスレッドではないほかのスレッドが非同期な例外を送信することがあります。非同期な例外が、現スレッドのネイティブコードの例外にすぐに影響することはありませんが、次の時点で影響します。

  • ネイティブコードが、同期した例外を発生させることができる JNI 関数の 1 つを呼び出す
  • ネイティブコードが ExceptionOccurred() を使用して、同期および非同期の例外があるかを明示的にチェックする。

同期した例外を発生させる可能性のある JNI 関数だけが非同期な例外をチェックします。

ネイティブメソッドでは、必要な場所 (ほかの例外のチェックがない密なループの中など) に ExceptionOccurred() のチェックを挿入して、現在のスレッドが非同期の例外に適当な時間の範囲内で応答することを保証する必要があります。

例外処理

ネイティブコードで例外を処理するには、次の 2 とおりの方法があります。

  • ネイティブメソッドは、ただちに復帰して、ネイティブメソッド呼び出しを開始した Java コード中で例外をスローさせることができる。
  • ネイティブコードは、ExceptionClear() を呼び出して例外をクリアしてから、自身の例外処理コードを実行できる。

例外の発生後、ネイティブコードは、ほかの JNI 呼び出しを行う前にまず例外をクリアする必要があります。未処理の例外があるとき、安全に呼び出せる JNI 関数は次のとおりです。


  ExceptionOccurred()
  ExceptionDescribe()
  ExceptionClear()
  ExceptionCheck()
  ReleaseStringChars()
  ReleaseStringUTFChars()
  ReleaseStringCritical()
  Release<Type>ArrayElements()
  ReleasePrimitiveArrayCritical()
  DeleteLocalRef()
  DeleteGlobalRef()
  DeleteWeakGlobalRef()
  MonitorExit()
  PushLocalFrame()
  PopLocalFrame()

 

 


目次 | 前へ | 次へ

Copyright © 1993, 2013, Oracle and/or its affiliates. All rights reserved.