はじめにJNIはJavaとCベースのコード間でやり取りを行うための低レベルのメカニズムです。この技術の最も一般的な利用法は、Javaのクロスプラットフォーム機能やAPIと簡単には対応がつかないシステム機能にアクセスすることです。Mac OS Xの場合は、Mac OS Xの「アドレスブック」のデータにアクセスしたり、Quartz Composerのコンポジションを表示することなどが考えられます。 また、JNIを使用すると、CコードベースからJavaへの、またはその逆の段階的な移行も容易に行うことができます。プロジェクト全体を一度に移行するのではなく、JNIを使用して複数のリリースで変換を実施し、たとえば新しいJavaコードと残りのCコードとの統合の状況を維持できます。 最終的に、JNIはネイティブなアプリケーションからJVMを作成してやり取りするための呼び出しインターフェイスを提供します。 このテクニカルノートでは、Mac OS X上におけるJNIの扱い方の詳細について説明します。データおよびオブジェクト参照の簡単な変換についてから始めて、Java GUI内でのネイティブなCocoa GUIオブジェクトの統合など、高度なトピックへ進みます。また、問題の解決と回避のために、スレッド処理に関して考慮すべき事柄と各種の手法についても述べます。最後に、Core FoundationまたはCocoaアプリケーションの中からJVMを正しく作成する方法についても説明します。 このテクニカルノートで述べる情報や手法は、Mac OS X上のJava 1.4以降に特有のものです。JNIに関する基本知識があることを前提としています。JNIについて詳しくない方は、まず『The Java Native Interface: Programmer's Guide and Specification』をお読みください。 XcodeでのJNIアプリケーションのビルドXcode IDEは、1つのプロジェクトからJavaコードとネイティブコードの両方をビルドしてバンドルできるため、JNIアプリケーションを作成するための非常に強力なツールになります。しかしこのテクニカルノートでは、JNIプログラミングを重点的に取り上げます。以下に示す多数のサンプルにはすべて、その出発点としてXcodeプロジェクトが含まれています。XcodeでゼロからJNIプロジェクトをビルドする場合は、『Building a Universal JNI Project With Xcode』を参照してください。 Javaコードとネイティブコードの間のデータ転送文字列の取り扱いJNIを使ってJavaの ネイティブ文字列からのJava文字列の作成Mac OS Xでは、 リスト1:NSStringからのJava文字列の作成 NSString *myNSString = @"This is an NSString"; // lengthはUTF-16文字の数を返すが、 // それは必ずしも出力/結合される文字の数ではない jsize buflength = [myNSString length]; unichar buffer[buflength]; [myNSString getCharacters:buffer]; jstring javaStr = (*env)->NewString(env, (jchar *)buffer, buflength); リスト2には、CまたはC++でプログラミングするときに、Core Foundationの リスト2:CFStringからのJava文字列の作成
CFStringRef myCFString = CFSTR("This is a CFString");
CFRange range;
range.location = 0;
// CFStringGetLengthはUTF-16文字の数を返すが、
// それは必ずしも出力/結合される文字の数ではない
range.length = CFStringGetLength(myCFString);
UniChar charBuf[range.length];
CFStringGetCharacters(myCFString, range, charBuf);
jstring javaStr = (*env)->NewString(env, (jchar *)charBuf, (jsize)range.length);
Java文字列からのネイティブ文字列の作成Java文字列からネイティブ文字列( リスト3:Java文字列からのNSStringの作成
const jchar *chars = (*env)->GetStringChars(env, my_jstring, NULL);
NSString *myNSString = [NSString stringWithCharacters:(UniChar *)chars
length:(*env)->GetStringLength(env, my_jstring)];
(*env)->ReleaseStringChars(env, my_jstring, chars);
リスト4:Java文字列からのCFStringの作成
const jchar *chars = (*env)->GetStringChars(env, my_jstring, NULL);
CFStringRef myCFString = CFStringCreateWithCharacters (kCFAllocatorDefault,
chars, (*env)->GetStringLength(env, my_jstring));
(*env)->ReleaseStringChars(env, my_jstring, chars);
重要:上記の例はどちらも、必要不可欠な Unicode補助文字の取り扱い上記の例は、環境間における文字列全体の変換を示しています。Unicode補助文字(
オブジェクト参照の取り扱いネイティブコードでJavaオブジェクト参照を使用するための規則は、JavaコードでCポインタを使用するための規則と同様に、Mac OS Xでも他のプラットフォームとまったく変わりません。ただし、オブジェクト参照をJNIと共有する際の要点について、簡単に触れておく必要があります。 ほとんどすべての型のJavaオブジェクトへの参照が、JNIでは汎用的な ポインタをJavaコードに返すJNI関数には、戻り型として Javaとネイティブとの間のグラフィカルな対話単純なデータ共有のほかに、JNIを使用すると、基盤となるプラットフォームのユーザインターフェイスリソースにアクセスすることもできます。JNIを使ってJavaとCocoaのユーザインターフェイスコンポーネントを統合するためのメカニズムがいくつか用意されています。このセクションでは、これらのメカニズムと、それらをうまく利用するための重要なガイドラインについて述べます。以下の内容と例は、QuartzとCocoaについてある程度理解していることを前提としています。これらの技術についての詳細は、「Getting Started With Cocoa」および『Quartz 2D Programming Guide』を参照してください。 AWT Native InterfaceAWT Native Interface (JAWT)は、ネイティブコードを使用してJavaコンポーネントに描画をするための最も古く、最も一般的な方法です。JAWT対応のAWTコンポーネントは、ネイティブライブラリを使用して、Quartz (Core Graphics)、Core Image、またはMac OS Xで利用可能な他のグラフィカルフレームワークにアクセスできます。 JAWTの概説と使い方については、『The AWT Native Interface』を参照してください。サンプルコードプロジェクト「JAWTExample」では、Mac OS Xに特有の手順と構造について説明しています。 ネイティブレンダリングに加えて、JAWTでは(Mac OS Xの場合)Javaコンポーネントの CocoaComponent
必要なことは、何か CocoaComponentのメッセージング
リスト7:CocoaComponent.sendMessageの使用例
public void actionPerformed(ActionEvent e) {
if (e.getSource() == loadButton) {
view.loadComposition ("Particle System.qtz");
} else {
view.startRendering();
}
}
上記の リスト8:特定の用途のためのsendMessageの抽象化
final static int LOAD_MESSAGE = 0;
final static int START_MESSAGE = 1;
public void loadComposition(String fullPath) {
sendMessage(LOAD_MESSAGE, fullPath);
}
public void startRendering() {
sendMessage(START_MESSAGE, null);
}
最後に、 リスト9:JavaのsendMessage呼び出しに応答するためのawtMessage:message:env:の実装
- (void) awtMessage:(jint)messageID message:(jobject)message env:(JNIEnv *)env {
jchar *chars;
switch (messageID) {
case apple_dts_samplecode_qccocoacomponent_JavaQCView_LOAD_MESSAGE:
// CocoaComponent.sendMessageがオブジェクトを受け取る。以下ではStringが必要になる
if ((*env)->IsInstanceOf(env, message, (*env)->FindClass(env, "java/lang/String"))) {
chars = (*env)->GetStringChars(env, (jstring)message, NULL);
NSString *cocoaPath = [NSString stringWithCharacters:(unichar *)chars
length:(*env)->GetStringLength(env, message)];
[self loadCompositionFromFile:cocoaPath];
(*env)->ReleaseStringChars(env, message, chars);
} else {
jclass argExcClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException");
(*env)->ThrowNew(env, argExcClass,
"JavaQCView: sendMessage with an ID of LOAD_MESSAGE must be accompanied by a String");
}
break;
case apple_dts_samplecode_qccocoacomponent_JavaQCView_START_MESSAGE:
[self startRendering];
break;
default:
break;
}
}
CocoaComponentからのメッセージの受信Javaをコールバックするのはもう少し複雑になりますが、それでも完全な標準のJNIを実行することになります。たとえば、Javaダイアログを表示してユーザに警告するだけなら、特定のエントリポイントは必要ありません。しかし、CocoaビューがそのJavaピア( リスト10:CocoaComponentへのグローバル参照の保存
JNIEXPORT jlong JNICALL Java_apple_dts_samplecode_cwcocoacomponent_JavaColorWell_createNSViewLong
(JNIEnv *env, jobject caller) {
return (jlong)[JavaColorWell colorWellWithCaller:caller env:env];
}
@implementation JavaColorWell
+ (id) colorWellWithCaller:(jobject) caller env:(JNIEnv *)env {
return [[[JavaColorWell alloc] initWithCaller:caller env:env] autorelease];
}
// 外側のJavaオブジェクトを指すJNIグローバル参照を取得する
// これを後で使用して、カラー変更後にAWTイベントを作動させる
- (id) initWithCaller:(jobject)caller env:(JNIEnv *)env {
self = [super init];
// "javaPeer"はJavaColorWell.hに宣言されている
javaPeer = (*env)->NewGlobalRef(env, caller);
return self;
}
次に、Cocoa JNIをよく理解している方であれば、上記の手法では問題が生じることに気付くでしょう。グローバル参照がガーベジコレクションに影響を及ぼすため、グローバル参照が存在するかぎり、 リスト11:基盤のCocoaビューへのremoveNotifyの通知
final static int REMOVE_NOTIFY = 0;
// 階層から削除されたことをピアに通知する
public void removeNotify() {
sendMessage(REMOVE_NOTIFY, null);
super.removeNotify();
}
リスト12:removeNotifyに対応したJNIグローバル参照の削除
- (void) awtMessage:(jint)messageID message:(jobject)message env:(JNIEnv *)env {
switch (messageID) {
// コンポーネントがそのコンテナから削除されたら、JavaピアへのglobalRefを削除する
case apple_dts_samplecode_cwcocoacomponent_JavaColorWell_REMOVE_NOTIFY:
if (javaPeer != NULL) {
(*env)->DeleteGlobalRef(env, javaPeer);
}
break;
// その他の場合...
}
}
スレッドセーフなJNIプログラミング前のセクションに示した例は基本的にかなり単純です。明確なエントリポイントが定義されており、JavaおよびCocoa環境はそれぞれのジョブを実行し、通常のアプリケーションの動作には明らかな変化がありません。しかし、CocoaとAWTの両方のイベントモデルを利用したコードを統合する場合は、注意が必要です。 CocoaにはJava 1.4以降のAWTが実装されているのはそのとおりですが、AWT Event Queueは基盤のCocoaアプリケーションのメインイベントループ(「スレッド0」とも呼ばれるメインスレッドで実行する)とは別のスレッドで実行されます。次に、JAWTや 注:このセクションでは、ほぼ例外なく「AWT」という用語を使っていますが、Swingアプリケーションにも当てはまります。「AppKit」という用語は、Cocoa Application KitフレームワークとサポートしているAPIを指します。
上記の最初の2つの項目は、AppKitおよびAWTのあらゆる統合( AWT/SwingからのAppKitの呼び出しビューの更新や他のイベントベースのアクションをもたらすAppKitを呼び出すときには、メインのAppKitスレッドでそれらの呼び出しを行う必要があります。 リスト13:AppKitの操作のためのperformSelectorOnMainThreadの使用
JNIEXPORT void JNICALL Java_apple_dts_samplecode_jsheets_JSheetDelegate_nativeShowSheet
(JNIEnv *env, jclass caller, jint type, jobject parent, jobject listener) {
// メインのAppKitスレッドでないかぎり、AutoreleasePoolが用意されていると想定しないこと
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
JSheetDelegate *jdel;
NSWindow *parentWindow = GetWindowFromComponent(parent, env);
switch (type) {
case apple_dts_samplecode_jsheets_JSheetDelegate_OPEN_PANEL:
jdel = [JSheetDelegate delegateWithListener:listener env:env];
// 後で使用するためにデリゲートを保持する。使用後にopenPanelDidEnd:で解放する
[jdel retain];
[jdel performSelectorOnMainThread:@selector(showOpenPanelForWindow:)
withObject:parentWindow waitUntilDone:NO];
break;
}
[pool release];
}
- (void) showOpenPanelForWindow:(NSWindow *)parentWindow {
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setAllowsMultipleSelection:YES];
[op beginSheetForDirectory:NSHomeDirectory() file:nil types:nil
modalForWindow:parentWindow modalDelegate:self
didEndSelector:@selector(openPanelDidEnd:returnCode:contextInfo:) contextInfo:nil];
}
ここで注意すべき重要なことが2つあります。 純粋なCocoaアプリケーションでも、たとえば、ソケットから情報を受け取った後に周辺のスレッドがユーザインターフェイスを更新する必要があるときなどは、この手法が必要です。 ここで、前述の AppKitからのAWT/Swingの呼び出し逆方向(AppKitからのAWTの呼び出し)の場合にも同じ原則が当てはまります。ネイティブコードが、それが通知対象として登録されているCocoa通知を受け取るものとします。通知はメインのAppKitスレッドで行われるため、通知ハンドラからAWTを直接呼び出すと、アプリケーションで容易にデッドロックが生じるおそれがあります。解決策として、 リスト14:JavaへのNSSavePanelの結果の報告
- (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
JNIEnv *env;
bool shouldDetach = false;
if (GetJNIEnv(&env, &shouldDetach) != JNI_OK) {
NSLog(@"savePanelDidEnd: could not attach to JVM");
return;
}
// ユーザがSaveをクリックした場合は、Javaをコールバックする
if (returnCode == NSOKButton) {
if (saveFinish_mID != NULL) {
jsize buflength = [[sheet filename] length];
jchar buffer[buflength];
[[sheet filename] getCharacters:(unichar *)buffer];
jstring str = (*env)->NewString(env, buffer, buflength);
(*env)->CallStaticVoidMethod(env, jDelegateClass, saveFinish_mID, sheetListener, str);
(*env)->DeleteLocalRef(env, str);
} else NSLog(@"savePanelDidEnd: null Java methodID");
} else if (cancel_mID != NULL) {
(*env)->CallStaticVoidMethod(env, jDelegateClass, cancel_mID, sheetListener);
}
(*env)->DeleteGlobalRef(env, sheetListener);
if (shouldDetach) {
(*jvm)->DetachCurrentThread(jvm);
}
[self autorelease];
}
リスト15:AWTの操作のためのinvokeLaterの使用
private static void fireSaveSheetFinished(final SaveSheetListener ssl, final String filename) {
EventQueue.invokeLater(new Runnable() {
public void run() {
ssl.saveSheetFinished(new SheetEvent(filename));
}
});
}
注:リスト14の イベントスレッドではないスレッドからのAppKitまたはAWTの呼び出し周辺の「ワーカー」スレッドを扱うときは、前述のガイドラインが若干変わります。それでも、AppKitにメッセージを送るには JavaおよびCocoaのスレッド安全性に関する詳細は、『Multithreaded Programming Topics』の「Cocoa Thread Safety」および『Threads and Swing』を参照してください。 ネイティブコードからのJava仮想マシンの呼び出しVM呼び出しインターフェイスの詳細については、『The Java Native Interface: Programmer's Guide and Specification』の7章を参照してください。ネイティブなMac OS XアプリケーションへのJVMの組み込みは、他のプラットフォームの場合と大きく異なる点の1つです。AWTを使用する場合は、アプリケーションのメインスレッドでJVMを開始してはなりません。多くのチュートリアルやドキュメントの説明では、メインスレッドでJVMを開始しているため、Mac OS X固有のこの要件を認識しておくことが重要です。 以降のセクションでは、Core FoundationおよびCocoaの環境からJVMを正しく作成する方法と、Javaの特定のバージョンを明示的に要求する方法を説明します。 Core FoundationからのJVMの作成JVMを作成するCore Foundationデベロッパの方は( リスト16:JVMを呼び出す新しいpthreadの作成
/* VMを実行するスレッドを開始する。*/
pthread_t vmthread;
/* 最初のpthreadのスタックサイズをコピーして、新しいpthreadを作成する */
struct rlimit limit;
size_t stack_size = 0;
int rc = getrlimit(RLIMIT_STACK, &limit);
if (rc == 0) {
if (limit.rlim_cur != 0LL) {
stack_size = (size_t)limit.rlim_cur;
}
}
pthread_attr_t thread_attr;
pthread_attr_init(&thread_attr);
pthread_attr_setscope(&thread_attr, PTHREAD_SCOPE_SYSTEM);
pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED);
if (stack_size > 0) {
pthread_attr_setstacksize(&thread_attr, stack_size);
}
/* JVMを開始するスレッドを開始する。*/
/* startupJavaはJVMを作成する別個の関数である */
pthread_create(&vmthread, &thread_attr, startupJava, launchOptions);
pthread_attr_destroy(&thread_attr);
CocoaからのJVMの作成CocoaからのJVM呼び出しの背後にある原理は、Core Foundationからの呼び出しの場合と同じです。Cocoaデベロッパは リスト17:JVMを呼び出す新しいNSThreadのデタッチ
- (IBAction)applicationWillFinishLaunching:(id)sender {
// 新しいスレッドをデタッチし、そのスレッドでVMを呼び出す。
[NSThread detachNewThreadSelector:@selector(startupJava:)toTarget:self withObject:nil];
}
- (void)startupJava:(id)userData {
// 新しいネイティブスレッド(CocoaおよびJava)はすべて自動解放プールを必要とする。
NSAutoreleasePool *pool = [[NSAutoreleasePool allocWithZone:NULL] init];
// JVMを起動する(startupJavaはどこか他の場所で定義されているC関数)
startupJava();
[pool release];
// JVMが終了したら、このアプリケーションを終了する
[[NSApplication sharedApplication] terminate:self];
}
メインスレッドの使用を控えるJavaコードでAWT/SwingベースのGUIを使用している場合は、ネイティブなアプリケーションのメインスレッドからJVMを開始できないことを理解しておくことが重要です。そのようなアプリケーションでは、Cocoaのイベントループが使用できるように、メインスレッドを解放しておく必要があります。上記の例はどちらも、この規則に従っています。リスト16では、 リスト18:メインスレッドでのJavaの開始後にAWTを初期化する際のエラー
Exception in thread "main" java.lang.InternalError: Can't start the AWT [...]
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1751)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1668)
at java.lang.Runtime.loadLibrary0(Runtime.java:822)
at java.lang.System.loadLibrary(System.java:992)
at sun.security.action.LoadLibraryAction.run(LoadLibraryAction.java:50)
at java.security.AccessController.doPrivileged(Native Method)
at sun.awt.NativeLibLoader.loadLibraries(NativeLibLoader.java:38)
at sun.awt.DebugHelper.<clinit>(DebugHelper.java:29)
at java.awt.Component.<clinit>(Component.java:545)
「ヘッドレス」Javaコード(AWTを使用しないコード)を実行するネイティブランチャには、このことが義務付けられていないため、メインスレッドでJVMを安全に開始できます。ただし、呼び出されたJavaコードがAWTイベントキューを使用する可能性が少しでもある場合は、メインスレッドを解放しておく必要があります。状況を問わず、メインスレッド以外からJVMを開始してリスクを最小限に抑えることを強く推奨します。 特定のJ2SEバージョンの要求Mac OS X上に複数のJavaのバージョンが存在していると、実際に必要なJavaバージョンを選択する必要があり、これはネイティブランチャにとってジレンマとなります。この問題を解決するには、 Mac OS X上で呼び出しAPIを使用するときに、次のように覚えておくべきことがいくつかあります。
サンプルコードプロジェクト「simpleJavaLauncher」では、前の2つの例で使用できる リスト19:Core FoundationからのJ2SE 5.0の検出と要求
CFStringRef targetJVM = CFSTR("1.5");
CFBundleRef JavaVMBundle;
CFURLRef JavaVMBundleURL;
CFURLRef JavaVMBundlerVersionsDirURL;
CFURLRef TargetJavaVM;
UInt8 pathToTargetJVM [PATH_MAX];
struct stat sbuf;
// 識別子を使用してJavaVMバンドルを探す
JavaVMBundle = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.JavaVM") );
if(JavaVMBundle != NULL) {
// JavaVMバンドルのパスを取得する
JavaVMBundleURL = CFBundleCopyBundleURL(JavaVMBundle);
CFRelease(JavaVMBundle);
if(JavaVMBundleURL != NULL) {
// パスにバージョンコンポーネントを付加する
JavaVMBundlerVersionsDirURL = CFURLCreateCopyAppendingPathComponent
(kCFAllocatorDefault,JavaVMBundleURL,CFSTR("Versions"),true);
CFRelease(JavaVMBundleURL);
if(JavaVMBundlerVersionsDirURL != NULL) {
// パスにターゲットJVMのバージョンを付加する
TargetJavaVM = CFURLCreateCopyAppendingPathComponent
(kCFAllocatorDefault,JavaVMBundlerVersionsDirURL,targetJVM,true);
CFRelease(JavaVMBundlerVersionsDirURL);
// 必要なメジャーバージョンがフレームワークにあるか確認する
if(TargetJavaVM != NULL) {
if(CFURLGetFileSystemRepresentation
(TargetJavaVM,true,pathToTargetJVM,PATH_MAX )) {
if(stat((char*)pathToTargetJVM,&sbuf) == 0) {
if(CFStringGetCString(targetJVM, (char*)pathToTargetJVM,
PATH_MAX, kCFStringEncodingUTF8)) {
setenv("JAVA_JVM_VERSION", (char*)pathToTargetJVM,1);
}
}
CFRelease(TargetJavaVM);
}
}
}
}
}
参考資料ドキュメント改訂履歴
掲載日: 2006-04-17 | ||||||||||
|