Apple Developer Connection
高度な検索
Member Login ログイン | ご入会 ADC連絡先

Technical Note TN2147
Mac OS XにおけるJNI開発

JNI (Java Native Interface)は、JavaコードとCの派生言語(C、C++、Objective-C)で書かれたコードを統合するための標準メカニズムです。これを使用して、データにアクセスしたり、ネイティブな要素をJavaユーザインターフェイスに組み込んだり、ネイティブなアプリケーションからJava仮想マシン(JVM)を作成したりすることができます。

このテクニカルノートでは、Mac OS XでのJNIプログラミング特有の手法や問題を、必須事項(および禁止事項)の明示的な例を挙げながら説明します。

すでにMac OS X上でJNIを使用している場合、またはMac OS X上のJava以外のフレームワークの1つとのインターフェイスが必要なアプリケーションを、Java 1.4以降で作成している場合は、このテクニカルノートをお読みください。また、アプリケーションからJavaのライブラリやAPIを利用する必要がある、Core FoundationまたはCocoaのデベロッパの方もこのテクニカルノートをお読みください。





はじめに

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のStringオブジェクトを作成したり、それらにアクセスしたりする方法は、他のプラットフォームの場合とまったく変わりません。Javaと同様に、Core FoundationおよびCocoa文字列オブジェクトは、UTF-16 Unicode文字の配列を表します。そのため、Mac OS XでJava文字列型と他のネイティブの文字列型の間で変換を行う場合は、Unicode文字型を使うことが推奨されます。従来のJNIチュートリアルの多くではJava文字列を扱うときにUTF-8文字を使用していたので、このことに言及しておく必要があります。

ネイティブ文字列からのJava文字列の作成

Mac OS Xでは、jchar型とUniChar型は相互に交換可能で、どちらからでも安全に型変換できるため、Java文字列オブジェクトとネイティブ文字列オブジェクト間の変換は非常に簡単です。Cocoa文字列(NSString)からJava文字列(jstring)への変換をリスト1に示します。

リスト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のCFStringからjstringを作成する方法を示します。

リスト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文字列からネイティブ文字列(NSStringCFString)を作成するのも同様に簡単です。2つの手法をリスト3とリスト4に示します。

リスト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);

重要:上記の例はどちらも、必要不可欠なReleaseStringCharsの呼び出しで終わっていますが、これはJava文字列にアクセスする必要がなくなったことをJVMに通知しています。『The Java Native Interface: Programmer's Guide and Specification』のセクション10.11に次のような記述があります:「ReleaseStringChars関数の呼び出しを忘れると、jstringオブジェクトが無期限に固定されてメモリフラグメンテーションを起こしたり、C のコピーが無限に保持され、メモリリークが生じることがあります」。あるいは、Javaのcharまたはbyte配列をJNI経由で渡すと、GetStringCharsReleaseStringCharsを呼び出す必要がなくなります。

先頭に戻る

Unicode補助文字の取り扱い

上記の例は、環境間における文字列全体の変換を示しています。Unicode補助文字(0x100000x10FFFF)を扱う場合は、1文字または部分文字列にアクセスすると悪影響をもたらすことがあります。Mac OS XとJavaはどちらも、補助文字をUTF-16文字のサロゲートペアとして扱います。任意のインデックスからUTF-16文字にアクセスすると、代理ペアの半分しか取得されないおそれがあります。必要なデータをすべて確実に取得するために、以下のAPIを使用してください。

先頭に戻る

オブジェクト参照の取り扱い

ネイティブコードでJavaオブジェクト参照を使用するための規則は、JavaコードでCポインタを使用するための規則と同様に、Mac OS Xでも他のプラットフォームとまったく変わりません。ただし、オブジェクト参照をJNIと共有する際の要点について、簡単に触れておく必要があります。

ほとんどすべての型のJavaオブジェクトへの参照が、JNIでは汎用的なjobject型で表されます。その他の明示的な型としては、jstring(前のセクションを参照)およびjclassjava.lang.Classを示す)があります。jobject参照は通常、JNIのグローバル参照を使用してCの構造体またはC++/Objective-Cのオブジェクトに保存されます。グローバル参照を使用するための手法とルールについては、『The Java Native Interface: Programmer's Guide and Specification』の5章を参照してください。

ポインタをJavaコードに返すJNI関数には、戻り型としてjlongを設定する必要があります。これにより、ポインタを64ビットのJava long変数に保存でき、必要な処理のために後でJNI関数に渡すことができます。jlongを32ビットシステム上で適切なポインタ型に変換すると、アドレスが保存されている下位32ビットが正しく維持されます。

先頭に戻る

Javaとネイティブとの間のグラフィカルな対話

単純なデータ共有のほかに、JNIを使用すると、基盤となるプラットフォームのユーザインターフェイスリソースにアクセスすることもできます。JNIを使ってJavaとCocoaのユーザインターフェイスコンポーネントを統合するためのメカニズムがいくつか用意されています。このセクションでは、これらのメカニズムと、それらをうまく利用するための重要なガイドラインについて述べます。以下の内容と例は、QuartzとCocoaについてある程度理解していることを前提としています。これらの技術についての詳細は、「Getting Started With Cocoa」および『Quartz 2D Programming Guide』を参照してください。

AWT Native Interface

AWT 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コンポーネントのNSViewベースのネイティブピアや親のNSWindowなど、プラットフォーム固有のリソースにアクセスすることもでき、これには標準的ではありませんが適正な使い方であるという効果があります。サンプルコードプロジェクト「JSheets」では、JAWTを使用して、JFrameからドキュメントモーダルシートを表示しています。

先頭に戻る

CocoaComponent

CocoaComponentは拡張可能なクラスであり、ネイティブのCocoaビューをJavaコンテナに組み込むことができます。コンテナに追加して表示すると、CocoaComponentオブジェクトによって、基盤のCocoaビューがすべての描画およびイベント処理を実行できます。この技術は、たとえば、Web KitフレームワークのWebViewをJavaフレーム内部に置いたり、JavaアプリケーションをWebブラウザに迅速に切り替えるのに利用できます。

CocoaComponentの実装は2つの部分で構成されています。インスタンス化されて、AWT階層に追加されるJavaクラスと、CocoaComponentオブジェクトの実行時の動作を指定するObjective-CのNSViewサブクラスです。独自のCocoaComponentを作成するには、以下のことを行う必要があります。

  • com.apple.eawt.CocoaComponentクラスを拡張する。

  • createNSViewおよびcreateNSViewLongメソッドを実装する。システムアーキテクチャに応じて、CocoaComponentがコンポーネント階層に追加される前後に、これらのメソッドの一方が呼び出され、基盤のNSViewへのポインタを表すJava int(またはlong)がメソッドから返されます。64ビットのJava longへのポインタを返すcreateNSViewLongを実装することと、(32ビットをサポートする場合は)JavaのintにキャストしたcreateNSViewLongの結果を単純に返すcreateNSViewを実装することが推奨されます。

    リスト5:createNSViewおよびcreateNSViewLongメソッド

    // ネイティブ側でNSViewをインスタンス化し、longとして返す
    public native long createNSViewLong();
    
    // この使用は廃止されている。正しいcreateNSViewLong実装をキャストするだけ
    public int createNSView() {
      return (int)createNSViewLong();
    }
    
  • Objective-CでNSViewサブクラスを実装する。ここで、独自のCocoaComponentがどんなもので、何を実行するかを決めます。WebView、Address BookフレームワークのABPeoplePickerView、またはNSViewベースの他のオブジェクトが可能です。

  • createNSViewLongのネイティブの実装を用意する。ここでは、独自のNSViewをインスタンス化して、そのポインタをjlongとしてJavaに返します。ビューを保持する必要はありません。CocoaComponentオブジェクトがコンテナ階層に追加されると、AWTの実装によってビューが保持されます。

    リスト6:createNSViewLongのネイティブの実装

    JNIEXPORT jlong JNICALL Java_com_mycompany_MyCocoaComponent_createNSViewLong (JNIEnv *env, jobject caller) {
      MyCCView *newView = [[[MyCCView alloc] init] autorelease];
      return (jlong)newView;
    }
    

必要なことは、何かCocoaComponentを作成し、Javaコンテナに表示させることだけです。ただし、CocoaビューとそのJavaピア間で、双方向でやり取りする必要がある場合も考えられます。

CocoaComponentのメッセージング

CocoaComponentはJavaとCocoaのやり取りのために、sendMessageという簡易メソッドを提供しています。このメソッドによって、(独自に定義した)操作コードと単一パラメータオブジェクトを基盤のCocoaビューに渡し、独自のawtMessage:message:envメソッドで使用・解釈することができます。サンプルコードプロジェクト「QCCocoaComponent」では、sendMessageを使って、Quartz ComposerのコンポジションをJavaフレーム内部に読み込み、再生しています。リスト7に、サンプルプロジェクトのJavaQCViewクラスで定義したメソッドを呼び出すコントローラクラスを示します。

リスト7:CocoaComponent.sendMessageの使用例

public void actionPerformed(ActionEvent e) {
  if (e.getSource() == loadButton) {
    view.loadComposition ("Particle System.qtz");
  } else {
    view.startRendering();
  }
}

上記のloadCompositionおよびstartRenderingメソッドは、sendMessage APIを使って基盤のQuartz Composerビューとやり取りします。

リスト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);
}

最後に、QCViewサブクラスは、CocoaComponentオブジェクトからの呼び出しに応答するためにawtMessage:message:envを実装しています。

リスト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ピア(CocoaComponentオブジェクト)と直接対話する必要が生じることもあります。他のプラットフォームの場合と同様に、JNIのグロバーバル参照を使用することで、これが簡単にできるようになります。Javaオブジェクトのグローバル参照を作成・保持することで、必要に応じて呼び出すことができます。グローバル参照を作成する適切なタイミングは、createNSViewLongのネイティブの実装内で、当該関数がCocoaComponentを表すjobjectの参照を受け取るときです。リスト10に、サンプルコードプロジェクト「CWCocoaComponent」でこれをどのように行っているかを示します。

リスト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 JavaColorWellクラスが、標準のJNIメソッド検索処理を用いてそのグローバル参照を呼び出します。CからのJavaメソッド呼び出しの基本については、『The Java Native Interface: Programmer's Guide and Specification』のセクション4.2を参照してください。

JNIをよく理解している方であれば、上記の手法では問題が生じることに気付くでしょう。グローバル参照がガーベジコレクションに影響を及ぼすため、グローバル参照が存在するかぎり、CocoaComponentオブジェクトは回収されません。継承されたjava.awt.Component.removeNotifyメソッドによって、そのコンテナからのCocoaComponentの削除が通知されたときが、グローバル参照を削除する適切なタイミングであり、このタスクはsendMessageによって簡単になります。リスト11とリスト12に示すデモについては、サンプルコードプロジェクト「CWCocoaComponent」を参照してください。

リスト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やCocoaComponentなどのJNI手法を使用すると、後述するような、同期の問題が生じます。

注:このセクションでは、ほぼ例外なく「AWT」という用語を使っていますが、Swingアプリケーションにも当てはまります。「AppKit」という用語は、Cocoa Application KitフレームワークとサポートしているAPIを指します。

CocoaComponentのjavadocでは次のように述べられています。

  • AppKitスレッドに対してAWTイベントディスパッチスレッドをブロックしないでください。ブロックすると、おそらくデッドロックが生じます。

  • AWTイベントスレッドに対してAppKitスレッドをブロックしないでください。ブロックすると、おそらくデッドロックが生じます。この理由により、AppKitスレッドから、Javaによるアクセスが可能なロック(AWT TreeLockなど)をグラブすることは、安全でないと見なされています。AppKitスレッドからは、AWTまたはSwingメソッドを呼び出さないでください。

  • 使用するNSViewをAppKitスレッドのCocoaビュー階層に追加したら、実装のNSViewに対するすべての呼び出しをAppKitスレッドで行う必要があります。

CocoaComponentを扱う場合は、createNSView(またはcreateNSViewLong)から戻った直後にCocoaビューが階層に追加され、この呼び出しはJavaコンポーネントを追加するスレッドで行われると考える必要があります。たとえば、mainからコンポーネントを追加する場合に、AWTイベントスレッドで呼び出しが行われると想定しないでください

上記の最初の2つの項目は、AppKitおよびAWTのあらゆる統合(CocoaComponentなど)に当てはまります。一方のスレッドに対して他方のスレッドをブロックできないため、AppKitコードとAWTコード間でやり取りする場合は非同期モデルを採用するしかありません。どちらの環境にも、これを行うためのユーティリティメソッドがあります。

AWT/SwingからのAppKitの呼び出し

ビューの更新や他のイベントベースのアクションをもたらすAppKitを呼び出すときには、メインのAppKitスレッドでそれらの呼び出しを行う必要があります。NSObjectで定義されているperformSelectorOnMainThread:メソッドの1つに渡されるメソッドの呼び出しは分離してください。以下に示すサンプルコードプロジェクト「JSheets」からの抜粋の中で、その手法を説明します。

リスト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つあります。performSelectorOnMainThread:の呼び出しとwaitUntilDone:NOパラメータです。メインスレッドで実行するだけでは不十分で、非同期で実行する必要がありますwaitUntilDone:YESを渡すと、不適切なスレッドを用いるのと同じくらいのトラブルを引き起こすおそれがあります。サンプルコードプロジェクト「JSheets」には、performSelectorOnMainThread:を呼び出さないnativeShowSheet関数のもう1つの(無効な)実装例が掲載されています。もう1つの例がどうなるか、必ず確認してください。

純粋なCocoaアプリケーションでも、たとえば、ソケットから情報を受け取った後に周辺のスレッドがユーザインターフェイスを更新する必要があるときなどは、この手法が必要です。

ここで、前述のCocoaComponentの例でこの手法を明示的に使用していない理由が疑問になるかもしれません。なぜなら、メインスレッドで基盤のCocoaビューと非同期でメッセージのやり取りをするために、sendMessageメソッドが実装されているためです(これが、sendMessageの戻り型にvoidがある理由です)。次に、sendMessageには2つの利便性があります。つまり、多数のJNI関数が必要ないことと、メインのAppKitスレッドへの自動非同期メッセージングを提供することです。

先頭に戻る

AppKitからのAWT/Swingの呼び出し

逆方向(AppKitからのAWTの呼び出し)の場合にも同じ原則が当てはまります。ネイティブコードが、それが通知対象として登録されているCocoa通知を受け取るものとします。通知はメインのAppKitスレッドで行われるため、通知ハンドラからAWTを直接呼び出すと、アプリケーションで容易にデッドロックが生じるおそれがあります。解決策として、java.awt.EventQueueおよびjavax.swing.SwingUtilitiesクラス(SwingUtilitiesEventQueueのラッパークラスにすぎません)では標準のinvokeLaterメソッドを使用してください。ここでも同様に、サンプルコードプロジェクト「JSheets」を参照します。

リスト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のsaveFinish_mIDおよびcancel_mID変数は、ソースファイルのどこかほかの場所で定義されキャッシュされた(jmethodID型の)Javaメソッドへの参照です。詳細については、完全なJSheetsプロジェクトを参照してください。

先頭に戻る

イベントスレッドではないスレッドからのAppKitまたはAWTの呼び出し

周辺の「ワーカー」スレッドを扱うときは、前述のガイドラインが若干変わります。それでも、AppKitにメッセージを送るにはperformSelectorOnMainThread:変数の1つを使用する必要があり、AWTを呼び出すには適切なEventQueueまたはSwingUtilitiesメソッドを使用する必要があります。たとえば、SwingWorkerスレッドは、AppKitを呼び出すときには依然としてCocoaのスレッド処理規則に従う必要があります。1つだけ違うのは、これらの呼び出しはブロッキング方法で行えることです(CocoaではwaitUntilDone:YES、JavaではinvokeAndWaitを使用)。しかし、イベントスレッドではないスレッドから操作する場合でも、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デベロッパの方は(pthreads APIを使用して)新しいスレッドを作成し、VM呼び出しを実行する関数を渡す必要があります。これにより、アプリケーションのメインスレッド以外のスレッドにJVMが作成されます。以下に示すサンプルコードプロジェクト「simpleJavaLauncher」からの抜粋で、このプロセスについて説明します。

リスト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デベロッパはNSThread APIを使用して、アプリケーションのメインスレッドからJVM呼び出しを移動できます。リスト16に、このようなアプローチのごく簡単な例を示します。

リスト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では、mainCFRunLoopを開始しており、AWTが作成するNSApplicationがこれを使用します。リスト17はNSApplicationapplicationWillFinishLaunching:デリゲートメソッドに応答し、NSApplicationがすでにメインスレッドにあることを示しています。ネイティブランチャのメインスレッドからAWTアプリケーションを開始すると、パフォーマンスに重大な影響を及ぼし、回復不能なエラーを発生させるおそれがあります。リスト18に、最も一般的なエラーがスローされる書き換え例を示します。

リスト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バージョンを選択する必要があり、これはネイティブランチャにとってジレンマとなります。この問題を解決するには、JNI_CreateJavaVM環境を呼び出す前に、通常はsetenv関数を使ってJAVA_JVM_VERSION環境変数を定義します。JavaVMInitArgs構造体のversionメンバをJNI_VERSION_1_4に設定します。

Mac OS X上で呼び出しAPIを使用するときに、次のように覚えておくべきことがいくつかあります。

  • JAVA_JVM_VERSIONを設定する前に、JavaVMフレームワークを調べて希望のJavaバージョンの有無を確認する。JAVA_JVM_VERSIONをサポートされていないバージョンや存在しないバージョンに設定した場合のJNI_CreateJavaVMの動作は、定義されていません。

  • 特定のメジャーバージョン(1.4、1.5など)のJavaが必要な場合、そのバージョンが指定なしで起動しているように見えていても、必ず要求すること。特定のシステムでは特定バージョンのJavaが「デフォルト」であると想定してはなりません。

  • 1.3 JVMの呼び出しはサポートされていない。JNI_VERSION_1_2を渡すと、Java 1.3を明示的に要求することになります。このためJNI_VERSION_1_4を使用する必要があります。

サンプルコードプロジェクト「simpleJavaLauncher」では、前の2つの例で使用できるstartupJava関数が宣言されています。この関数には、J2SE 5.0を検出して明示的に要求するロジックが含まれています。

リスト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初版

掲載日: 2006-04-17




Did this document help you?
Yes: Tell us what works for you.

It’s good, but: Report typos, inaccuracies, and so forth.

It wasn’t helpful: Tell us what would have helped.