2008年4月1日火曜日

EclipseとMinGWでJNIモジュールを開発する

ずいぶん前の記事で、JNIモジュールをEclipseでビルドやデバッグする方法を書いたのだが、久しぶりにJNIモジュールを作ってみようとしたら、同じようにはまってしまったw。今回は、はまったついでに、手順についてまとめることにする

前提としては、WindowsでEclipseを使って、JNIモジュールを作るということで、コンパイラはMinGWのG++を使う。ネイティブ側はC++で書くことにする(Cでもそれほど違いはないはずだが)。
あとは、JDKドキュメントにあるJava Native Interface 仕様とMinGWのFAQぐらいは読んでいるという前提になる。EclipseはJavaとC/C++の開発環境(CDT)の両方がインストールされているものとする。

プロジェクトの作成

まずは、Javaのプロジェクトを作成する。Javaの方はJDKのドキュメントにあるとおり、nativeなメソッドを持つクラスを作成し、static{}の中でSysytem.LoadLibrary();を記述する。LoadLibraryの引数は、パッケージがある場合、"package_class"のように"."(ドット)を"_"(アンダースコア)に置き換えた文字列にする。
例)myproject.testというパッケージでクラスがMyclassの場合、"myproject_test_Myclass"となる

C++のプロジェクトを作成するのだが、そのときにプロジェクトタイプは、"Shared Library"を指定する。蛇足かもしれないが、C++でJNIのAPIを呼び出すときには、JNIEnvのオブジェクトのメンバー関数を呼ぶ形になる。例えば、JNIのAPIがvoid hoge(JNIEnv* env, jstring str);という風にドキュメントに書いてあったら、C++では、env->hoge(str);という風に呼び出す。
次にビルドの設定で、出力ファイルはLoadLibrary();で設定した文字列に拡張子".dll"が付くファイルが出力されるように指定する。makeコマンドの設定のところで、MinGWであれば、デフォルトを外して"mingw32-make -k"とする。
ツールの設定(g++などの設定)のところを設定する、C++のコンパイルの設定で、プリプロセッサ(-D)のところに"_JNI_IMPLEMENTATION_"というdefineを追加する。あと、インクルードパス(-I)にJDKにあるincludeとinclude/win32の二つを設定する。もし、JDK_HOMEとかの環境変数が設定されていれば"${JDK_HOME}\include"と"${JDK_HOME}\include\win32"となる。この手の環境変数を設定したいならば、環境変数のところで設定できる。
次にリンカの設定のところで、"-Wl,--kill-at"というオプションを設定するのであるが、適当なところがなさそうなので、コマンドラインのところで"g++"のあとにスペースを空けて設定する。リンクするライブラリ(-l)のところには、"jvm"を設定し、ライブラリの検索パスのところには、JDKにあるlibを設定する。JDK_HOMEを設定していれば"${JDK_HOME}\lib"となる。

javahを実行する設定

Javaのソースでnativeを含むクラスファイルをjavahコマンドに渡すと、JNIようのCのヘッダファイルを出力するのだが、javahをどうやって起動するのか?ということです。一度javahを実行すれば、ファイルが出来上がるので、DOS窓でコマンドを打ってもいいわけですが、きっと何度かコマンドを実行するハメになるので、どこかに書いておいて、簡単に実行できるようにしておきたい、と考えてみた。
その方法はいくつかあるが、とりあえず思いついたのは

  • Javaのプロジェクトにantを書いて、手で実行する
  • Cのビルド設定のステップビルドのところで、ビルド前のコマンドのところでjavahを書いて、ビルド時に実行する
  • ビルド前コマンドのころで、javahを起動するmakefileを書いて、ビルド時にmakeを実行する

といったところだが、どれか楽そうなのを選べばよいと思う。ちなみにantは、antを実行するjavaコマンドがJDKのものでないと、javahタスクは実行できないもよう。あと、ビルド前コマンドは、ビルドを実行するときに毎回起動されるので、場合によってはうざいかもしれない。少なくとも自動ビルド向きではない。
makefileを書いて、DOS窓から手動実行するぐらいが無難なのかもしれない…。

Javaの実行設定

nativeを含むクラスを読み込むと、LoadLibrary();でDLLをロードする仕組みなので、EclipseでJavaの実行を設定して、実行(Run)すればよい。ここでの問題は、DLLをロードしようとしたときにjava.library.pathというシステムプロパティに登録してあるパスの中から、DLLを探し出して、ロードするという仕組みになっているので、それらのパスに含まれてないと、実行時に失敗するということである。これを解決する方法もいくつか考えられる。

  • java.library.pathにあるパスにDLLをコピーする
  • 実行時にjava.library.pathをDLLが存在するパスに設定する(起動時オプションのVMの設定で"-D java.library.path=…"と書く)
  • 実行時にPATH変数(SolarisとかはLD_LIBRARY_PATH)にDLLが存在するパスを追加する

とりあえず、ここではデバッグをすることを目標とすることにして、実行時にビルドされたデバッグようのDLLのパスをPATH変数を追加するという方法をとることにする。
具体的には、実行時の環境変数の設定で、"PATH"と変数を追加して、値を「${workspace_loc:<Cのプロジェクト名>/Debug}」とする。このとき、環境変数は追加するの方にしておいたほうがよい。

この段階で、Javaプロジェクトのほうが実行できなければ、デバッグもできないので、普通に実行できるようにしておく。

JNIモジュールのデバッグ

実行(Run)で動いていることが前提として、デバッグをするのだが、あとはC側のデバッグの設定をする。
アタッチをするデバッグを定義して、アプリケーションのところには、Debugフォルダに出力されたDLLを設定する(普通は"Debug/<LoadLibrary();に設定した文字列>.DLL")。これで、設定は完了。
デバッグの手順は以下の通り。

  • デバッグしたいCのソースにブレイクポイントを貼る
  • JavaのLoadLibrary()が実行された後でかつ、デバッグしたいCの関数が呼び出される前のどこかにブレイクポイントを貼る
  • Javaをデバッグ実行する
  • Javaのブレイクポイントで止まったら、Cをデバッグ実行する(アタッチをする)。アタッチコマンドは"javaw"であるが、Eclipseも"javaw"で動いているので、間違えないこと。タスクマネージャをみて、EclipseのPIDを確認しておいたほうがよいかも。
  • Cの実行が止まるので、Cのモジュール(gdb/mi)を選んで、再開させて実行中(Running)の状態にする
  • Javaのモジュールを選んで、ブレイクポイントで止まっているJavaを再開させる
  • Cのほうがブレイクポイントで止まる

と、いった具合でちょっと面倒だがこういった感じになる。
JavaのブレイクポイントのところでLoadLibrary()のあと…といっているところは、LoadLibrary();のところにブレイクポイントを貼って、LoadLibrary()をステップ実行させるという方法が無難かもしれない(手順は増えてさらに面倒になるけども)。
あと、アタッチのあとで、コンソールにエラーのような文字列が現れるが、そのまま、実行を再開させても問題ないもよう。場合によっては、問題が生じるかもしれないが。
アタッチ直後にCのデバッグが停止したり、エラーを出力するのは、g++、gdbのバージョンなどで変わる可能性がある。

ということで、なんとかEclipseとMinGWでJNIのモジュールが開発できるようになった。上記にも述べたが、MinGWのバージョン(g++やgdbなど)によって、状況は変わる可能性がある。ここではMinGWは5.1.3を使っている。

0 件のコメント: