ラベル JNI の投稿を表示しています。 すべての投稿を表示
ラベル JNI の投稿を表示しています。 すべての投稿を表示

2008年4月27日日曜日

GCCを使ったJNIなどのDLLでSSE命令を使うときの問題点

JavaからJNIでDLLを呼び出せるので、JavaVMで処理に時間がかかりそうな処理をC/C++で開発してみようと思うのは普通だと思うし、どうせならMMXやSSEなどで高速に演算させたいと思うこともある。
GCCには、MMX、SSEなどの命令を呼び出すためのAPI(__builtin_ia32_hogehoge())があるので、これらを使えば、わざわざアセンブラのお世話にならずとも実現することが可能である。ということで、JNIなDLLをSSE命令を使って開発するのであるが、これが一筋縄に行かないようなのである。

SSEには、メモリー上のデーターをアクセスするための命令に128bitアライメントされているデーター用とそうでないもの用の2つがある(MOVAPSとMOVUPSなど)。要するに、128bitアライメントされているほうが高速にアクセスできるということであるが、もし、アライメントがされていないデーターにアクセスすれば、いわゆるソフトウェア割り込みが発生してアプリケーションエラーとなってしまう。
これらのアライメントを前提とする命令を使うには、コード上データーがアライメント上にあることが保証されているなければならないということになる。

普通、C/C++でプログラムするときには、データーがメモリ上どう配置されるかということを気にすることなく、もし、なんだかの制約があるとすれば、コンパイラーが何とかしてくれる(それ用のオプションをつけて実行するとか)ことにしている。
ただし、デフォルトでない特殊なアライメントなどを利用したい場合には、GCCでは__attribute__((aligned(n)))のような属性をつけられる。これは例えば、char data[256] __attribute__((aligned(16))) = {0};のように使うのだが、この例では、このchar配列を128bit(16byte)上に配置するようにしていている。

ただし、このように書けるのはC/C++上での静的な変数に対してであり、動的に得る場合(mallocやnewで生成する領域)では、このような属性では制御できない。ただ、C/C++の場合、ポインターはメモリーアドレスそのものであると考えられるので、ポインターをずらして、合わせてしまうという事が可能である(本質的にはAPI側でなんとかしてほしいところだが)。

このようにすれば、SSEでのアライメントの問題が解決するということにしたいのだが、実際にはそういう訳にはいかない。というのが、本題であったりする。
ここで根が深いのは、基本的にはコンパイラーのオプションや属性の指定で解決されているべきものが、実際にはアライメントされていないということである。
アセンブラ以外でアライメントなどを考慮しなくてはいけないのは、C/C++であるとしてでもおかしな話であると思うが、CPU固有の命令を使おうとしているので、なんだかの特殊な処理が必要であると考えても仕方がないかもしれない。確実に動くコードが書ければ、それはそれで問題ないといえるだろう。

ところで、C/C++で変数といわれるものは2つあり、それはいわゆるstaticで宣言されるものと、関数やブロックのなかで宣言されるいわゆるauto変数といわれるものである。
staticな変数は、モジュールがロードされるときに確保され、その領域はモジュールがなくなるまで存在する。この変数は、その領域のアドレスをどこかに保持し、領域内の相対アドレスをコード内に持って、それを足してアドレスを確定してアクセスする。
ここでアライメントを保持するには、領域の先頭のアドレスをアライメントされるように配置して、相対アドレスもアライメントを保持するように割り当てるようにする。
コンパイラーでオプションや属性で指定するアライメントは、相対アドレスにを指定するものであり、領域のアドレスはモジュールのロードにアライメントされて確保されるので、問題なく使える。

auto変数は、スタックといわれる領域に確保される。これは、関数が呼び出されるときのスタックのアドレス(スタックポインター)を基に相対的に割り当てられる。
autoな変数のアライメントは、staticと同様に相対アドレスは、コンパイラーが割り当てて、スタックは、モジュールのロード後、最初の関数を呼び出すときにアライメントされるように割り当てられるようになっている。その後、呼び出される関数もアライメントするように配置することによって、アライメントを維持するようにしている。
そうすると、staticな変数と同様にauto変数も問題なく動くはずなのだが…。

最初に述べたのだが、DLLでSSEを使うときの問題があるということなのだが、それは、DLLのスタックは、呼ばれる元のモジュールのスタックを引き継いで利用する。これは、C/C++で関数を呼び出すために必要な仕組みである。

もし、C/C++コード上でアライメントが不明であれば、アライメントを前提としない命令を使えばよいのだが、コンパイラーはauto変数に割り当てられたSSE用の変数を、アライメントされているものとして、読み書きするような命令を出力するのである。
ということで、おわかりであると思うが、DLLを呼び出すモジュールがアライメントを維持しないようにスタックを使っている場合、DLLでアライメントを保持するようになっていても、うまく動かないということになるわけである。それは、JNIがJavaVMから呼び出されるときも起きていて、JNIのDLLでSSE命令を使おうとすると、ほとんどうまくいかない(うまくいくのは、SSE用の変数がすべてレジスタ上に配置されたときか、運良くスタックがアライメントされているときだけ)。

どうやら、このことを問題だと思っている人が多く居るらしく、GCCでこれを回避するオプションを作ることが想定されているようだが、まだ実装されていないもよう。ましては、MinGWでの移植にも時間がかかるため、おそらくMinGWのGCC環境では、うまく動くコードをはくコンパイラーはしばらく先になるだろう。

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を使っている。

2007年4月18日水曜日

EclipseでJNIをデバッグする

前回、EclipseでJNIの開発について書いたが、その後デバッグをするにはどうするのかというのをゴリゴリとやっていたが、例のごとくはまってしまってので、忘れないように書いておくことにする。 まずは、JNIモジュールのデバッグは次のような基本手順を踏むということする。

  1. JNIのソースにブレイクポイントを設定
  2. 呼び出すJavaソースにブレイクポイントを設定
  3. Javaをデバッグ実行
  4. JNIモジュールをデバッガでアタッチ

まずはJavaのブレイクポイントは、System.LoadLibrary();を呼び出した後でかつJNIのブレイクポイントが呼び出される前に設定する。Javaのデバッグ実行でこの設定したブレイクポイントで止まった状態で、JNIモジュールをアタッチする。

JNIモジュールのデバッグ設定は、Eclipseの「構成およびデバッグ」のダイアログで、「ローカルアプリケーションへの接続」という構成で設定できる。このとき、アタッチの対象となるモジュール(ここではWin32なのでDLL)を「メイン」のC/C++アプリケーションというところに書くのだが、ここにはビルドしたモジュールを設定する(デフォルトだと「デバッグ/<DLLのファイル名>」)。

あと、「デバッガー」の項目のところは、「gdb/mi」を選択して、gdbのコマンドのところは、実行できる適切なパスが設定されていればよい。あと、このときに「詳細コンソールモード」というチェックをONにしておくと、デバッガーが何をしているのかわかるので最初のうちはONにしておいたほうがよいだろう。

この設定で指定したモジュール(DLL)がJavaから呼び出されないとアタッチできないので、Javaのデバッグ構成の管理で、引数のところでプログラムの引数かVMの引数のところで、「-Djava.library.path=<デバッグするモジュールのパス>」を設定をする。

これでEclipseからの操作でデバッグができる。DLLのデバッグで、gdbのアタッチ後に中断状態になるが、そのまま再開すればモジュールは実行される。

あとは、MinGWの固有だと思われるが、gdbのバージョンが古いとEclipseから投げるMIのコマンドの不一致か、ブレイクポイントがうまく設定されないようだった。そのため、MinGWからgdb6.xをダウンロードしてインストールすればよいようだ。

もうひとつ、これはうちの固有の問題かもしれないが、gdbでデバッグしようとすると「LdrAccessResource」のところでセグメンテイションエラーが発生して、gdbが落ちてしまう。これについては、いろいろ調べているものの解決方法がわからず、現状デバッグ不能である。どうやら、ntdllとgdbの相性の問題のようで詳細はわかりませんでした。

2007年4月12日木曜日

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

プログラミングTIPSではないのですが、EclipseでJNIのモジュールを開発を試みたときにプロジェクトをどう設定すればよいかという話です。要するにちょっと設定にはまったので、忘れないようにメモっておこうということです。ただし、以下の方法はベストな方法とは思ってません。とりあえず、こうやったらできましたっていう程度のものだと思ってください。ちなみにですが、Win32でEclipseとMinGWを使って開発するというお話しです(他のプラットホームでは違うところがあるかもしれません)。

蛇足かもしれませんが、JNIの開発そのものは、JDKのドキュメント(Java5のJDKのリンクですが)に載っていて、MinGWでJNIのモジュールをコンパイルするにはどうするかというのは、MinGWのFAQに書いてあります。

まずは、JNIのモジュールを開発するには、nativeの宣言を含むJavaのソースをまず書いてコンパイルする。classファイルからjavahでC(++)のヘッダーを出力して、C(++)のコードを書いてコンパイルする。出来上がったモジュールをJavaから実行させる、という手順であると思っていることが前提となります。

何が話題かというと、JavaのモジュールはJavaのプロジェクトで開発して、JNIのモジュールはCDTを使って、C(++)のプロジェクトで開発するのですが、javahで作られるヘッダーをどうやって、C(++)のプロジェクトへもってくるか、できたモジュールをどこにコピーして、実行するかということです。

C(++)のプロジェクトのほうがビルドの設定の自由度が高いと踏んで、C(++)のプロジェクト側でがんばるということにします。依存関係としては、C(++)のプロジェクトがJavaのプロジェクトを参照しているというふうにします(お互いに参照していてもよいとは思います)。

C(++)のプロジェクトの設定で「C/C++ビルド」という項目の中に、ビルドステップというのあって、ここにビルドの前後に何かコマンドが実行できるので、これを利用することにします。 まずは、javahをビルド前に実行するためにビルド前のステップのところに以下のように入力します。

<JDKのフルパス>\bin\javah -jni -d .. -classpath ../../<Javaのプロジェクト名> <パッケージ名>.<クラス名>

※もし、「\」がバックスラッシュに見えたら、「¥」の半角だと思ってください このとき、 「<パッケージ名>.<クラス名>」は、複数記述することができます。

これで、JavaのプロジェクトからJNIのモジュールを読み込んで実行できるわけです(もちろん、何かバグってたらだめですが…)。デバッグや実行もEclipseからやりたいので、「構成および実行」の設定をして、Javaを実行させればよいのですが、JNIのモジュールがjava.library.pathにないと実行できません。 そのときにどうするかというと、JNIモジュールを読み込むjavahを実行したJavaプロジェクトとは別に、これらを使うJavaプロジェクトを作成します。 この新しいほうのプロジェクトのビルドの構成の「プロジェクト(P)」でJNIのJavaプロジェクトを参照するように設定すれば、実行できます。この設定に「ネイティブ・ライブラリーのロケーション」というサブツリーがあると思いますが、ここでJNIのネイティブモジュール(Win32だとDLL)のパスを設定すればよいです。

リリースしたときにjava.library.pathの設定をどうするのかというところは気になるところですが、VMの実行時にパラメタで渡さなくても、いわゆるpath変数で見つけられるところにモジュールがあれば実行できるようです。