Andoridは、ARMv5TE(ARMv5 Thumb Enhance、Androidで採用されている代表的なCPU)というCPUで動作する、Linux環境であるため、対象CPU用のC言語でコンパイルされたネイティブのアプリを実行することが可能である。しかし、Androidマーケットや、Androidのアプリの制限からC言語で作成したアプリケーションをAndroid上でダイレクトに起動することはできず、あくまでもJavaのオブラートにかけた上で利用する形となる。
EclipseなどのIDE上で、ARM5のネイティブコードを作成するには、クロスコンパイラ環境を構築する必要がある。LinuxやOSXはUnix系の環境の為、gccのコンパイラ環境が標準で準備されているが、Winodowsユーザーの場合には、Cygwin環境を構築する必要がある。ここではコンパイルの基本的な環境は準備されているものとして話を進める。またEclipseを利用する場合には、CDT( C/C++ Development Tools )を導入しておく。
Android NDKは、Android SDKをインストール後にインストールしなければならない。
NDKといえども、上記の通りあくまでJavaから呼び出されるモジュールを作成するために、プロジェクトの作成は、Android SDKを利用した、Android Projectとして作成される。プロジェクトを作成後は、プロジェクトの直下に、Nativeソースである、C/C++のソースコードを設置するためのフォルダとして <project>/jni/ というフォルダを作成し、Cのソースを設置する。またCソースの為のMakeFileは、Android.mk という名前で作成しておく必要がある。
上記の設定で、コマンドラインから開発中のプロジェクトフォルダにカレントフォルダを移動し、ndk-build とするだけで、下位フォルダの <project>/jni/ フォルダにある、Android.mk ファイルをよみこみ、ARM5用の アプリ名.so ファイル(Linuxの動的ライブラリファイル)を <project>/obj/local/armeabi/ フォルダ以下に作成する。
その後、Eclipse上で実行をするだけで、Android SDKが自動的に .so をパッケージし、実行時に /data/data/com.example.アプリ名/lib/libアプリ名.so のような形で作成したライブラリを展開しロードする。
上記でインストールした、Android NDKの中には samples フォルダがあり、Eclipse上から新規でAndroid Projectを選び、既存のソースからを選択し、複数あるサンプルから自分で確認したいサンプルを読み込んでテストすると良いだろう。もちろん、実行の為には上記のコンパイルと実行の項目で指示した ndk-build を行う必要がある。
基本的にJava側では、ライブラリのロードと、関数の定義の2つだけで対応できる。
// ライブラリのロード static { System.loadLibrary("hello-jni"); } // 関数の定義 public native String stringFromJNI(); // 実際の利用 String text = stringFromJNI();
これらは、hello-jni というCのライブラリファイル(例:hello-jni.so)をロードし、stringFromJNI() という関数を利用する事を定義している。余談だが複数のライブラリのロードをした場合は、最初のロードしたライブラリが優先される。
C側で特に注意するべきは、Object型のJavaとのデータ受け渡しのため、複雑な変数や名前を使う。
jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env, jobject thiz ) { return (*env)->NewStringUTF(env, "Hello from JNI !"); }
これは、javaのString型の戻り値(jstring型)を持つ、関数を定義している。JNIEnv型はJNI先頭のデータ受け渡し用の方であり、これを理解することがJNIを理解するといっても過言ではない。
Android.mkは、Cのソースをビルドするにあたり、MakeFileと同じようにndk-buildによって参照されるファイルである。フォーマットの詳細はここを参照。
java.nio.ByteBufferを使う
JNIはJava側との通信に非常にコストが掛かる。そのためデータ参照を効率化するために、java.nio.ByteBufferクラスを利用することで、Javaのデータを直接参照できる。
ByteBuffer buffer = ByteBuffer.allocateDirect(バイト数); buffer.order(ByteOrder.nativeOrder()); FloatBuffer floatBuffer = buffer.asFloatBuffer(); floatBuffer.put(x); nativeCall(floatBuffer); // Native呼び出し
C言語(Native)側
JNIEXPORT void JNICALL Java_com_domain_nativeCall (JNIEnv*env, jobject thiz, jobject buffer){ float *buf = (float *)env->GetDirectBufferAddress(env, buffer); float x = *buf++; }
JAVA側
private native void setDataPointer( int[] data ); public void ArraySend(){ int[] data = new int[5]; for( int i=0 ; i<5 ; i++ ){ data[i] = 5000 + i; } setDataPointer(data); for( int i=0 ; i<5 ; i++ ){ Log.d(TAG, " newNum[" + i + "] = " + data[i]); } }
C側
void Java_com_testDraw_ArrayTest_setDataPointer( JNIEnv* env, jobject thiz, jintArray arrayInt ) { jboolean resultFlag; // 関数の結果を戻すためのフラグ領域 int length; // 取得した配列の長さ int i; // ループカウンタ // データの取り出し jint * newArray = (*env)->GetIntArrayElements(env,arrayInt,&resultFlag); length = (*env)->GetArrayLength(env,arrayInt); // データ加工 for( i=0 ; i< length ; i++ ){ newArray[i] = 100 + i; } // 取得した領域の開放(Java側でstaticで確保している場合は必要ない) (*env)->ReleaseIntArrayElements(env, arrayInt, newArray, 0); }
JAVAの型 | JNIを利用したのCの型名 | 説明 |
boolean | jboolean | unsigned 8 ビット |
byte | jbyte | signed 8 ビット |
char | jchar | unsigned 16 ビット |
short | jshort | signed 16 ビット |
int | jint | signed 32 ビット |
jsize | ||
long | jlong | signed 64 ビット |
float | jfloat | 32 ビット |
double | jdouble | 64 ビット |
void | void | N/A |
JNI_OnUnload()が呼び出されない
C/C++では、メモリを確保したら解放しなければならないのですが、ライブラリが解放されると、呼び出されるはずのJNI_OnUnload()関数が呼び出されません。原因は分かっていません。
クラスが何度も初期化される
もっと不可解なのが、static節が何度も呼び出されることです。例えば今回のサンプルのように、以下のコードのようにすると、不定期に何度もこのコードが実行されます。
static { System.loadLibrary("FireEffect"); }
結果として、JNI_OnLoad()が何度も呼び出されてしまうため、実は「JNI_OnLoad()やJNI_OnUnload()でメモリやリソースを管理する」という方法はAndroidでは破たんします。
※参照:Android NDKでJNIを使用してアプリを高速化するには
呼び出しの型の名前
Java側の型の種類 | int | void | boolean | short | doble | long | string | int array | long array |
CallMethodでの短縮型 | I | V | Z | S | D | J | X | [I | [J |
Nativeで動作する場合、デバッグが非常に難しい。そこでSIGNAL-11などのSIGNALコマンドによって停止した場合の例を説明する。
この場合、Logcatに下記のようなダンプが表示される。
06-17 01:17:01.261: INFO/DEBUG(2375): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 06-17 01:17:01.261: INFO/DEBUG(2375): Build fingerprint: 'samsung/SC-01C/SC-01C/SC-01C:2.2/FROYO/OMJK2:user/release-keys' 06-17 01:17:01.261: INFO/DEBUG(2375): pid: 23452, tid: 23466 >>> com.sample.android.myapp <<< 06-17 01:17:01.261: INFO/DEBUG(2375): signal 11 (SIGSEGV), fault addr 81b4c5a7 06-17 01:17:01.261: INFO/DEBUG(2375): r0 00000006 r1 00000018 r2 81b4adbe r3 81b4c5a6 06-17 01:17:01.261: INFO/DEBUG(2375): r4 81b49d60 r5 00000000 r6 430c7ec0 r7 430c7ecc 06-17 01:17:01.261: INFO/DEBUG(2375): r8 4470cb80 r9 430c7ec4 10 430c7eac fp 00000130 06-17 01:17:01.265: INFO/DEBUG(2375): ip 81b49d70 sp 4470caa0 lr 819890c0 pc 819b05c8 cpsr 20000010 06-17 01:17:01.265: INFO/DEBUG(2375): d0 643a64696f72646e d1 6472656767756265 06-17 01:17:01.265: INFO/DEBUG(2375): d2 47f8a41847f8a4ff d3 47f6e9a847f8a3ff <省略> 06-17 01:17:01.265: INFO/DEBUG(2375): scr 20000012 06-17 01:17:01.288: INFO/DEBUG(2375): #00 pc 001b05c8 /data/data/com.sample.android.myapp/lib/libmyapp-native.so 06-17 01:17:01.288: INFO/DEBUG(2375): #01 pc 001b006c /data/data/com.sample.android.myapp/lib/libmyapp-native.so 06-17 01:17:01.288: INFO/DEBUG(2375): #02 pc 0017a58c /data/data/com.sample.android.myapp/lib/libmyapp-native.so 06-17 01:17:01.288: INFO/DEBUG(2375): #03 pc 0017dddc /data/data/com.sample.android.myapp/lib/libmyapp-native.so 06-17 01:17:01.288: INFO/DEBUG(2375): #04 pc 0017d3c0 /data/data/com.sample.android.myapp/lib/libmyapp-native.so 06-17 01:17:01.288: INFO/DEBUG(2375): #05 pc 001c0a14 /data/data/com.sample.android.myapp/lib/libmyapp-native.so 06-17 01:17:01.288: INFO/DEBUG(2375): #06 pc 00016df4 /system/lib/libdvm.so 06-17 01:17:01.292: INFO/DEBUG(2375): #07 pc 00045284 /system/lib/libdvm.so 06-17 01:17:01.292: INFO/DEBUG(2375): code around pc: 06-17 01:17:01.292: INFO/DEBUG(2375): 819b05a8 e3e03000 e5cd3036 ea00000e e5dd2051 <省略> 06-17 01:17:01.292: INFO/DEBUG(2375): code around lr: 06-17 01:17:01.292: INFO/DEBUG(2375): 819890a0 e1cd30be ea00000a e59d3004 e5932000 <省略> 06-17 01:17:01.292: INFO/DEBUG(2375): stack: 06-17 01:17:01.292: INFO/DEBUG(2375): 4470ca60 00244658 [heap] 06-17 01:17:01.292: INFO/DEBUG(2375): 4470ca64 0000818c /system/bin/app_process 06-17 01:17:01.292: INFO/DEBUG(2375): 4470ca68 001f0000 [heap] 06-17 01:17:01.292: INFO/DEBUG(2375): 4470ca6c 81af1700 /data/data/com.sample.android.myapp/lib/libmyapp-native.so
ここで注目されるべきは、signal 11で停止した事と、scr 20000012以下の行で説明される関数の呼び出しの遷移である。表記は#00が落ちた場所であり、#00を呼び出したのが#01、#01を呼び出したのが#02と、順番が逆で表示されている。次にこれら各行に「pc 001b05c8」というようなアドレスが記入されているが、このままではソースのどの部分に当たる行かわかりづらい。
まず最初に、Android用としては、NDKのフォルダに、toolchains/arm-linux-androideabi-4.4.3/prebuilt/xxxx/bin/arm-linux-androideabi-objdumpが存在するので、これを使う。以下に説明するのは、一般的なobjdumpについてである。gnuのBinutilsに含まれるツールである。機能はコンパイルされた実行形式のファイルから、そのアドレスに対応するソースとアセンブラの行を表示してくれる、便利なツールであり、ここからダウンロードして、自分の環境で使えるようにする。
# tar -zxvf binutils-2.21.tar.gz # cd binutils-2.21 # ./configure # make # make install
使い方は簡単であり、下記の形でソースを混ぜて表示するだけである。ここで先程のアドレスを元にソースのどの部分が落ちたのかを大体把握することが可能である。
# objdump -d -s -l libmyapp-native.so
他の詳しいオプションなどについては、このページが詳しい
staticな変数や、グローバルな変数は、利用時に必ず初期化する必要がある。
static int gCounter = 10;
のような記述は、通常のCでのライブラリでは、初期化されるがJNIでは.so ファイルがメモリに残ったまま、再度必要な関数が呼び出される可能性があるため、このプログラムロード時に行われるような static 変数が初期化されず、過去の値が設定されたままになる。
コマンド | 概要 |
ndk-build | ビルドの実行 |
ndk-build clean | 過去に生成したすべてのファイルの削除 |
ndk-build NDK_DEBUG=1 | デバッグモードでビルド。シンボリック情報やログを入れ、デバッグ可能なバイナリの生成 |
ndk-build NDK_DEBUG=0 | デバッグビルドを無効(リリース用) |
ndk-build V=1 | Verboseモード。makeに渡す文字列をすべて表示 |
ndk-build -C <project> | プロジェクトパスを指定しビルド。 |
include $(引数)
CLEAR_VARS | LOCAL_XXX変数を消す |
BUILD_SHARED_LIBRARY | LOCAL_MODULE,LOCAL_SRC_FILESをもとにshard libraryをビルドする、lib$(LOCAL_MODULE).soができる |
BUILD_STATIC_LIBRARY | static libraryをビルドする、shard libraryからはLOCAL_STATIC_LIBRARIES,LOCAL_STATIC_WHOLE_LIBRARIESに指定してリンクする、lib$(LOCAL_MODULE).aができる |
PREBUILT_SHARED_LIBRARY | 事前にビルドされたshard libraryを取り込む、prebuiltするときにLOCAL_SRC_FILESで取り込むバイナリファイルを指定する(アーキテクチャに合わせて適切なバイナリモジュールを選ぶ必要がある、TARGET_ARCH_ABIを使ってパスを指定するのが良い)。他のモジュールからはLOCAL_SHARED_LIBRARYで読み込める。ヘッダを公開するときはLOCAL_EXPORT_C_INCLUDES を使う。 |
PREBUILT_STATIC_LIBRARY | PREBUILT_SHARED_LIBRARYのstatic library版 |
TARGET_ARCH | ターゲットのCPUアーキテクチャ名 |
TARGET_PLATFORM | ターゲットのアンドロイドプラットフォーム(≒version) |
TARGET_ARCH_ABI | ターゲットのCPU+ABI |
TARGET_ABI | $(TARGET_PLATFORM)-$(TARGET_ARCH_ABI) |