Java向けにC++クラスをラップする方法
JNI (Java Native Interface)ネタは、この記事で一応最後です。
前々回は、Javaから渡された配列や文字列を簡単に受け取る方法について述べました。配列の受け渡しに関しては、最悪だと呼び出し毎にネイティブヒープへのコピーが発生する可能性があるので、ネイティブ側とVM側のどちらにデータを配置するのが正しいのかを考慮して設計する必要があります。
前回は、C++をJNI経由で呼び出す際の例外処理について述べました。STLに例外が必要となるわけですし、Android等でも基本的には-fexceptionsを有効にしてビルドした方がいいと思います。
さて、ここまでの知識だと、JavaからC言語のインターフェイス経由で関数を呼び出すということは出来ますが、C++のクラスをJavaから管理するとなるともう少し工夫が必要です。今回は、オブジェクト指向なやり方でC++のクラスを利用するにはどうすればいいのかについて述べていきます。
ポインタなんて所詮整数だ
Javaには、CやC++のポインタを認識する仕組みはありません。*1Java仮想マシン上の"綺麗な世界"には、ポインタなどという穢らわしきモノは存在しないわけです。
しかし、結局のところ、ポインタ≒アドレスなんて所詮整数なので、Javaのクラスに整数メンバを用意しておけばアドレスを保存しておくことができます。そいつをJNI経由でC++側から読み出せば、C++側でインスタンスを特定できるという話です。今回の記事の根幹はここだけです。
この辺りの話題は、以下の記事を参考にしています。
http://thebreakfastpost.com/2012/01/21/wrapping-a-c-library-with-jni-introduction/
例として、以下の様なテストクラスをC++によって実装してみることにします。
public class WrapperTest { private long mHandle = 0; // <=== こっそりアドレスを保存する public WrapperTest() { init(); } private native void init(); public native void put(int x); public native void show(); public native void destroy(); }
ほとんどネイティブ関数となっていますが、mHandleなる整数メンバを入れていることに注目です。*2こいつをC++から読み書きすることで、Javaオブジェクトにぶら下がったC++インスタンスのアドレスを特定します。
ちなみに、Android標準ライブラリの多くが裏でネイティブを呼び出す形になっているのですが、使われているテクニックはやはり同じのようです。
ラッパークラス用テンプレート
さて、ここからはC++での実装に入っていきます。理屈は極めて単純なものの、毎回新しいラッパークラスを作る度に整数<->ポインタ間キャストについて考えるのは面倒でしょう。というわけで、ラッパークラスを作るための補助テンプレートについて紹介します。参考記事よりはもう少し賢い実装になっていると思います。
ほぼ同様のコードはGitHubにも上げてあります: https://github.com/meryngii/jni_util
template <typename T> class Wrapper { public: explicit Wrapper(JNIEnv* env, jobject objj) : env_(env), objj_(objj) { } Wrapper(const Wrapper&) = default; Wrapper& operator = (const Wrapper&) = default; T& get() const { T* ptr = getPointer(); if (!ptr) { throw std::runtime_error("Uninitialized native object was used!"); } return *ptr; } template <typename... Args> void create(Args&&... args) const { destroy(); // delete if an object exists setPointer(new T(std::forward(args)...)); } void destroy() const { delete getPointer(); setPointer(nullptr); } // Pointer Operations T* getPointer() const { return reinterpret_cast<T*>(env_->GetLongField(objj_, getFieldID(objj_))); } void setPointer(T* ptr) const { env_->SetLongField(objj_, getFieldID(objj_), reinterpret_cast<jlong>(ptr)); } private: jfieldID getFieldID(jobject objj) const { if (!classj_) { classj_ = env_->GetObjectClass(objj_); checkException(env_); } if (!fieldID_) { fieldID_ = env_->GetFieldID(classj_, "mHandle", "J"); checkException(env_); } return fieldID_; } private: JNIEnv* env_; jobject objj_; static jclass classj_; static jfieldID fieldID_; };
ポイントとなる関数はgetFieldID, set/getPointerの3つで、後はオマケです。Javaのメンバ変数を読み書きするにはjfieldIDが必要となるので、ネイティブ側でstatic変数にキャッシュしておきます。
jclassやjfieldIDといった値は、実行時に一度手に入れば(確か)プロセス終了時まで有効なので、必ずキャッシュしましょう。ちなみに、jclassやjobjectはクラスやオブジェクトが同じでも異なるポインタ値となる可能性があるので、env->IsSameObjectといった関数がわざわざ定義されています。jfieldIDやjmethodIDは一意性が保たれているのでキーとして使うことも可能ですが、個人的にあまり危なそうなことはオススメしません。
上のWrapperテンプレートを使えば、後はラッパークラスを作るのは簡単です。C++で普通にクラスを作って、それにJNIのインターフェイスを加えます。
class WrapperTest { public: void put(int x) { data_.push_back(x); } void show() { for (int x : data_) std::cout << x << " "; std::cout << std::endl; } private: std::vector<int> data_; }; extern "C" { JNIEXPORT void JNICALL Java_WrapperTest_init (JNIEnv* env, jobject thisj) { convertException(env, [=] () { Wrapper<WrapperTest>(env, thisj).create(); }); } JNIEXPORT void JNICALL Java_WrapperTest_put (JNIEnv* env, jobject thisj, jint x) { convertException(env, [=] () { auto& self = Wrapper<WrapperTest>(env, thisj).get(); self.put(x); }); } // ... }
ネイティブオブジェクトの破棄
基本的な話としては以上ですが、まだオブジェクトの破棄について言及していませんでした。
JavaのオブジェクトはGCによって不定期に開放されます。finalizeメソッドをオーバーライドすることによって、GCが走った時にリソースの最終処理を行うことが可能になっているので、普通に考えればC++のオブジェクトもその際に開放するのが自然な発想でしょう。ところが、finalizeメソッドはいつ呼び出されるか予測が出来ず、しかもアプリケーションの終了タイミングによっては呼び出されない可能性があります。
C++プログラマは「デストラクタは必ず呼び出される」という考えでクラスを設計するので、finalizeにネイティブリソースの破棄を一任するのは良い考えとはいえないでしょう。結局のところ、少々面倒ではありますが素直に破棄用メソッドを作って、Java側のクラス利用者がfinallyなりで責任を持って呼び出すという手法が一番マシと考えられます。上のクラスにはdestroyメソッドを定義してあります。
extern "C" JNIEXPORT void JNICALL Java_WrapperTest_destroy (JNIEnv* env, jobject thisj, jstring strj) { convertException(env, [=] () { Wrapper<WrapperTest>(env, thisj).destroy(); }); }
finalizeは当てになりませんが、一応保険として最後に破棄するようにしておくことは悪くないと思います。そのためにもdestroyは何度呼び出してもOKな作りにしておく必要があるでしょう。
@Override protected void finalize() throws Throwable { destroy(); super.finalize(); }
おわりに
最後に、JNIを使う際の注意点について簡単にまとめておきます。
- Java Native Interface を使用する上でのベスト・プラクティスをじっくり読む。
- (上にも書いてあるが)JNIの返り値は全力でグローバルにキャッシュする。
- C++の例外が漏れ出さないように気を付ける。逆に、Java側の例外が発生したまま継続しない。
- finalizeが呼び出されない可能性に気を付ける。
- UnsatistiedLinkErrorで泣かない。
- extern "C"を付けたか?
- メソッド名・クラス名は正しいか?
- javahを試したか?
- System.loadLibraryを忘れていないか?
- ぶっちゃけJNIは生産性が低いので、出来ることなら使わないほうがいい。(今日のオチ)
お粗末さまでした。
JNIとC++例外
JNI関連の話が続きます。
今日は短いですが、JNIとC++例外について。
最近ではAndroid NDKもC++の例外(やRTTI)に対応していて、そのおかげで例外が必要なSTLなどのライブラリを自由に使えるようになっています。しかし、Java側はC++の例外を理解してくれないので、もしJNIを介した関数がネイティブ領域で例外をキャッチし損ねると、すぐにSegmentation Faultで落ちます。
解決策としては、JNIから呼び出されたC++側の内部処理を、noexceptな関数に閉じ込めてしまえばよいわけです。もちろんエラーが発見できないのもまずいので、JNIを使ってJava側の例外に変換してしまいます。
JNIEXPORT void JNICALL Java_Foo_bar (JNIEnv* env, jobject thisj) { jni_util::convertException(env, [=] () { // do something with env and thisj // ... throw std::runtime_error("Hoge failed!"); // C++ exception thrown! // ... }); // converts C++ exceptions to Java exceptions }
地味にC++11のラムダキャプチャの恩恵を得られる例でもあります。関数1つを見るとわずかな差ですが、クラスが増えてくると管理も大変になってくるので、記述量を減らす努力は重要です。
実装は以下のように非常に単純。例によってGitHubにも同じものがあります。
https://github.com/meryngii/jni_util
inline void throwRuntimeException(JNIEnv* env, const char* what) { jclass classj = env->FindClass("Ljava/lang/RuntimeException:"); if (classj == nullptr) return; env->ThrowNew(classj, what); env->DeleteLocalRef(classj); } template <typename Func> inline void convertException(JNIEnv* env, Func func) noexcept { try { func(); } catch (std::exception& e) { throwRuntimeException(env, e.what()); } catch (...) { throwRuntimeException(env, "Unindentified exception thrown (not derived from std::exception)"); } }
ここで、Java側の例外が発生しても、C++側はすぐに終了しないという重要な問題があります。JNIの関数を呼び出したときには、常に例外が発生しているかどうかを検査しなければいけません。こんな感じで。
class JavaException : public std::exception { }; // 例外が発生しうるJNIコールの後で呼ぶ inline void checkException(JNIEnv* env) { if (env->ExceptionOccurred()) throw JavaException(); }
上と組み合わせると、C++側で例外を利用したコードを書きつつも、Java側の事情にも配慮することができるというわけです。
今回は次回の布石で、次回はJavaから利用できるようC++のクラスをラップする方法について書きます。
参照カウントとバッファ再利用
std::shared_ptrの参照カウントを、バッファメモリの再利用に応用できるのではないかというお話。例として、単純なリソースクラスとそれのローダを考えてみます。
#include <vector> #include <memory> #include <numeric> class Resource { public: std::size_t size() const { return data_.size(); } void resize(std::size_t size) { data_.resize(size); } std::vector<int>::iterator begin() { return data_.begin(); } std::vector<int>::iterator end() { return data_.end(); } private: std::vector<int> data_; }; class Loader { public: std::shared_ptr<Resource> load(); private: std::shared_ptr<Resource> resource_; }; std::shared_ptr<Resource> Loader::load() { if (!resource_.unique()) // <=== resource_ = std::make_shared<Resource>(); resource_->resize(100); std::iota(resource_->begin(), resource_->end(), 0); // prepares data return resource_; } void f() { Loader loader; // load and release for (int i = 0; i < 100; i++) { auto resource = loader.load(); // "loader" reuses "resource_" // because "resource" is released here. } } void g() { Loader loader; // load and store std::vector< std::shared_ptr<Resource> > gotResources; for (int i = 0; i < 100; i++) { auto resource = loader.load(); gotResources.push_back(resource); // "loader" does not reuse "resource_" // because it's still shared with gotResources, } // ... }
毎回同じデータが返ってくるくだらないコードですが…。
話の肝は<===と書いてあるところの2行だけです。要はshared_ptrの指している先がunique(=参照カウントが1)だったら、自分しか使ってない訳だからリソースを再利用してもいいじゃん、というわけです。
参照カウントが0(=何も持っていない)ときにもuniqueにはならないので、nullの場合も自動的に確保されます。
大きなデータを受け取る場合は、利用側であらかじめ一時バッファ変数を用意しておいて、内部処理でそこに入れてもらうというやり方が一番よく使われますし簡単です。しかし、メモリを再利用したい場合は利用側が適切に管理する必要がありますし、バッファの型も一つに固定されます。上の例のようにshared_ptrで利用者にバッファを返却し、その利用状況を逐一監視しておけば、もう少し柔軟に運用できるんじゃないかというわけです。
shared_ptrの参照カウントを、単なるメモリの確実な解放だけでなく別の面白い用途に使えないかなあと考えたお話でした。
もっとも、上の例は一番単純な例で、もう少しうまくやるにはバッファプールなどを作る必要があると思います。ちなみに、weak_ptrだと解放されてしまうのでうまくいきません。
JNIの自動リソース解放
久々に記事を書こうと思い立ったので書きます。ブログという形態が情報共有に適切なのかどうかはさておいて、小さなひらめきであっても何らかの形で記録していかなければ自分の糧にならない、と改めて感じている今日この頃です。
最近Android NDKで作業する機会を得ていまして、C++をモリモリ書ける喜びを感じています。Androidデバイス本来の力を生かすにはネイティブコードは現状必須です。初期のAndroidのモッサリ感は、限られた資源の中でわざわざJava VM上にアプリケーションを実行しているせいだったのではないかと勘ぐることもしばしばです。
ネイティブコードを利用したAndroidアプリケーションを作るにはJNI (Java Native Interface)を使うことが必須となるのですが、このJNIがいろんな意味で非常に厄介です。JNIの問題点を適当にあげつらってみます。
- グルーコードを書くのが非常に面倒。(アプリケーションの本質でないところに労力をさく必要)
- ネイティブ領域で各種IDをいちいちキャッシュしないと実行速度が大幅に低下する。
- JavaもC++も静的コンパイルされる言語なのに、実行時まで正常に関数が呼び出されるかわからない。(失敗するとUnsatisfiedLinkErrorがthrowされる)
- ローカル参照は512個まで。(DeleteLocalRefで解放せずにそれ以上確保しようとすると即死)
以上のような内容に関して、IBMのサイトの以下の記事が参考になります。多くのこういったバッドノウハウ系の記事は読む気がしないものですが、この記事に関しては隅々まで非常に実践的な内容なのでおすすめです。
http://www.ibm.com/developerworks/jp/java/library/j-jni/
今回は、文字列と配列アクセスのグルーコードを書く手間を削減する方法についてです。大した内容ではないですが、一応ソースコードはGitHubにあげてあります。
https://github.com/meryngii/jni_util
Javaから渡される文字列と配列へのアクセスには、事前にネイティブ領域にロードする関数を呼び出す必要があります。そしてさらに、ネイティブ関数が終了するときには解放処理の関数をまた呼び出す必要があります。こんな感じで。
extern "C" JNIEXPORT void JNICALL Java_Foo_bar (JNIEnv* env, jobject thisj, jintArray arrayj) { jboolean isCopy; int* elements = env->GetIntArrayElements(arrayj, &isCopy); // Do something with array... env->ReleaseIntArrayElements(arrayj, elements, 0); }
C++プログラマとしてはRelease系の関数を直接呼び出したくないもので、これはRAII的にデストラクタで処理させるものでしょう。同じ考えのコードは探してみたらありました。
JNI C++ templates to acquire/release resources
あといちいち要素の型名を書かなければいけないのも気に食わないですね。ここはテンプレートでうまいことやるべきでしょう。というわけで以下のように。
#include "jni_util.hpp" extern "C" JNIEXPORT void JNICALL Java_Foo_bar (JNIEnv* env, jobject thisj, jintArray arrayj) { jni_util::CopiedArrayAccess<jint> array(env, arrayj); int* elements = array.elements(); size_t size = array.size(); // Do something with array... // Destructor automatically releases the resource }
まあ結局jintとか書いてる訳ですが…。本当はちゃんとiteratorをつけてコンテナとしての要件を満たすようにしようかなとも思うのですが、とりあえず実用上はこれで十分でしょう。
JNIでのJava配列へのアクセスには2種類あって、
- Get***ArrayElementsで一気にコピーする
- Get***ArrayRegionで少しずつコピーする
の2種類あります。それぞれ適当にCopied, Regionalと名前をつけて対応させておきました。
Release時の第3引数はmodeで、0, JNI_COMMIT, JNI_ABORTから選べます。0にしてReleaseしないと変更した値がJava側で反映されません。読み出しのみの場合はJNI_ABORTでOKです。*1modeに関してもコンストラクタに与える第3引数として付加してあります。
とりあえずこんなところで。
*1:JNI_COMMITって何のために使うんだろう…。
unite-outlineのLua向けinfo作った
今さらUnite.vim便利そうなので使い始めました。
unite-outlineを追加すると簡易アウトラインが見れるわけですが、Lua用が無かったので作りました。製作時間2時間半。
https://github.com/meryngii/unite-outline/blob/master/autoload/unite/sources/outline/defaults/lua.vim
単純に関数の一覧を生成するだけです。ラムダとかどうしようか…。
VimScriptは初めて書きましたけど、:helpは大事だと思いました。
他にも足りないものがあったら追加しようと思います。
amPlugのAUX端子修理
珍しく電子工作な話です。amPlugという安価で著名なヘッドホンアンプがありまして、半年前にベース用を買ったらどこでも練習できるようになって捗っていたのです。しかし、今日の練習前にAUX端子に刺したケーブルの根元を机にぶつけ、片耳側が鳴らなくなってしました…。
KORGのサイトには無料修理する気がないようにしか見えなかったので、分解修理を決行。入力端子の裏のねじを一本外して、カバーの真ん中をグリグリしていると比較的簡単に外れます。
コネクタを軽く押してみると、丸印のところが基板から剥がれそうになっていました。パターンを読むとたまたま近くに半田が盛ってあったので、リード線でつなげてあげるとあっさり修理完了。
未だに少し接触不良が残っていますが、両耳聞こえるようになりました。
メカニカルな部品はすぐに壊れるので困りますね。僕がパソコンを壊すのはいつもハードディスクか、周辺機器の端子からだったりしますし。
数式のLL構文解析
以前から数式処理をやってみたいと思っていたので、その下準備として構文解析を適当に書いてみました。完成に至るまで二転三転しましたが…。
それにしてもこういうコード片だとHaskellは大活躍ですね。Haskellすごい。
import Data.Char -- 以下から引用。標準のreadは失敗するとエラーを投げる。 -- http://d.hatena.ne.jp/sshi/20060630/p2 read' :: (Read a) => String -> Maybe a read' s = case [x | (x,t) <- reads s, ("","") <- lex t] of [x] -> Just x _ -> Nothing -- 式表現 data Expr = Const Integer | Symbol String | Add Expr Expr | Mul Expr Expr | Power Expr Expr deriving Show showExpr :: Expr -> String showExpr (Const x) = show x showExpr (Symbol x) = x showExpr (Add x y) = "(" ++ showExpr x ++ " + " ++ showExpr y ++ ")" showExpr (Mul x y) = "(" ++ showExpr x ++ " * " ++ showExpr y ++ ")" showExpr (Power x y) = showExpr x ++ "^" ++ showExpr y -- 構文解析 term, power, factor, expression :: [String] -> (Expr, [String]) term ("(":is) = let (e, (")":is')) = expression is in (e, is') term (i:is) = case read' i of Just x -> (Const x, is) Nothing -> (Symbol i, is) power = pow . term where pow (e, "^":is) = let (e', is') = power is in (Power e e', is') pow x = x factor = fac . power where fac (e, "*":is) = let (e', is') = power is in fac ((Mul e e'), is') fac (e, "/":is) = let (e', is') = power is in fac ((Mul e (Power e' (Const $ -1))), is') fac x = x expression = exp . factor where exp (e, "+":is) = let (e', is') = factor is in exp ((Add e e'), is') exp (e, "-":is) = let (e', is') = factor is in exp ((Add e (Mul (Const $ -1) e')), is') exp x = x -- 字句解析 lexer :: String -> [String] lexer [] = [] lexer (x:xs) | isAlpha x = wordWhile (\x -> isAlpha x && isDigit x) | isDigit x = wordWhile isDigit | isSpace x = lexer xs | isAscii x = [x] : lexer xs where wordWhile p = (x : takeWhile p xs) : (lexer $ dropWhile p xs) -- 構文解析 + 字句解析 parse = fst . expression . lexer
実行結果
> parse "a+b+c" Add (Add (Symbol "a") (Symbol "b")) (Symbol "c") > showExpr $ parse "1*2+3/4" "((1 * 2) + (3 * 4^-1))" > showExpr $ parse "a*y^2-b/c*y+1" "(((a * y^2) + (-1 * ((b * c^-1) * y))) + 1)"
減算と除算はそれぞれ乗算と累乗に直してあります。Subとかを作るのは簡単にできます。
LL法のパーサを実装すると、必ず左再帰の問題が発生しますね。
wikipedia:左再帰
今回作ったパーサは、左再帰を回避しつつ、左結合の構文木が作られるように配慮してあります。左結合の演算子(+, *)では、再帰呼び出しの中で、引数に呼び出し元の情報を溜め込んでいくのがポイントです。以下にBNFも置いておきます。
expression = factor expression' expression' = + factor expression' | - factor expression' | ε factor = power factor' factor' = * power factor' | / power factor' | ε power = term power' power' = ^ power' | ε term = Const | Symbol | ( expression )
実行速度は遅いと思いますが、こういうコードで速度が要求される場面もあまりないでしょう。もっといい方法あったら教えてください。