目次||

第2章: 設計の概要

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

この章では次のトピックについて説明します。

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

ネイティブ・コードは、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はクラス・ローダーごとのロードされたネイティブ・ライブラリのリストを内部的に維持します。 ベンダーは、名前ができるだけ競合しないネイティブ・ライブラリ名を選択する必要があります。

動的にリンクされたライブラリと静的にリンクされたライブラリのサポート、およびそれぞれのライフサイクル管理の"load"および"unload"関数フックは、ライブラリとバージョン管理にある「呼び出しAPI」セクション」で詳しく説明されています。

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

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

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

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

class Cls1 {
    int g(int i);
    native int g(double d);
}

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

Unicode文字の変換
エスケープ・シーケンス 表示
_0xxxx ASCII英数字([A-Za-z0-9])以外の文字を表すUnicode文字xxxx _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とネイティブ・メソッド間でコピーされます。 他方、任意の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配列を反復演算し、各要素をすべて関数呼出しによって取り出すことは、非効率です。

1つの解決法では、"pinning"という概念が導入されているため、ネイティブ・メソッドは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つのケースがあります。

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

非同期の例外

1つのスレッドは、Java 2 SDKリリース1.2以降で非推奨されているThread.stop()メソッドを呼び出すことによって、別のスレッドで非同期例外を発生させることがあります。 Thread.stop()を使用することを強く推奨します。Thread.stop()は一般的に不確定なアプリケーション状態につながります。

さらに、JVMは、JNI APIコールの直接の結果ではなく、さまざまなJVM内部エラーのために、現在のスレッドで例外を生成することがあります: VirtualMachineErrorのようなStackOverflowErrorまたはOutOfMemoryError これらは、非同期例外とも呼ばれます。

非同期例外は、現在のスレッドのネイティブ・コードの実行に直ちには影響しません:

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

ネイティブ・メソッドは、ExceptionOccurred()チェックを必要な場所に挿入する必要があります(他の例外チェックなしで長い実行コードなど)。 これにより、現在のスレッドが妥当な時間内に非同期例外に確実に応答するようになります。 ただし、その非同期性のため、コールの前に例外チェックを行うことは、チェックとコールの間で非同期例外が発生しないことを保証するものではありません。

例外処理

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

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

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

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

目次||