Impalaという分散クエリエンジンを使っていてImpaladが突然JVMのSIGSEGVで異常終了してしまうことがありソースを追いかけている。ソースを読んでいるとJNI経由でC++の世界とJavaの世界との連携を行なっている部分があり、JNIについて知識が乏しかったので今回JNI(Java Native Interface)に関して自分なりに調べ以下備忘録としてまとめてみる。
JNI(Java Native Interface)
Javaの世界とC/C++の世界との間をつないでくれる。JavaとC/C++のようなネイティブで実行されるプログラム間で連携するためのインターフェース仕様。JavaからC/C++で書かれたプログラムを呼び出したり、逆にC/C++からJavaのオブジェクトを呼び出したりすることが可能。
Quick Start
まずは、簡単なサンプルを動かしてみて実行イメージをつかむ。
JavaからC/C++のプログラムを呼ぶ
以下のステップで実行する。
- nativeメソッドを定義し、javacでJavaプログラムをコンパイル
- javahコマンドでヘッダーファイル生成
- c/c++の実装部分を書く
- ダイナミックライブラリ(Shared library)を生成
- JavaからSystem.loadLibraryでダイナミックライブラリをロード
- 実行
まずは、nativeメソッド定義する。ここでは、引数に渡した文字列を出力する単純なサンプルを想定している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package sample; public class JNITest { // Define native method. native void hello(String s); static { // Load native library. System.loadLibrary("sample_JNITest"); } public static void main(String[] args) { JNITest j = new JNITest(); // Call function via jni j.hello("Call method via JNI"); } } |
続いてコンパイルする。
1 |
$ javac src/sample/JNITest.java -d build/classes/ |
クラスファイルからjavahでヘッダファイルを生成する。
1 |
$ javah -classpath build/classes sample.JNITest |
これにより生成されたファイルは以下のようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class sample_JNITest */ #ifndef _Included_sample_JNITest #define _Included_sample_JNITest #ifdef __cplusplus extern "C" { #endif /* * Class: sample_JNITest * Method: hello * Signature: (Ljava/lang/String;)V */ JNIEXPORT void JNICALL Java_sample_JNITest_hello (JNIEnv *, jobject, jstring); #ifdef __cplusplus } #endif #endif |
メソッド名は、以下のような規則で構成されるらしい。
- 接頭辞Java_
- 分解された完全修飾クラス名
- 下線 (「_」) 区切り文字
- 分解されたメソッド名
- オーバーロードされたネイティブメソッドでは、2 個の下線 (「__」) に続いて分解された引数のシグニチャー
続いて実装。C++で書くと多少ポインタ周りがスッキリする。各種関数がインラインメンバ関数として定義されているため(jni.h参照のstruct JNIEnv_
を参照)
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> #include "sample_JNITest.h" JNIEXPORT void JNICALL Java_sample_JNITest_hello (JNIEnv *env, jobject o, jstring s) { const char *str = env->GetStringUTFChars(s, 0); std::cout << str << std::endl; env->ReleaseStringUTFChars(s, str); } |
ダイナミックライブラリを作成する。
1 2 3 4 5 |
$ g++ -dynamiclib \ -I$(/usr/libexec/java_home)/include/darwin \ -I$(/usr/libexec/java_home)/include \ -o libsample_JNITest.dylib \ sample_JNITest.cc |
最後にJavaを実行して正常にロードされているか確認。
1 2 |
$ java -classpath build/classes -Djava.library.path=lib sample.JNITest Call method via JNI |
C/C++からJavaのプログラムを呼ぶ
続いて今度はC/C++からJavaのプログラムを呼び出す例を見てみる。基本的な流れは以下のようになるだろうか。
- VM作成
- クラス探索
- メソッドの呼び出し
- VM破棄
以下実際のソースで見てみる。ここでは、java.lang.Mathクラスのrandomメソッドを実行してみる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#include "jni.h" #include <iostream> #include <string> using namespace std; int main(int argc, char* argv[]) { JavaVM *jvm; JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption *options = new JavaVMOption[1]; options[0].optionString = (char*)"-Djava.class.path=."; vm_args.version = JNI_VERSION_1_6; vm_args.options = options; vm_args.nOptions = 1; vm_args.ignoreUnrecognized = JNI_FALSE; if (JNI_CreateJavaVM(&jvm, (void **)&env, &vm_args) < 0) { cerr << "Failed to create JavaVM" << endl; return EXIT_FAILURE; } delete[] options; jclass cls = env->FindClass("java/lang/Math"); if (cls == NULL) { cerr << "Cannot find class java.lang.Math" << endl; return EXIT_FAILURE; } jmethodID mid = env->GetStaticMethodID(cls, "random", "()D"); if (mid == NULL) { cerr << "Cannot find method random()" << endl; return EXIT_FAILURE; } jdouble ret = env->CallStaticDoubleMethod(cls, mid, NULL); cout << ret << endl; jvm->DestroyJavaVM(); return EXIT_SUCCESS; } |
続いてコンパイル。
1 2 3 4 5 6 7 |
$ g++ -o jni_test \ -I$(/usr/libexec/java_home)/include/darwin \ -I$(/usr/libexec/java_home)/include \ -L$(/usr/libexec/java_home)/jre/lib/server \ -ljvm jni_test.cc $ LD_LIBRARY_PATH=$(/usr/libexec/java_home)/jre/lib/server ./jni_test 0.0100115 |
なお、Macで実行した場合Java SE 6の環境がインストールされていないとダイアログが表示されて実行することができない。取り急ぎの実行に際しては、以下のリンクから過去のバージョンをダウンロードすることができる。
Type Mapping
JavaとC/C++の世界ではルールも異なればデータの型も異なる。したがって両者の間で型をどのように扱うのか、あらかじめ取り決めをしてやる必要がある。jni.hを参考にすると以下のようにマッピングされていることがわかる。
プリミティブ型
Javaの型 | ネイティブの型 | 説明 | Ex) jni.h / jni_x86.h (OpenJDK9) |
boolean | jboolean | unsigned 8bit | unsigned char |
byte | jbyte | signed 8bit | signed char |
char | jchar | unsigned 16bit | unsigned short |
short | jshort | signed 16bit | short |
int | jint | signed 32bit | int |
long | jlong | signed 64bit | #if defined(_LP64) long #else long long #endif |
float | jfloat | 32bit | float |
double | jdouble | 64bit | double |
void | void | – | – |
その他以下のようなタイプエイリアスやマクロが定義されている。
1 2 3 4 5 6 7 8 |
typedef jint jsize; /* * jboolean constants */ #define JNI_FALSE 0 #define JNI_TRUE 1 |
参照型
以下のような構造になっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// jni.hより // Cの場合 struct _jobject; typedef struct _jobject *jobject; typedef jobject jclass; typedef jobject jthrowable; typedef jobject jstring; typedef jobject jarray; typedef jarray jbooleanArray; typedef jarray jbyteArray; typedef jarray jcharArray; typedef jarray jshortArray; typedef jarray jintArray; typedef jarray jlongArray; typedef jarray jfloatArray; typedef jarray jdoubleArray; typedef jarray jobjectArray; ------------------------- 以下のような構造 jobject |- jclass |- jstring |- jarray |- 各種プリミティブな配列 |- jthrowable |
Signature
JVMの型のシグネチャー表現は以下のようになっている。.class
ファイルを開くと見かける英数字や記号などのアレ。
以下jvm.h
からの抜粋。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* JVM method signatures */ #define JVM_SIGNATURE_ARRAY '[' #define JVM_SIGNATURE_BYTE 'B' #define JVM_SIGNATURE_CHAR 'C' #define JVM_SIGNATURE_CLASS 'L' #define JVM_SIGNATURE_ENDCLASS ';' #define JVM_SIGNATURE_ENUM 'E' #define JVM_SIGNATURE_FLOAT 'F' #define JVM_SIGNATURE_DOUBLE 'D' #define JVM_SIGNATURE_FUNC '(' #define JVM_SIGNATURE_ENDFUNC ')' #define JVM_SIGNATURE_INT 'I' #define JVM_SIGNATURE_LONG 'J' #define JVM_SIGNATURE_SHORT 'S' #define JVM_SIGNATURE_VOID 'V' #define JVM_SIGNATURE_BOOLEAN 'Z' |
上の表現でなんとなくわかるが、まとめると以下のようになる。
Type Signature | Javaのデータ型 |
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L fully-qualified-class; | 完全修飾指定 Ex) java.lang.String; |
[type | 配列 Ex) int[]の場合 [I |
(arg-types)ret-type | メソッドの型 Ex) void main(int argc, String[] args); (I[Ljava.lang.String;)V |
V | void |
これにより、あらためてC/C++からのJavaプログラムを実行するのには以下のようになる。
1 2 3 4 5 |
public class Hello { public static void main(String[] args) { System.out.println("Hello " + args[0]); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 例ではエラーチェックは省略 cls = env->FindClass("Hello"); // void main(String[] args) mid = env->GetStaticMethodID(cls, "main", "([Ljava/lang/String;)V"); jclass strclass = env->FindClass("java/lang/String"); jobjectArray arr = env->NewObjectArray(1, strclass, NULL); jstring s = env->NewStringUTF("DebugLife"); env->SetObjectArrayElement(arr, 0, s); env->CallStaticVoidMethod(cls, mid, arr); env->DeleteLocalRef(s); env->DeleteLocalRef(arr); env->DeleteLocalRef(strclass); env->DeleteLocalRef(cls); jvm->DestroyJavaVM(); |
参考リンク
- https://docs.oracle.com/javase/jp/8/docs/technotes/guides/jni/spec/functions.html
- http://www.ne.jp/asahi/hishidama/home/tech/java/jni.html
- https://ja.wikipedia.org/wiki/Java_Native_Interface
- https://newcircle.com/bookshelf/java_fundamentals_tutorial/_java_native_interface_jni
- https://support.apple.com/kb/DL1572?viewlocale=en_US&locale=ja_JP
- https://github.com/cloudera/Impala
JNI:Java Native Interfaceプログラミング―C/C++コードを用いたJavaアプリケーション開発 (Java books)
ピアソン・エデュケーション
売り上げランキング: 547,674
ピアソンエデュケーション
売り上げランキング: 462,364
コメント