AndroidでJavaコードからネイティブ関数を呼び出す方法についてまとめました。com.example.android.MainActivityというJavaコードと、jni/hello/hello.cというネイティブコードを使います。
ネイティブからJavaを呼び出す場合はこちらを参照してください。
Table of Contents
1 ネイティブ関数の命名規則
com.example.androidパッケージのMainActivityクラスから呼び出すhelloWorld関数は以下の名前になります。
package com.example.android; <snip> public class MainActivity extends Activity { private native void helloWorld(); <snip> }
Java_com_example_android_MainActivity_helloWorld Java_[.が_に置き換えられたパッケージ名]_[クラス名]_[関数名]
1.1 関数名にアンダースコアがついている場合
ネイティブコードの関数名にアンダースコアが含まれる場合は注意が必要です。
ネイティブコード側で_を_1にする必要があります。
hello_worldという関数名を使う場合、Java側は以下のようになります。
package com.example.android; <snip> public class MainActivity extends Activity { private native void hello_world(); <snip> }
ネイティブコード側は以下のようになります。
パッケージ名、クラス名の後の名前がhello_1worldになっています。
JNIEXPORT void JNICALL Java_com_example_android_MainActivity_hello_1world (JNIEnv *, jobject);
ネイティブコードの関数名はスナークケースではなく、キャメルケースで命名した方が良いでしょう。
2 javahでネイティブコードのヘッダファイルを作成
javahでネイティブコードの関数を定義したヘッダファイルが生成されます。
classpathオプションに、Androidプロジェクトが生成するbin/classesディレクトリとandroid.jarを指定し、ネイティブコードの関数を呼び出すJavaクラス名を指定します。
$ javah -classpath ~/bin/sdk/platforms/android-17/android.jar:./bin/classes \ com.example.android.MainActivity
以下のヘッダファイルが生成されました。
$ cat com_example_android_MainActivity.h /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_example_android_MainActivity */ #ifndef _Included_com_example_android_MainActivity #define _Included_com_example_android_MainActivity #ifdef __cplusplus extern "C" { #endif #undef com_example_android_MainActivity_MODE_PRIVATE #define com_example_android_MainActivity_MODE_PRIVATE 0L <snip> /* * Class: com_example_android_MainActivity * Method: nativeHelloWorld * Signature: ()V */ JNIEXPORT void JNICALL Java_com_example_android_MainActivity_nativeHelloWorld (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
ネイティブコードを以下のように定義します。
#include <com_example_android_MainActivity.h> #include <android/log.h> #define JNI_METHOD_NAME(name) \ Java_com_example_android_MainActivity_##name #define JNI_DEFINE_METHOD(type, name, args...) \ JNIEXPORT type JNICALL JNI_METHOD_NAME(name)(JNIEnv *env, \ jobject object, ##args) #define JNI_LOG(args...) \ __android_log_print(ANDROID_LOG_INFO, "hello", ##args) JNI_DEFINE_METHOD(void, helloWorld) { JNI_LOG("Hello, World\n"); }
2.1 custom_rules.xmlでjavah
android create projectでAndroidプロジェクトを作成した場合にant用のbuild.xmlが作成されます。
custom_rules.xmlはbuild.xmlから呼ばれるant用のファイルです。
こちらにユーザ固有の処理を追加することができます。
antはjavahにも対応しているので、今回はjavahをcustom_rules.xmlで実行します。
destdirにヘッダファイルの出力先(ネイティブコードのディレクトリ)、classpathにbin/classesとandroid.jar、classにMainActivityを指定します。
<?xml version="1.0" encoding="UTF-8"?> <project> <target name="-post-compile"> <javah destdir="jni/hello" classpath="bin/classes:${sdk.dir}/platforms/android-17/android.jar" class="com.example.android.MainActivity"/> <exec executable="ndk-build" failonerror="true"/> </target> </project>
targetディレクティブのnameによって、どの段階でユーザ固有処理を実行するかを決定します。
Javaコードは-compileの段階でコンパイルされる為、"-post-compile"を指定します。
また、javahでヘッダファイルが生成される為、それをincludeするネイティブコードをndk-buildでコンパイルします。
antコマンドを実行すると以下のログが得られます。
-compile: Compiling 3 source files to /Users/hiroom2/src/AndroidJniFromJava/bin/classes -post-compile: Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 [armeabi-v7a] Compile thumb : hello <= hello.c [armeabi-v7a] SharedLibrary : libhello.so [armeabi-v7a] Install : libhello.so => libs/armeabi-v7a/libhello.so
私の環境ではexport JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8を指定している為、javah実行時に"Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8"と表示されています。
3 サンプルコード
jcharやGetCharArrayElement等のJNIで使用する型や関数は以下で定義されています。
各関数の詳細はOracleのマニュアルに記載されています。
ndk/platforms/android-17/arch-arm/usr/include/jni.h
サンプルコードではJNIEnv *envのメンバ関数ポインタに対して、以下のマクロを使用して関数呼び出しを実行します。
#define JNI_CALL(name, args...) (*env)->name(env, ##args)
3.1 Javaの配列をネイティブで表示する
Java側でchar配列を確保して、ネイティブ関数に渡します。
printArray(new char [] { 'H', 'e', 'l', 'l', 'o' });
ネイティブ側ではjcharArray型でchar配列を受け取ります。
JNI_DEFINE_METHOD(void, printArray, jcharArray array) { jboolean isCopy; jchar *p = JNI_CALL(GetCharArrayElements, array, &isCopy); int i, size = JNI_CALL(GetArrayLength, array); if (!isCopy) { JNI_LOG("GetCharArrayElements error"); return; } for (i = 0; i < size; i++) JNI_LOG("array[%d] = %c(%02x)\n", i, p[i], p[i]); JNI_CALL(ReleaseCharArrayElements, array, p, 0); }
GetCharArrayElementsでjcharArrayにアクセスする為のjcharポインタを取得します。
jbooleanのisCopyに成功したかどうかが格納されます。
GetArrayLengthで配列の長さを取得します。
ReleaseCharArrayElementsはGetCharArrayElementsで確保したjcharポインタを解放します。
3.2 ネイティブで確保した配列をJavaで表示する
ネイティブで確保した配列をJava側で表示します。
char [] array = getArray(); for (int i = 0; i < array.length; ++i) Log.d(gTAG, "array[" + i + "] = " + array[i]);
ネイティブ側でJavaのchar配列を確保し、"world"という内容にしてからJava側へ返します。
JNI_DEFINE_METHOD(jcharArray, getArray) { jcharArray array = JNI_CALL(NewCharArray, 6); jboolean isCopy; jchar *p = JNI_CALL(GetCharArrayElements, array, &isCopy); if (!isCopy) { JNI_LOG("GetCharArrayElements error"); return; } p[0] = 'w'; p[1] = 'o'; p[2] = 'r'; p[3] = 'l'; p[4] = 'd'; p[5] = '\0'; JNI_CALL(ReleaseCharArrayElements, array, p, 0); return array; }
NewCharArrayでJavaのchar配列を確保します。p[0] = 'w'を実行されてもarrayには反映されません。ReleaseCharArrayElementsが実行されてから、変更内容が反映されます。
3.3 実行結果
logcatに以下のログが出力されます。
06-10 20:56:49.902: I/hello(17184): Hello, World 06-10 20:56:49.902: I/hello(17184): array[0] = H(48) 06-10 20:56:49.902: I/hello(17184): array[1] = e(65) 06-10 20:56:49.902: I/hello(17184): array[2] = l(6c) 06-10 20:56:49.902: I/hello(17184): array[3] = l(6c) 06-10 20:56:49.902: I/hello(17184): array[4] = o(6f) 06-10 20:56:49.902: D/hello(17184): array[0] = w 06-10 20:56:49.902: D/hello(17184): array[1] = o 06-10 20:56:49.902: D/hello(17184): array[2] = r 06-10 20:56:49.902: D/hello(17184): array[3] = l 06-10 20:56:49.902: D/hello(17184): array[4] = d 06-10 20:56:49.902: D/hello(17184): array[5] = ��