AndroidでネイティブからJavaを呼び出す

AndroidでネイティブコードからJavaメソッドを呼び出す方法についてまとめました。

com.example.android.MainActivityというJavaコードと、jni/hello/hello.cというネイティブコードを使います。

先にMainActivityからネイティブ関数を呼び出した後、ネイティブコードからJavaメソッドを呼び出す形になります。

Javaからネイティブを呼び出す場合はこちらを参照してください。

 

1 ネイティブコードからJavaメソッドを呼ぶ

以下の手順でJavaメソッドを呼び出します。

1. FindClassでクラスを取得する
2. GetMethodIDでメソッドIDを取得する
3. Call[戻り値の型]Methodでメソッドを呼び出す

JNIのインターフェースはJNIEnvのメンバ関数ポインタ経由で呼び出します。

(*env)->Func(env, ...)

android-17の場合は以下の場所にJNIインターフェースが定義されています。

ndk/platforms/android-17/arch-arm/usr/include/jni.h

1.1 FindClassで指定するクラス名

com.example.android.MainActivityというように、パッケージ名とクラス名を指定する必要があります。

加えて、.を/に置き換える必要があります。

(*env)->FindClass(env, "com/example/android/MainActivity");

1.2 GetMethodIDにメソッドのシグネチャを指定

GetMethodIDはクラスとメソッド名だけでなく、メソッドのシグネチャを指定する必要があります。

(*env)->GetMethodID(env, [FindClassで取得したクラス],
                    "helloWorld", "()V");

上記の"()V"がシグネチャです。

シグネチャはメソッドの引数と戻り値から規則的に生成されるものです。

 

この規則を覚えていれば問題ないのですが、忘れてしまった場合(というか覚えるのがしんどい)にjavapコマンドを用いることで、メソッドのシグネチャを確認できます

$ javap -s -classpath bin/classes com.example.android.MainActivity
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Compiled from "MainActivity.java"
public class com.example.android.MainActivity extends
android.app.Activity {
  public static final java.lang.String gTAG;
    Signature: Ljava/lang/String;
  public com.example.android.MainActivity();
    Signature: ()V

  public void onCreate(android.os.Bundle);
    Signature: (Landroid/os/Bundle;)V

  public void helloWorld();
    Signature: ()V
}

1.3 Call[戻り値の型]Methodについて

メソッド名とシグネチャからメソッドIDを取得したので、すでに戻り値がメソッドIDで判別できそうですが、Call[戻り値]Methodoで戻り値の型を明示した関数呼び出しが必要になります。

以下はその一部です。

void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
void (*CallVoidMethodV)(JNIEnv*, jobject, jmethodID, va_list);
void (*CallVoidMethodA)(JNIEnv*, jobject, jmethodID, jvalue*);

Method、MethodV、MethodAはJavaメソッドへの引数の渡し方が異なるだけです。

1.4 ネイティブコードからJavaメソッドを呼び出すコード

Java側のコードは以下の通りです。

package com.example.android;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity
{
  public static final String gTAG = "hello";

  private native void callFromNative();

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

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    callFromNative();
  }

  public void helloWorld()
  {
    Log.d(gTAG, "Hello, World");
  }
}

ネイティブ側のコードは以下の通りです。

callFromNative関数からhelloWorldメソッドを呼び出します。

#include <stdio.h>
#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_GET_ENV() env
#define JNI_GET_OBJECT() object
#define JNI_CALL(name, args...) (*env)->name(env, ##args)
#define JNI_CALL_METHOD(name, id, args...) \
  (*env)->name(env, object, id, ##args)
#define JNI_LOG(args...) \
  __android_log_print(ANDROID_LOG_INFO, "hello", ##args)

#define JAVA_CLASS_NAME "com/example/android/MainActivity"

JNI_DEFINE_METHOD(void, callFromNative)
{
  jclass c = JNI_CALL(FindClass, JAVA_CLASS_NAME);
  jmethodID id = JNI_CALL(GetMethodID, c, "helloWorld", "()V");
  if (id == 0) {
    JNI_LOG("cannot find method helloworld ()V");
    return;
  }
  JNI_CALL_METHOD(CallVoidMethod, id);
}

2 ネイティブスレッドからJavaメソッドを呼ぶ

Javaのスレッドからではなく、ネイティブプログラムを開始した場合、以下の手順でJavaメソッドを呼び出します。

1. JavaVMを作成する
2. AttachCurrentThreadでJavaVMにアタッチする
3. FindClassでクラスを取得する
4. GetMethodIDでメソッドIDを取得する
5. Call[戻り値の型]Methodでメソッドを呼び出す
6. DetachCurrentThreadでJavaVMをデタッチする
7. JavaVMを破棄する

Androidの場合、MainActivity経由でネイティブ関数を呼ぶので、1と2の処理が必要ありません。

ところがMainActivity経由呼ばれたネイティブ関数内で別のネイティブスレッドを作成した場合は異なり、2のAttachCurrentThreadが必要になります

 

AttachCurrentThreadはJavaVM構造体のメンバ関数ポインタとして定義されています。

typedef const struct JNIInvokeInterface* JavaVM;
<snip>
struct JNIInvokeInterface {
<snip>
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
<snip>
};

JavaVMはJNIEnv構造体のメンバ関数ポインタGetJavaVMから取得できます。

jint        (*GetJavaVM)(JNIEnv*, JavaVM**);

さらにjobjectをネイティブスレッドから参照できるようにJNIEnv構造体のメンバ関数ポインタNewGlobalRefを使用します

NewGlobalRefを用いることでjobjectがグローバル参照可能になります。NewGlobalRefしたjobjectはDeleteGlobalRefで解放されます。

jobject     (*NewGlobalRef)(JNIEnv*, jobject);
void        (*DeleteGlobalRef)(JNIEnv*, jobject);

以上を踏まえ、以下の手順でネイティブスレッドからJavaメソッドを呼び出します。

1. Javaからネイティブ関数を呼び出す
2. ネイティブ関数でFindClassとGetMethodIDで取得したメソッドIDを
   外部変数に保存する
3. ネイティブ関数でNewGlobalRefしたjobjectを外部変数に保存する
4. ネイティブ関数でGetJavaVMしたJavaVMを外部変数に保存する
5. pthread_createでネイティブスレッドを生成する
6. 生成されたネイティブスレッドにて、外部変数のJavaVMを
   AttachCurrentThreadして、JNIEnvを取得する
7. 生成されたネイティブスレッドにて、外部変数のメソッドIDと
   jobjectからJavaメソッドを呼び出す
8. 生成されたネイティブスレッドにて、NewGlobalRefしたjobjectを
   DeleteGlobalRefで削除する
9. 生成されたネイティブスレッドにて、AttachCurrentThreadしたJavaVM
   をDetachCurrentThreadでデタッチする

2.1 サンプルプログラム

Javaのコードは以下の通りです。

package com.example.android;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity
{
  public static final String gTAG = "hello";

  private native void callFromNative();
  private native void callFromNativeThread();

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

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    callFromNative();
    callFromNativeThread();
  }

  public void helloWorld()
  {
    Log.d(gTAG, "Hello, World");
  }
}

ネイティブのコードは以下の通りです。

#include <stdio.h>
#include <com_example_android_MainActivity.h>
#include <android/log.h>
#include <pthread.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_GET_ENV() env
#define JNI_GET_OBJECT() object
#define JNI_CALL(name, args...) (*env)->name(env, ##args)
#define JNI_CALL_METHOD(name, id, args...) \
  (*env)->name(env, object, id, ##args)
#define JNI_ATTACH(env) \
  (*gJavaVM)->AttachCurrentThread(gJavaVM, &env, NULL);
#define JNI_DETACH() \
  (*gJavaVM)->DetachCurrentThread(gJavaVM)
#define JNI_LOG(args...) \
  __android_log_print(ANDROID_LOG_INFO, "hello", ##args)

#define JAVA_CLASS_NAME "com/example/android/MainActivity"

static JavaVM *gJavaVM;
static jobject gObject;
static jmethodID gMethodID;
static pthread_t gThread;

JNI_DEFINE_METHOD(void, callFromNative)
{
  jclass c = JNI_CALL(FindClass, JAVA_CLASS_NAME);
  jmethodID id = JNI_CALL(GetMethodID, c, "helloWorld", "()V");
  if (id == 0) {
    JNI_LOG("cannot find method helloworld ()V");
    return;
  }
  JNI_CALL_METHOD(CallVoidMethod, id);
}

static void *runNativeThread(void *data)
{
  JNIEnv *env;
  jobject object = gObject;

  JNI_ATTACH(env);
  JNI_CALL_METHOD(CallVoidMethod, gMethodID);
  JNI_CALL(DeleteGlobalRef, gObject);
  JNI_DETACH();
  return NULL;
}

JNI_DEFINE_METHOD(void, callFromNativeThread)
{
  jclass c;
  int retval;

  c = JNI_CALL(FindClass, JAVA_CLASS_NAME);
  gMethodID = JNI_CALL(GetMethodID, c, "helloWorld", "()V");
  gObject = JNI_CALL(NewGlobalRef, JNI_GET_OBJECT());
  JNI_CALL(GetJavaVM, &gJavaVM);

  retval = pthread_create(&gThread, NULL, runNativeThread, NULL);
  if (retval < 0)
    JNI_LOG("pthread_create error");
}

実行するとlogcatで以下の出力が得られます。

06-11 00:37:25.315: D/hello(7555): Hello, World
06-11 00:37:25.319: D/hello(7555): Hello, World
ダウンロード
AndroidでネイティブからJavaを呼び出すサンプルプログラム
AndroidJniFromNative.tar.gz
GNU tar 37.1 KB