Java
C
JNI

JNIでCからJavaのライブラリを呼び出す

TechTalk(社内勉強会)で話したUsing Go to call Java librariesに関連して、JNIを使ってCからJavaを呼び出す方法を解説します。

Java Native Interface(JNI)

JNIはCからJavaのライブラリを使ったり、その逆のことができる仕組みです。公式ドキュメントはJava Native Interface仕様の目次からたどれます。
この記事ではCからJavaを使う方向で、メソッドを呼び出すことにポイントを絞ってまとめています。

JVMのロードとアタッチ

JNIでは、JVMをネイティブアプリ上でロードして、Javaのコードをその上で動作させます。
JNI_CreateJavaVM()でJVMが生成できます。

#include <jni.h>

JNIEnv *env;
JavaVM *jvm;

// JVMの起動オプションなど
JavaVMInitArgs vm_args;
vm_args.version = JNI_VERSION_1_8;
JNI_GetDefaultJavaVMInitArgs(&vm_args);

// JVMをロード
JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args);

使い終わったらJNI_DestroyJavaVM()を呼んだほうが良いのかと思いきや、呼び出しAPIには「VMのアンロードはサポートされていません。」としれっと書いてあります。

確かに、アンロード後に再度ロードしようとするとエラー(JNI_EEXIST)になります。だからといって、jvm変数を引き回す必要はありません。同じプロセス内でロードしたJVMをアタッチして使いまわせます。

// 過去にロードしたJavaVMを取得
JNI_GetCreatedJavaVMs(&jvm, 1, NULL);
// アタッチしてJNIEnvを取得
(*jvm)->AttachCurrentThread(jvm, (void **)&env,NULL);

インスタンス生成と、メソッド呼び出し

メソッド呼び出しにはメソッド名とそのシグニチャ(全ての引数と戻り値の型、詳細は後述)を指定します。Javaはメソッドのオーバーロードができる言語なので、引数の型とメソッド名のセットで呼び出す処理が一意に定まります。

まず、GetMethodID()または、GetStaticMethodID()で実行する処理をしめすメソッドIDを取得します。コンストラクタだけは特別なメソッド名<init>GetMethodID()で取得します。その他はメソッドの定義にあわせて指定します。
以下の例は、自作クラスTestのスタティックメソッドhello()を実行する場合の例です。

// まずは該当するクラスを探す
jclass clazz = (*env)->FindClass(env, "Test");
// メソッドIDを取得、この例では引数なし、戻り値voidのスタティックメソッド
jmethodID id = (*env)->GetStaticMethodID(env, clazz, "hello", "()V");

次に、取得したメソッドIDを引数に取る下記のルーチンを実行します。<type>にはVoidとかObjectなどの戻り値に応じた文字列が入ります。それぞれsuffixにAとつくものは引数を配列で渡し、Vva_listで渡します。無印は可変長引数をとります。

  • インスタンス生成の場合はNewObject()
  • メソッド呼び出しの場合は、Call<type>Method()
  • スタティックメソッドの場合は、CallStatic<type>Method

先程のhello()を呼び出す場合は、こんな感じになります。

// 上記で取得したクラスとメソッド、引数を渡してメソッドを呼び出す。
(*env)->CallStaticVoidMethodA(env, clazz, id, NULL);

Javaの変数をCで扱う

メソッドを呼び出す場合、引数や戻り値としてJavaのオブジェクトが帰ってくる場合もあります。プリミティブ型はそれぞれ対応するCの型が用意されており、参照型の変数(インスタンスや配列)はjobjectという型でJVM内への参照として表現されます。これらの型はすべてjvalue共用体のメンバとなっています。
それぞれのサイズなどは、JNIの型とデータ構造に表があります。

typedef union jvalue {
    jboolean z;
    jbyte    b;
    jchar    c;
    jshort   s;
    jint     i;
    jlong    j;
    jfloat   f;
    jdouble  d;
    jobject  l;
} jvalue;

シグニチャとその確認方法

シグニチャはJavaの型を表現する方法です。下記の表に示すものが全てのパターンです。配列やメソッドは他の型の組み合わせで、完全修飾のクラスはLjava/lang/Object;のようにパッケージの区切り文字を/として指定します。

型シグニチャ Java型
Z boolean
B byte
C char
S short
I int
J long
F float
D double
Lfully-qualified-class; クラス
[type 配列
(arg-types)ret-type メソッド

実際のシグニチャはjavapコマンドの-sオプションで確認できます。
下記の例ではjnigo/TestClass.javaから生成されたclassファイルを読み込んでいます。

$ javap -s TestClass.class
Compiled from "TestClass.java"
public class TestClass {
  public boolean vboolean;
    descriptor: Z
  public byte vbyte;
    descriptor: B
...

  public static double smvdouble();
    descriptor: ()D

  public static TestClass smvclass();
    descriptor: ()LTestClass;

...

例えば、TestClassのシグニチャはLTestClass;なので、引数無しでTestClassを戻り値にとる関数は()LTestClass;となります。classファイルを直接指定する他に、javap -s java.lang.StringのようにFQCNを指定して表示することも出来ます。

例外とエラーハンドル

JNIの中で上がる例外はネイティブコード側では、能動的に確認しないと発生しているかわかりません。
ExceptionCheck()は例外の有無だけ、ExceptionOccurred()によって例外が取得できます。ExceptionDescribe()はstderrにスタックトレースを出します。

ExceptionOccurred()を利用すればtry~catch相当をCで書くことが出来ますが、とりあえずのpanicデバッグ用途であれば、下記のように処理を止めてしまうのが簡単です。

if ((*env)->ExceptionCheck(env)) {
  (*env)->ExceptionDescribe(env);
  exit(1);
}

JNI自体のエラーコードは、JNI_OKでなければ失敗です。エラーに分解能が必要な場合、以下のエラー番号でハンドル可能です。

jni.h
/*
 * possible return values for JNI functions.
 */

#define JNI_OK           0                 /* success */
#define JNI_ERR          (-1)              /* unknown error */
#define JNI_EDETACHED    (-2)              /* thread detached from the VM */
#define JNI_EVERSION     (-3)              /* JNI version error */
#define JNI_ENOMEM       (-4)              /* not enough memory */
#define JNI_EEXIST       (-5)              /* VM already created */
#define JNI_EINVAL       (-6)              /* invalid arguments */

グローバル参照とガベージコレクション

参照型変数はCで使用中であっても、Java側で参照がなくなったあとは、ガベージコレクションによって開放されてしまいます。グローバル参照の取得と開放をネイティブコード側でコントロールすることで、その挙動もコントロール可能です。
NewGlobalRef()でグローバル参照の取得、DeleteGlobalRef()で開放ができます。

// グローバル参照の取得、objは参照型の変数
jobject ref = (*env)->NewGlobalRef(env, obj);
// 作成したグローバル参照の開放
(*env)->DeleteGlobalRef(env, ref);

あとがき

JNIでCからJavaのメソッドを呼び出す方法を中心に説明しました。簡単な用途で使う分には上記の情報だけで十分ですが、本格的に活用していく場合はガベージコレクションの挙動や、パフォーマンスへの配慮など、もっとJNIを使いこなす必要がありそうです。
JVMをネイティブコードからつかえると、何か面白いことができそうですね。

参考

IBM Knowledge Center - Java Native Interface (JNI) について
Java Native Interface を使用する上でのベスト・プラクティス
juntaki/jnigo: JNI wrapper for Go