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] = ��