目次 | 前の項目 | 次の項目 Java Native Interface 仕様

設計の概要


第 2 章

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

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

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

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

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

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

JNI インタフェースポインタは現在のスレッドの中だけで有効です。したがって、ネイティブメソッドがスレッド間でインタフェースポインタを渡さないようにしてください。JNI を実装している VM は、JNI インタフェースポインタによって指示された領域にスレッドのローカルデータを割り当てて格納することもできます。

ネイティブメソッドは JNI インタフェースポインタを引数として受け取ります。したがって、VM が同じ Java スレッドからネイティブメソッドに複数の呼び出しを行う場合は、ネイティブメソッドに同じインタフェースポインタを渡すことが保証されています。しかし、ネイティブメソッドは、異なる Java スレッドからでも呼び出すことができるので、異なる JNI インタフェースポインタを受け取ることもあります。

ネイティブメソッドのロードとリンク

ネイティブメソッドは、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() 関数は、静的リンクされた関数を使用する場合に特に有用です。

ネイティブメソッド名の解決

動的リンカはネイティブメソッドの名前に基づいてエントリを解決します。ネイティブメソッド名は、次のコンポーネントを連結して作られます。

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 ... 

} 

常にインタフェースポインタ env を使用して Java オブジェクトを操作していることに注意してください。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 に基づいてすべてのローカル参照を解放すべきです。しかし、プログラマが明示的にローカル参照を解放する必要がある場合もあります。たとえば、次のような状況があります。

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

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

ローカル参照の実装

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

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

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

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

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 のクラッシュを含む、不測の結果に至ることがあります。

Java の例外

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

例外とエラーコード

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

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

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

非同期な例外

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

同期した例外を発生させる可能性のある JNI 関数だけが非同期な例外をチェックすることに注意してください。

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

例外の処理

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

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


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

 


目次 | 前の項目 | 次の項目