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

2012年11月17日土曜日

JNI 呼び出しで気をつけること

 とてもくだらないことで何時間も費やしてしまった…。いろいろ調べてるうちに誤情報とかもあって混乱したので、JNI 呼び出しの気をつけることをまとめてます。

●Java 側の定義
 定義は以下のような形で行います。

package com.sample;
class MyClass {
   public native void  my_method( int arg );
}

 C 側の関数の実体を定義していなくてもコンパイルは通ります。これは実行時に、現在読み込んでいる実行モジュール (.so) から関数を探すようになっているためで、コンパイルが通ったからといって呼び出しが成功するとは限りません。

●C 側の関数名は自分で書かずに javah を使用する
 Java から呼び出す関数名の定義にはいくつか約束事があり、それを全部理解しているならいいのですが、いろいろわかりにくいため javah でプロトタイプ宣言を自動作成するのが一番確実です。
 例えば、'_'(アンダーバー) は JNI ではパッケージ名の区切り記号として使用するため、関数名にアンダーバーが含まれる場合には特殊な置き換えが必要になります。
 また、クラスにメンバ変数があるかどうかによって、関数の第2引数の型が変化するようです。(jobject だったり jclass だったり)
 これらの細かい仕様は javah でヘッダを出力するようにすればすべて自動でやってくれます。それから、初心者の方がよくはまりがちな extern "C" もちゃんと一緒に出力してくれます。

 javah -classpath <パス名> <クラス名>

 <パス名>はコンパイル済みの Java のクラスのあるパスを指定します。eclipse でコンパイルしているなら「プロジェクトのパス/bin/classes」になります。
 <クラス名>はパッケージ名も含めたフルクラス名です。上の例なら com.sample.MyClass になります。

●Java 側で JNI モジュールを読み込む
 JNI メソッドを呼び出すより前に、必ず JNI モジュールを読み込んでいる必要があります。ほとんどの場合は、起動と同時に読み込むことになると思うので、起動時に実行するクラスで、

class Main
{
    static {
        System.loadLibrary( "MyModule" );
    }
}

と書くことになります。
 注意点としては、読み込むモジュールのファイル名は必ず "lib???.so" というファイル名である必要があり、loadLibrary で指定する名前は、このファイル名から "lib" と ".so" を除いた部分であるという点です。上の例なら、"libMyModule.so" というファイルが読み込まれます。

2011年9月30日金曜日

Java って…

愚痴です。

最近仕事で Android の開発をしているのですが、Java の扱いづらさに悪戦苦闘しております。
もともと Java の利点は、プラットフォームに関係なく JavaVM 上で共通のプログラムが動く、というものだったと思います。最初は 100% Pure Java なんて言い方をしてましたね、確か。それが現在は、jni などという仕組みを導入してネイティブコードの呼び出しを許してしまうことで、この利点は失われてしまいました。さらに、携帯や Android は独自のライブラリを持っており、各機種で共通に動作するようなプログラムを作ることもできません。
さらには Java という言語の仕様も C++ プログラマから見るとあまりにも不完全で、ガベッジコレクションのような機能も、もはや不便なものでしかありません (後述)。
Java ってもう、いろいろ駄目なんじゃね?という愚痴です。

●ガベッジコレクションという足枷
ガベッジコレクションは、不要になった (どこからも参照されていない) メモリを自動的に解放してくれるものですが、これが C++ に慣れたプログラマにとっては非常に不便な機能です。なぜならこれは、いつ解放されるかプログラマが知ることはできないからです。
C++ プログラマは普通、初期化処理をコンストラクタに書き、終了処理をデストラクタに書きます。そして、そのクラスをスコープの中で使用すると、処理がスコープの外に出る際に必ずデストラクタが呼ばれることがわかっています。ですから、メモリの解放以外の終了処理、例えばリソースの解放処理やファイルのクローズをデストラクタに書き、これらの終了処理のし忘れを確実に回避することができます。
しかし Java や .NET のようなガベッジコレクションのシステムではメモリがいつ解放されるかわからない=デストラクタの呼ばれるタイミングがわからないため、このような終了処理をデストラクタに書くことができません。つまり、ファイルのクローズなどをプログラマが毎回手動で行うしかないわけです。これにより、終了処理のし忘れというバグをいつまでも作り続ける恐れがあります。
メモリの自動開放についても、C++ ではスマートポインタの導入により、昔のようなメモリリーク問題はほぼなくなってきています。しかも、スマートポインタならメモリ解放のタイミングもプログラマが予測可能なので、メモリ残量のコントロールも可能です。
ガベッジコレクションはこのように、予測不能であるために、プログラマにとっては足枷でしかないのです。

●処理が異常に重い
どこかの段階から Java では JIT という機能が追加され、内部で Java マシンコードを各プラットフォームにネイティブなコードに置き換え高速化するという機能が実装されたそうです。しかし、JIT で最適化する際の効率化のため、Java コンパイラ自体はほとんど最適化を行わないのだとか。そのおかげで JIT を搭載していない携帯機等では、とてつもなく動作速度が遅くなります (Android は 2.2  以降で実装されているそうですが)。これをなんとかするために、携帯機のプログラマは涙ぐましいまでの苦労を強いられます。
特にひどいのが、オブジェクト指向なプログラムをすると呼び出しコストが高くなるので、クラスの数をなるべく少なくする必要があるというものです。僕が見たものの中には、クラスが1つしかなく、その中に大量のメンバ変数と、メソッドが並んでいるというようなものもありました。昔懐かしい構造化プログラミングとかが必要になるレベルです。
JIT を搭載していないシステムでは、まともなものは作れなくなっているように思います。

●プリプロセッサがない
移植性を高くするため、機種依存のコードは、コンパイル時に選択するようにしたいものです。C 言語では昔からプリプロセッサを利用して、このようなコードの切り分けを行ってきました。
しかし、Java にはこのようなものがありません。多くの Java プログラマは C 言語用のプリプロセッサを利用しているようです。
このような状況であるにもかかわらず Java がプリプロセッサを正式に採用しないのはなぜなんでしょう?一部には Java のコンセプトに合わないとかなんとか。しかし、これがないと、機種毎に違うコードを用意するためにソースを別々に書き、別々にメンテナンスする必要が発生します。これはプログラマにとって大きな負荷となり、更新し忘れなどが頻繁に発生することになるでしょう。
バグを増やす可能性を増やしてまでこだわるコンセプトってなんですか?と問いたい。

●開発環境
Java での統合開発環境は eclipse が一般的のようです。eclipse は Java で書かれているそうで、このため処理が異常に重いです。また、メモリも大量に必要とし、メモリが不足すると落ちます。あまりに不便なため、コマンドラインだけで開発している人も多いのではないでしょうか?しかし、デバッガは GUI が欲しいところ…。
こだわりなのかなんなのか知りませんが、なんでもかんでも Java である必要はないのではないでしょうか。


…とかいろいろ書きましたが現状どうしようもなく、仕事はこれでやらなきゃならないわけで。
まあ、Anidroid 自体の開発はおもしろそうなので、個人的に自分専用の Andoird 開発用 IDE でも作ってみるかな。もちろん C++ で!