JNIメモ1

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++のプログラムを呼ぶ

以下のステップで実行する。

  1. nativeメソッドを定義し、javacでJavaプログラムをコンパイル
  2. javahコマンドでヘッダーファイル生成
  3. c/c++の実装部分を書く
  4. ダイナミックライブラリ(Shared library)を生成
  5. JavaからSystem.loadLibraryでダイナミックライブラリをロード
  6. 実行

まずは、nativeメソッド定義する。ここでは、引数に渡した文字列を出力する単純なサンプルを想定している。

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");
    }    
}

続いてコンパイルする。

$ javac src/sample/JNITest.java -d build/classes/

クラスファイルからjavahでヘッダファイルを生成する。

$ javah -classpath build/classes sample.JNITest

これにより生成されたファイルは以下のようになる。

/* 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_を参照)

#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);
}

ダイナミックライブラリを作成する。

$ g++ -dynamiclib \
  -I$(/usr/libexec/java_home)/include/darwin \
  -I$(/usr/libexec/java_home)/include \
  -o libsample_JNITest.dylib \
  sample_JNITest.cc

最後にJavaを実行して正常にロードされているか確認。

$ java -classpath build/classes -Djava.library.path=lib sample.JNITest
Call method via JNI

C/C++からJavaのプログラムを呼ぶ

続いて今度はC/C++からJavaのプログラムを呼び出す例を見てみる。基本的な流れは以下のようになるだろうか。

  1. VM作成
  2. クラス探索
  3. メソッドの呼び出し
  4. VM破棄

以下実際のソースで見てみる。ここでは、java.lang.Mathクラスのrandomメソッドを実行してみる。

#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;
}

続いてコンパイル。

$ 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の環境がインストールされていないとダイアログが表示されて実行することができない。取り急ぎの実行に際しては、以下のリンクから過去のバージョンをダウンロードすることができる。

https://support.apple.com/kb/DL1572?viewlocale=en_US&locale=ja_JP

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

その他以下のようなタイプエイリアスやマクロが定義されている。

typedef jint            jsize;

/*
 * jboolean constants
 */

#define JNI_FALSE 0
#define JNI_TRUE 1

参照型

以下のような構造になっている。

// 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からの抜粋。

/* 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プログラムを実行するのには以下のようになる。

public class Hello {
  public static void main(String[] args) {
    System.out.println("Hello " + args[0]);
  }
}
// 例ではエラーチェックは省略
  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
Java仮想マシン仕様 (The Java series)
ティム リンドホルム フランク イェリン
ピアソンエデュケーション
売り上げランキング: 462,364

byebyehaikikyou

日記やIT系関連のネタ、WordPressに関することなど様々な事柄を書き付けた雑記です。ITエンジニア経験があるのでプログラミングに関することなどが多いです。

シェアする

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

コメントする

Translate »