ものがたり(旧)

atsushieno.hatenablog.com に続く

BridJ on Androidでネイティブコードを活用する

これは最近いじっていたBridJというライブラリについてまとめたものです。本当はもうちょっと体裁を整えてどっかに記事として投げられないかなあと思っていたんですが、そこに時間を割くのももったいないかなと思ってやめました。なわけで文体が違和感ありありですがそこは気にしない^2

      • -

AndroidとJNI

Javaでは、ネイティブコードを呼び出す方法として、JNI (Java Native Interface)が提供されています。JNIは、JavaC/C++で書かれたネイティブコードを繋ぐものです。具体的には、Javaでnativeキーワードを使用して定義したメソッドを入り口から、対応するCあるいはC++のコードを(プラットフォーム固有の)共有ライブラリとしてロードして実行できます。

JNIで使用する共有ライブラリとしては、Windowsであれば.dll、Mac OS Xであれば.dylib、Linuxであれば.soの各種ファイルが利用できます。JNI用のライブラリは、javahというツールにjavaのソースを渡すことで、Cのヘッダファイルを生成し、その関数を実装することで作成できます。

$ cat name/atsushieno/Test.java
package name.atsushieno;

public class Test
{
static native long dlopen (String libFileName, int mode);
static native int dlclose (long handle);
}

$ javac -d target name/atsushieno/Test.java

$ javah -classpath target name.atsushieno.Test

$ cat name_atsushieno_Test.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class name_atsushieno_Test */

#ifndef _Included_name_atsushieno_Test
#define _Included_name_atsushieno_Test
#ifdef __cplusplus
extern "C" {
#endif
/* (snip) */
JNIEXPORT jlong JNICALL Java_name_atsushieno_Test_dlopen
(JNIEnv *, jclass, jstring, jint);

/* (snip) */
JNIEXPORT jint JNICALL Java_name_atsushieno_Test_dlclose
(JNIEnv *, jclass, jlong);

#ifdef __cplusplus
}
#endif
#endif

JNIはAndroid仮想マシンであるdalvikでも利用できます。AndroidLinuxベースのプラットフォームであり、Android NDKというgccベースの開発環境を使用することで、共有ライブラリ (.so) を生成できます。Android NDKを利用して作成したライブラリは、プラットフォームに依存するので、各プラットフォーム向けにライブラリをビルドすることが望ましいです。公式にAndroid NDKに含まれるプラットフォームには、(大まかに言えば)armとx86が存在します。非公式にはMIPS用もあるようです。

以上のようにざっと説明すると簡単にも聞こえますが、実際にはそれほど簡単ではありません。javahで生成されるヘッダに含まれる関数は、ネイティブライブラリで実装されているコードに直接対応するものではないので、自分で実装しなければなりません(そうすることで、Sun/Oracleは、動的にライブラリを呼び出す際の煩雑な問題の幾ばくかをユーザーの責務としています)。さらに、Androidの場合、Android NDKを利用するプロジェクトの多くはndk-buildというビルドスクリプトを呼び出します。これはAndroid.mkという独自のMakefileを使用するもので、ユーザーはその書き方・使い方を覚えなければなりません(ndk-buildを使わない場合でも、NDKのツールチェインを正しくセットアップしてコンパイルするのは少々面倒ですが)。

しかし、Androidのネイティブコードには、大きな可能性があります。ひとつはパフォーマンスの大幅な改善です。Android 2.2以降はdalvikにJIT (Just-in-Time) コンパイラが搭載されるようになって、ネイティブに近い速度が出せるようになったコードもありますが、まだ追いつかない部分はあります。例えばネイティブで直接メモリ管理するコードには対抗できないでしょう。また、既存のネイティブコードが再利用できることは大きなメリットです。

というわけで、わたしはAndroidでもネイティブコードを積極的に活用できることが望ましいと考えています。そのために必要なステップとして、JNI用のCコードを書かずに利用できる(それによってネイティブコードを簡単に使える)、実用的な動的ライブラリ呼び出し環境が必要だと考えました。

AndroidとJNIについては、以下も参考になるでしょう(古い記事はNDKの使い方が古いこともありますが):

JNA、様々な類似プロジェクト、そしてBridJ

さて、Cコードを書かずにネイティブコードを呼び出す試みは、実のところSun Microsystemsでも行われていました。JNA (Java Native Access)です。これは、.NET FrameworkにおけるP/Invokeの機能にならって、Cコードを書かずに動的なネイティブライブラリの呼び出しを可能にしようというものです。JNAはlibffiを利用してC++とのinteroperabilityを実現しているようです。これでも利便性は向上しています。

JNAについてはこちらの記事が導入として参考になるでしょう: JNIより簡単にJavaとC/C++をつなぐ「JNA」とは

ただ、JNAはJava 1.4の時代に設計されたもので、Java 1.5のプリミティブ型やジェネリクスなど、有用な機能を活用できていません。Javaにはポインタ型が存在しないので、ポインタに相当するIntByReferenceやByteByReferenceといった型を使用してポインタを表現しますが、ジェネリクスが無いので、これらは全てByReferenceから派生する別々の型になります。ライセンスもLGPLで、組み込み用途によっては利用できないかもしれません。

JNAがいろいろと不便だったこともあって、JNAとは別に、さまざまなネイティブコード呼び出しのフレームワークが開発されてきました(JNIDirectJ/InvokeJavaCPPHawtJNIJavolutionなど)。それぞれに独自の利点がありますが、その中でわたしが特に興味をもったのがBridJというプロジェクトです。これは、JNAを起点としつつ、さらにC++の利用可能性を高め、Java 1.5のジェネリクスアノテーションを活用し、ライセンスもMIT/X11ベース、現在も活発に開発されています。わたしはこれに魅力を感じ、開発に協力することにしました。

ちなみにわたし自らも当初、Androidでも利用できるようなJNAとAPI互換の独自実装を作成しようとしていました。そのためには、特にJavaメソッドのさまざまな引数群をC関数の適切な引数型に変換した上で、Cの関数に引数として渡す機能が必要です。これを実現する標準的なCの方法は存在しないため、わたしは、次善の策として、これをプラットフォームやコンパイラフレームワーク別に実現するdyncallというライブラリを活用することを思いつき、libdlのdlopen関数と組み合わせて、任意の関数を実行する機能のプロトタイプを実装しました(dyncallはARMアーキテクチャgccのC呼び出しに対応しています)。BridJは、このdyncallを調べていた時に発見したもので、つまりBridJもわたしと同様のアプローチで関数呼び出しを実装しています。

JNAerator

BridJはNativeLibs4Javaというプロジェクトの一部であり、NativeLibs4Javaにはもうひとつ、JNAeratorという興味深いサブプロジェクトがあります。これは、Cのヘッダを解析して、前述のJNAに対応するJavaのライブラリを自動生成してしまおうというもので、BridJにも対応しています。JNAeratorを使えば、JNIのCコードを書くどころか、JNAで必要となるjava側のコードすら手書きする必要がなくなるのです。これはとても便利です。

JNAeratorは id:syuu1228:20100316:1268737011 - miniupnpc for JavaとJNAeratorのお話 - でも紹介されています。

(.NETのP/Invokeには、P/Invoke Signature Generatorというツールがあるようですが、実用的かどうかはわたしは評価していないのでコメントできません。わたしが.NET用でJNAeratorに最も近いと思ったのは、Gtk#におけるgapiと呼ばれる一連の自動生成ツールです。)

JNAeratorはJava Web Startアプリケーションとして実行できます。

わたしは自分のAndroidアプリケーションで、libcのdirent.hに含まれる関数を使いたいと考え、dirent.hをJNAeratorに渡して、生成されたjavaソースをプロジェクトに追加して呼び出してみました。後述する問題を修正するだけで、簡単にbionic libcの機能を呼び出すことができました。ユーザーは、生成されたライブラリの関数を呼び出すだけです。

特にBridJと組み合わせた場合、JNAeratorは、未知の型を単純にメンバーなしのinterfaceとして定義してしまい、それをポインタで利用する関数ではPointer型を経由してそのインターフェースを参照します。Cヘッダは何層にもインクルードしていることが多いですが、多くの場合、同じヘッダファイル内で必要とされる型で未定義のものは構造体へのポインタとして参照されているでしょう(たとえば、Cの標準ファイルI/Oを使用する時は、FILE型の詳細に触れることなくFILE*を使用するのが一般的です)。ポインタはPointer<FooBar>として自動生成され、それら未知の型を全てJNAeratorに渡さなくても、必要なものだけ渡すことで、型の内部構造には踏み込まずに、Cの関数を呼び出すことができるのです。

もちろん、必要であれば別途CヘッダをJNAeratorに渡して型を生成することも出来ます。後からダミーのinterfaceを実際の型に置き換える作業も、大規模なインポートを行うのでなければ、それほど困難ではないでしょう。

Dalvikのコード生成制約(解決済)

さて、BridJは便利ですが、Android上で実行できるようになるには大きな制約がありました。それはクラスコードの自動生成の制約です。

BridJでは、Cの関数ポインタに対応する関数コールバッククラスを生成して、それをユーザーが派生させて(その型の関数に対応する)メソッドをオーバーライドすることで、ユーザーコードを関数ポインタとして呼び出せるようにしています。この関数コールバッククラスの生成部分では、java.lang.ClassLoaderクラスのdefineClass()というメソッドを利用しているのですが、このメソッドがAndroidAPIでは実装されていません(UnsupportedOperationExceptionが投げられます)。

dalvik VMレジスタマシンであり、dexという独自のバイトコードのみがサポートされています。スタックマシンであるJVMが使用するJavaバイトコードは、あくまでアプリケーションのビルド時にdexに変換された上でapkに含まれるようになっています。そして、dexを実行時にロードすることはできても、Javaのクラスを動的にロードする機能は(前述の通り)実装されていません。このため、Androidでは動的なクラスの生成が出来ないとされています。

この問題を解決するため、例えばClojureには独自のdex変換サポートが含まれています。これは、dxというAndroid SDKに含まれるツールのソースを再利用することで実現されています。dxにはCfTranslatorというクラスがあり、これがjavaバイトコードをdexに変換するようです。BridJでも、とりあえずこのdxの機能を利用してdalvik用のコードを生成する機能を実装することにしました。

ちなみに、BridJでは、バイトコード生成にはASMというJavaのプロジェクトを利用しています。これは現在のところJVMバイトコードの生成しかサポートしていませんが、これがもしdexバイトコードを生成できるようになれば、BridJからもだいぶシームレスにコード生成が行えるようになるでしょう(し、Closureのような動的言語におけるニーズを満たすこともできるかもしれません)。ただ、設計思想の相違を乗り越えなければならないので、実現は難しいかもしれません。

BridJの制約(2011年6月現在)

BridJはまだバージョン1.0リリースにも到達していない、比較的若いプロジェクトであり、まだ少々制約があります。大きな問題としては、構造体の値渡しによる関数呼び出しがサポートできていません。多くのCライブラリでは構造体へのポインタを利用しているでしょうが、構造体引数が利用できないのでは困る場面も少なくないでしょう。ここはJNAでは実現していることであり、BridJでも今後なんらかのかたちで実装出来ないだろうかと思っています。

また、Android-x86に対応したビルドはまだ出来ていません。これは誰も試していないというのが実態なので、もしかしたら簡単にできるかもしれませんし、BridJが依存しているdyncallを修正しなければならないかもしれません。

BridJの制約にぶつかってどうしようもない場合は、それでもJNIを併用することが可能ですし、C/C++でポインタベースのライブラリ関数を作成して、それをBridJで呼び出すようにするのも良いでしょう。

Android NDKでネイティブライブラリをビルド

さて、ここからは具体的にBridJを利用するまでの手順を説明していこうと思います。大まかに分けると、以下のような流れになります。

まず、ネイティブライブラリを利用するには、Android NDKを使用してネイティブライブラリをビルドする必要があります。ただし、Android本体に含まれるネイティブライブラリについては、その必要はありません。Android NDKでは、Androidのバージョンごとにネイティブライブラリとして利用可能なものが決められています。AOSP (オープンソースAndroid)に含まれていても実機によって含まれていないものがあり得るので、Android NDKに含まれるものだけを利用するのが正統とされていることに注意してください。

Android NDKには、いわゆるGNU compiler collection (GCC)のツールが含まれています。Android NDKが特殊なのは、libcの実装としてbionicという独自の標準Cライブラリが使用されていることです(ライセンスの関係でリベラルなものがフルスクラッチで実装されています)。

これらのツールの標準的な使い方については、NDKのドキュメントを読むべきですが、(あくまで参考として)以下のようなconfigureオプションによるmakeと手動libcリンクによって、いわゆるautotoolsによるビルドシステムでもビルドできることもあります。


# ./configure --host=arm-eabi CC=arm-eabi-gcc CPPFLAGS="-I$NDK_ROOT/platforms/android-9/arch-arm/usr/include/" CFLAGS="-nostdlib" LDFLAGS="-Wl,-rpath-link=$NDK_ROOT/platforms/android-9/arch-arm/usr/lib/ -L$NDK_ROOT/platforms/android-9/arch-arm/usr/lib/" LIBS="-lc "
# make
# arm-eabi-gcc -nostdlib -shared -s -o YOUR_LIBRARY.so --whole-archive -Wl,-whole-archive YOUR_PROJECT_SOURCES/.libs/YOUR_LIBRARY.a -Wl,-no-whole-archive --no-whole-archive -L $NDK_ROOT/platforms/android-9/arch-arm/usr/lib -lc

Android NDK r5cを前提としています / autotoolsとNDKについてはこちらが参考になります: Building Open Source libraries with Android NDK

ただし、Unix環境向けに書かれた大抵のライブラリには依存ソフトウェアがあり、また暗黙的にglibcに依存していてbionicではビルド出来ないものも存在します。

(JNIの標準的な方法によらずに)ビルドしたネイティブライブラリは、apkパッケージの中で、lib/armeabi などのディレクトリに.soを含める必要があります(Javaプロジェクトであれば、プロジェクトのトップディレクトリに上記のディレクトリを作成して.soをコピーします)

JNAeratorStudioの使い方

BridJを利用する最も簡単な方法は、JNAeratorを利用してネイティブライブラリのバインディングを自動生成してしまうことです。ここでは、コードの自動生成を通じて、生成されたライブラリの構造に軽く言及しつつ、使い方に主眼を置いて説明することにします。

まず JNAeratorStudio を起動しましょう。Java Web Startが動作する環境がセットアップされていれば、JNAeratorのプロジェクトページにあるリンクから起動できます。

何回かJava Web Startから問い合わせられるアクセス要求を承諾して、JNAeratorStudioが立ち上がったら、以下の作業を行います。

  • Runtime のリストボックスで "BridJ (...)" を選択する。
  • Library Name のテキストボックスに、呼び出すCのライブラリ名を入力する。これはそのままパッケージ名にも使用されますが、名前に . (dot)を含めると生成コードがそのまま使えないので、区切りのない名前にしておいた方が良いでしょう。
  • 利用したいライブラリのCヘッダを貼りつける。(残念ながら、現状では1つのテキストエリアにしか対応していません。)
  • "Ready to JNAerate" と書かれた画面最下部にあるボタンを押す(凹んでいるので分かりにくいですが)。

以下は Unixのdirent.hを渡す場合の例です。

ファイルの生成には(特に初回は)時間がかかるので、しばらく待ちます。

変換が完了すると、複数のjavaソースファイルがjarにまとめられてローカルの一時ファイルとして保存されます。"Show JAR" ボタンを押して内容を表示して解凍するなり、JNAerated classes のリストボックスでファイルを選択して内容を確認するなりします。

生成されたライブラリには、構造体などの型に対応するクラスと、グローバルな(例えばCの)関数に対応するXxxxLibraryというクラスがあります。

それぞれに @Library というアノテーションが付いていますが、これは対応する定義が含まれているとされるネイティブライブラリの名前となります。ですので、パッケージ名とCのライブラリ名が異なる場合は、これらを全て書き換えなければなりません。わたしの場合、dirent.h に対応するバインディングを作成するために dirent という名前を設定しましたが、これはlibc.soに含まれるので @Library ("c") に書き換えました。

構造体に対応するクラスは、StructObjectクラスから派生することになり、構造体のメンバーフィールドには @Field というアノテーションが付けられ、これにはメンバーのC構造体における位置が渡されています(位置であり、メモリ上のサイズに基づくオフセットではありません)。ここで、もしJNAeratorがメンバーの型定義を解決できないフィールドがあった場合には、そのメンバーフィールドの生成がスキップされてしまいます。しかしそれらが足りないと、その型の非ポインタ値を利用するコードが正しく動作しない(メンバーの読み取りや書き込みに失敗する)ので、JNAeratorに必要な型情報を補充するか、生成されたコードに手を加える必要があります。そうしないと、正しく読み取り・書き込みが行われず、実行時に例外が発生することになります。

グローバル関数を全て定義した XxxxLibrary クラスには、「ランタイム」を定義する @Runtime というアノテーションが付いて、さらにC関数の呼び出しに対応するJava関数を定義しています。ここでは @Runtime にはBridJの CPPRuntime.class が渡されています。関数は定義上はstatic nativeメソッドとなっており、その呼び出しが実行時に処理されます。実際にはこのクラスのstaticコンストラクターでBridJ.register()というメソッドが呼び出されており、この中でネイティブライブラリがロードされ、System.loadLibrary()の呼び出しで定義された*BridJの*ネイティブライブラリが呼び出されて、その内部処理において実際に呼び出されるライブラリのC関数が適宜マッピングされる、という仕組みになっています。

BridJの使い方(の基本)

さて、今度はJNAeratorで生成されたコードをJavaのプロジェクトに組み込んで、それを呼び出します。といっても、XxxxLibrary クラスで定義されたstatic nativeメソッドを呼び出すだけです。

実際にはBridJの呼び出しにはJNAeratorで生成されたコードである必要は無く、@Libraryと@RuntimeをもちBridJ.register()を呼び出したクラスがnativeとして定義されたC関数を呼び出すだけで足りるはずですが、JNAeratorで生成したものを使う方が簡単でしょう。

メソッドの引数は、Cの基本型については概ね直感的に渡せます。Stringの渡し方はchar*, wchar_t*など、複数の種類があるので、適宜使い分ける必要があります。Stringをchar*にするには、Pointer.pointerToCString(String)というstaticメソッドを利用します。ポインタ変数を渡すには、Pointer.pointerTo(Object) というstaticメソッドを利用します。

(構造体の値渡しを含む関数は、2011年6月の時点でサポートされていないので、注意してください。BridJが正しくシンボル解決せず、ネイティブ関数とのマッピングの過程でエラーが発生し、呼び出しにも失敗します。)

逆に、メソッドの戻り値がPointerである場合など、Pointerを個別のJava型に変換する場合は、PointerクラスのgetXxx()インスタンスメソッドを利用します。getNativeObject(Class)が汎用的に利用できます。

Pointer型はジェネリッククラスであり、JNAのようにプリミティブ型に特化したXxxxByReference クラスよりもずっと直感的に使用できます。

追記: 重要: eclipse ADTのプロジェクトでBridJを参照する場合は(大抵の人がそうするかと思いますが)、bridj-0.5-android.jar をandroidプロジェクトで参照するだけではエラーになってしまいます。これは既知のeclipse ADTの問題です。eclipseから参照する場合は、いったんbridj-0.5-android.jarからlib/armeabi/libbridj.soを除外したjarを作成して、libbridj.soは自分のプロジェクトで直接 lib/armeabi 以下に配置するようにすれば、eclipseからでも問題なく利用できます。

利用例

最後になりますが、わたしがディレクトリツリーの効率的なルックアップのために dirent.h を利用した例を紹介します。これは、java.io.FileクラスのlistFiles()が、ディレクトリとファイルの全エントリについてFileインスタンスを生成する非効率を嫌って作成したもので、java.io.Fileより高速なディレクトリとファイルのルックアップを実現するものです。

以下は自動生成でないコードです:

以下は、JNAeratorによる自動生成に @Library を手書きで修正したコードです:

追記: 以下のコードでunistd.hに含まれるaccess()も呼び出していたので、これに対応する自動生成コードにもリンクしておきます(ほとんどの関数は無関係なので、コメントアウトしてあります) - unistdLibrary.java

以下は、以上のコードの利用例として、.oggファイルを含むディレクトリを列挙するものです。Android向けに、ルックアップしても時間の無駄になる特定のディレクトリは除外してあります。

   void getOggDirectories(String path, List<String> list) {
       if (unistdLibrary.access(Pointer.pointerToCString(path),
               unistdLibrary.X_OK | unistdLibrary.R_OK) != 0)
           return;
       if (path.equals("/proc") || path.equals("/sys") || path.equals("/data"))
           return;
       DirectoryIterator di = new DirectoryIterator(path);
       boolean hasOgg = false;
       do {
           DirectoryEntry de = di.next();
           if (de == null)
               break;
           if (de.getEntryType() == DirectoryIterator.ENTRY_TYPE_DIR) {
               String dn = de.getName();
               if (!dn.equals(".") && !dn.equals(".."))
                   getOggDirectories((path.equals("/") ? "" : path).concat(
                           File.separator).concat(dn), list);
           }
           if (!hasOgg && de.getName().toLowerCase().endsWith(".ogg")) {
               hasOgg = true;
               list.add(path);
           }
       } while (true);
   }

最後にひとこと

というわけで、今回はBridJについて簡単に紹介してみました。BridJで皆さんがネイティブライブラリ依存アプリケーションの開発を簡単にできるようになると、わたしも嬉しく思います。

使ってみて問題などがありましたら教えてもらえれば、開発者にフィードバックします。