ファイヤープロジェクト
参照
2003-07-20T15:13+09:00   matsu
参照とポインタは同じ概念のはずだが,JNIでは明確に区別する必要がある.
「JavaにはポインタがないからCより簡単だ」これはJavaの入門書などでよくみかける.確かにJavaにはポインタと呼ばれるものはないが,参照があるので同じことだ.まともなプログラミング言語ならポインタに相当するものは大抵あるものだ.ないとなるとその言語の表現力に大きく影響してしまう.で,Cにおけるポインタとは何か.「オブジェクトを指し示すもの」である.多くの場合それはアドレスで実装されるが,別にC言語の仕様ではない(ハズ)が,ここではアドレスで実装するものと考える.Javaにおける参照とは何か.これもやはり「オブジェクトを指し示すもの」である.実装についての仕様はよくわからないが,以下を踏まえておく必要がある.
  • JVMはGCでオブジェクトを削除する.
  • JVMはGCでオブジェクトを再配置する.
オブジェクトのアドレスがJVMによって動的に変化するから,参照は直接,または短絡的にアドレスで実装しいるわけではないと思う.オブジェクトへの参照と実際のアドレスはJVMによって管理される.ポインタと参照にはこのような違いがあるが,ポインタと参照は同じ「オブジェクトを指し示すもの」であるので,プログラマから見ればC,Javaそれぞれ単独でコーディングする上では両者の違いはほとんどない(「ポインタ演算」相当の「参照演算」なんてものはないが).が,JNIで両者が出会うとき,ポインタと参照の違いは明確に意識しておく必要がある.
ポインタと参照の違いは簡単に言うと,「プログラマが責任をもって管理するか,JVMがいろいろと操作をするか」である.そしてこの違いはCとJavaが出会うJNIのネイティブコードでは特に意識する必要がある.ネイティブコードはCもしくはC++である.いうまでもなく,ポインタは従来どおりのポインタとして扱える.問題は参照である.概念的にはポインタと参照は同じだが,参照はJVMにより管理,操作される.したがって,たとえ参照がポインタと同じように見えてもプログラマが好き勝手に保持,操作してはいけない.
JVMはネイティブコードに渡した参照に関する情報をテーブルに保持する.これにより,JVMは「ネイティブコードでこの参照が使用されているからGCのときに注意しなければならない(削除や移動をしてはいけない)」ことがわかる.このテーブルに保持される参照にはネイティブメソッドの引数にある全ての参照と返り値のための参照が含まれる.このテーブルに含まれる参照はローカル参照と呼ばれる.ローカル参照はそのネイティブメソッドの呼び出しの間でだけ有効である.すなわちネイティブコードのグローバル変数で参照を保持しておいて,複数のネイティブメソッドで使用することはできない.ネイティブメソッドの呼び出しが異なれば,テーブルに保持されている参照は無効になる.したがって,別のネイティブメソッドでその参照を使用すると結果は保証されない.やってはいけない例を以下に示す.まず不正なネイティブメソッドを呼び出すJavaクラス.
上のクラスファイルから生成されるヘッダ.
そして不正なネイティブコード.
上のネイティブコードでは,methodAでオブジェクトへの参照である引数objをグローバル変数invalidLocalReferenceに格納してmethodBでそれを使用している.これを実行すると私の環境では以下のようになった.
call methodA
MethodA : hasoCode = 8026323
call methodB
Now, access to invalid reference

Unexpected Signal : 11 occurred at PC=0x4013c75b
Function name=(N/A)
Library=/usr/lib/j2re1.3/lib/i386/client/libjvm.so

...省略...

Local Time = Mon Jun  2 02:18:34 2003
Elapsed Time = 0
#
# HotSpot Virtual Machine Error : 11
# Error ID : 4F530E43505002CC
# Please report this error at
# http://java.sun.com/cgi-bin/bugreport.cgi
#
# Java VM: Java HotSpot(TM) Client VM (Blackdown-1.3.1-FCS mixed mode)
#
# An error report file has been saved as hs_err_pid3952.log.
# Please refer to the file for further information.
#
アボートしました
なんかアボートした.黙ってそのまま実行されるよりはましだ.
先のサンプルは,ネイティブコードにおいてローカル参照をグローバル変数に格納する間違った例であった.しかし,JNIでは参照をネイティブコードでグローバル変数に格納する手段を提供されている.このとき,その参照はグローバル参照と呼ばれる.具体的な方法は,JVMにあるローカル参照をネイティブコードにおいてグローバルに参照する(グローバル参照とする)ことを伝えるものである.それはJNIEnvの関数であり,jni.hでは以下のように定義されている.
jobject (JNICALL *NewGlobalRef)
      (JNIEnv *env, jobject lobj);
引数にlobjとあるのはローカル参照ということだろう.そして返り値をグローバル参照としてグローバル変数に格納すればよい.この関数を使用して先の間違ったサンプルを修正してみた.まず,グローバル参照を使用するネイティブコードを呼び出すJavaコード.
そして上のJavaコードから生成されるヘッダ.
そしてグローバル参照を使用するネイティブコード.
NewGlobalRefで,ネイティブコードでグローバル参照を使用することをJVMに伝える.これによりJVMはグローバル参照をGCの対象にしない.ネイティブコードでグローバル参照を開放にするには,上のネイティブコードのようにDeleteGlobalRefを使用する.
void (JNICALL *DeleteGlobalRef)
      (JNIEnv *env, jobject gref);
DeleteGlobalRefを実行した後には,
gref = NULL;
としておいた方がお行儀がよいだろう.サンプルでは,これをコメントアウトしている.この状態でコンパイル,実行すると,
call methodA
MethodA : hasoCode = 8026323
call methodB
Now, access to globalReference
MethodB : hasoCode = 8026323
Now, call DeleteGlobalRef and access to globalReference
MethodB : hasoCode = 1217030
となった.これに対してgref = NULLの行のコメントアウトを外してコンパイル,実行した場合は,先の間違ったサンプルでも示したエラーが出てアボートする.ダメなときはダメってはっきり言ってくれる方が傷が浅くて済む(切ない思い出).
参照についてここでちょっとまとめておく.
  • JNIEnvから呼び出す関数に現れる参照にはグローバル参照,ローカル参照のどちらも使用できる.
  • JNIEnvから呼び出す関数からの返り値が参照であれば,それはローカル参照である.
  • ネイティブメソッドはグローバル参照とローカル参照のどちらも返すことができる.
  • ローカル参照はそのネイティブメソッドを実行しているスレッド内でのみ有効.
matsu(C)
Since 2002
Mail to matsu