Edit
Androidのネイティブ動作について (android-ndk-r5で確認)

Andoridは、ARMv5TE(ARMv5 Thumb Enhance、Androidで採用されている代表的なCPU)というCPUで動作する、Linux環境であるため、対象CPU用のC言語でコンパイルされたネイティブのアプリを実行することが可能である。しかし、Androidマーケットや、Androidのアプリの制限からC言語で作成したアプリケーションをAndroid上でダイレクトに起動することはできず、あくまでもJavaのオブラートにかけた上で利用する形となる。

Edit
クロスプラットフォーム環境の構築

EclipseなどのIDE上で、ARM5のネイティブコードを作成するには、クロスコンパイラ環境を構築する必要がある。LinuxやOSXはUnix系の環境の為、gccのコンパイラ環境が標準で準備されているが、Winodowsユーザーの場合には、Cygwin環境を構築する必要がある。ここではコンパイルの基本的な環境は準備されているものとして話を進める。またEclipseを利用する場合には、CDT( C/C++ Development Tools )を導入しておく。

Edit
Android NDK (Native Development Kit)のインストール

Android NDKは、Android SDKをインストール後にインストールしなければならない。

  1. ここのNDKのサイトから、開発環境に合わせたファイルをダウンロードする。(手順はSDKを同じ)
  2. ダウンロード後、android-sdkを同じフォルダに入れておく。
  3. PATHを通す。設置したSDKと違い、NDKフォルダのTOPだけを追加するだけでよい。(ndk-buildのパスを通すという意味となる)

Edit
Cのコードの設置

NDKといえども、上記の通りあくまでJavaから呼び出されるモジュールを作成するために、プロジェクトの作成は、Android SDKを利用した、Android Projectとして作成される。プロジェクトを作成後は、プロジェクトの直下に、Nativeソースである、C/C++のソースコードを設置するためのフォルダとして <project>/jni/ というフォルダを作成し、Cのソースを設置する。またCソースの為のMakeFileは、Android.mk という名前で作成しておく必要がある。

Edit
コンパイルと実行

上記の設定で、コマンドラインから開発中のプロジェクトフォルダにカレントフォルダを移動し、ndk-build とするだけで、下位フォルダの <project>/jni/ フォルダにある、Android.mk ファイルをよみこみ、ARM5用の アプリ名.so ファイル(Linuxの動的ライブラリファイル)を <project>/obj/local/armeabi/ フォルダ以下に作成する。

その後、Eclipse上で実行をするだけで、Android SDKが自動的に .so をパッケージし、実行時に /data/data/com.example.アプリ名/lib/libアプリ名.so のような形で作成したライブラリを展開しロードする。

Edit
サンプル実行

上記でインストールした、Android NDKの中には samples フォルダがあり、Eclipse上から新規でAndroid Projectを選び、既存のソースからを選択し、複数あるサンプルから自分で確認したいサンプルを読み込んでテストすると良いだろう。もちろん、実行の為には上記のコンパイルと実行の項目で指示した ndk-build を行う必要がある。

Edit
JNI(Java Native Interface)の説明

Edit
具体的なJavaのコード

基本的にJava側では、ライブラリのロードと、関数の定義の2つだけで対応できる。

// ライブラリのロード
static { System.loadLibrary("hello-jni"); }
// 関数の定義
public native String stringFromJNI();
// 実際の利用
String text = stringFromJNI();

これらは、hello-jni というCのライブラリファイル(例:hello-jni.so)をロードし、stringFromJNI() という関数を利用する事を定義している。余談だが複数のライブラリのロードをした場合は、最初のロードしたライブラリが優先される。

Edit
具体的なC側のコード

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を理解するといっても過言ではない。

Edit
Android.mkの記述について

Android.mkは、Cのソースをビルドするにあたり、MakeFileと同じようにndk-buildによって参照されるファイルである。フォーマットの詳細はここを参照。

Edit
ミニテクニック

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++;
}

簡単な説明ページ
JNIのC言語/C++側のコーディング

Edit
配列をやり取りする。

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);
}

Edit
JNI専用の型

JAVAの型JNIを利用したのCの型名説明
booleanjbooleanunsigned 8 ビット
bytejbytesigned 8 ビット
charjcharunsigned 16 ビット
shortjshortsigned 16 ビット
intjintsigned 32 ビット
jsize
longjlongsigned 64 ビット
floatjfloat32 ビット
doublejdouble64 ビット
voidvoidN/A

Edit
トラブルシューティング

JNI_OnUnload()が呼び出されない
C/C++では、メモリを確保したら解放しなければならないのですが、ライブラリが解放されると、呼び出されるはずのJNI_OnUnload()関数が呼び出されません。原因は分かっていません。

クラスが何度も初期化される
もっと不可解なのが、static節が何度も呼び出されることです。例えば今回のサンプルのように、以下のコードのようにすると、不定期に何度もこのコードが実行されます。

static {
   System.loadLibrary("FireEffect");
}

結果として、JNI_OnLoad()が何度も呼び出されてしまうため、実は「JNI_OnLoad()やJNI_OnUnload()でメモリやリソースを管理する」という方法はAndroidでは破たんします。
※参照:Android NDKでJNIを使用してアプリを高速化するには

Edit
C言語からJavaを呼び出す場合

呼び出しの型の名前

Java側の型の種類intvoidbooleanshortdoblelongstringint arraylong array
CallMethodでの短縮型IVZSDJX[I[J

Edit
メソッドシグネチャーの説明

Edit
JNA(Java Native Access)の説明

Edit
デバッグ

Nativeで動作する場合、デバッグが非常に難しい。そこでSIGNAL-11などのSIGNALコマンドによって停止した場合の例を説明する。

Edit
SIGNAL-11 で停止

この場合、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」というようなアドレスが記入されているが、このままではソースのどの部分に当たる行かわかりづらい。

Edit
objdump(実行形式のファイルから、Cのソースとアセンブラ及びアドレスの複合表示ツール)

まず最初に、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


他の詳しいオプションなどについては、このページが詳しい

Edit
利用上の注意点

Edit
static 定義の変数やグローバル変数について

staticな変数や、グローバルな変数は、利用時に必ず初期化する必要がある。

static int gCounter = 10;

のような記述は、通常のCでのライブラリでは、初期化されるがJNIでは.so ファイルがメモリに残ったまま、再度必要な関数が呼び出される可能性があるため、このプログラムロード時に行われるような static 変数が初期化されず、過去の値が設定されたままになる。

Edit
OpenGL on NDK

Edit
JNIのコンパイル

Edit
ndk-build

Edit
引数一覧

コマンド概要
ndk-buildビルドの実行
ndk-build clean過去に生成したすべてのファイルの削除
ndk-build NDK_DEBUG=1デバッグモードでビルド。シンボリック情報やログを入れ、デバッグ可能なバイナリの生成
ndk-build NDK_DEBUG=0デバッグビルドを無効(リリース用)
ndk-build V=1Verboseモード。makeに渡す文字列をすべて表示
ndk-build -C <project>プロジェクトパスを指定しビルド。

Edit
Android.mk

Edit
include構文で利用できる引数

include $(引数)

CLEAR_VARSLOCAL_XXX変数を消す
BUILD_SHARED_LIBRARYLOCAL_MODULE,LOCAL_SRC_FILESをもとにshard libraryをビルドする、lib$(LOCAL_MODULE).soができる
BUILD_STATIC_LIBRARYstatic 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_LIBRARYPREBUILT_SHARED_LIBRARYのstatic library版
TARGET_ARCHターゲットのCPUアーキテクチャ名
TARGET_PLATFORMターゲットのアンドロイドプラットフォーム(≒version)
TARGET_ARCH_ABIターゲットのCPU+ABI
TARGET_ABI$(TARGET_PLATFORM)-$(TARGET_ARCH_ABI)

Edit
コンパイル時のポイント

  1. .soを連結することはできない、複数のライブラリを連結したいときは、最終ライブラリだけ、.soにしてそれ以外はすべて、.a(static)で作成
  2. スタティックライブラリは基本的にPREBUILD_STATIC_LIBRARYでコンパイルする。
  3. 最終的な共有ライブラリ(.so)のコンパイル時には、LOCAL_STATIC_LIBRARIES又は LOCAL_WHOLE_STATIC_LIBRARIESで上のモジュールを指定する。(全部取り込みたいときのみ。使用するものだけ取り込みたいときはLOCAL_STATIC_LIBRARIESを利用)

Edit
その他の参考リンク