Programming Diary.

ソフトウェアエンジニアの技術blog

JNIによるJavaからネイティブコードの呼び出しとProject PanamaのJava FFI概要

はじめに

JavaコードとC/C++などのネイティブコードを連携させる仕組みに、JNI(Java Native Interface)が挙げられますが、実装上の手続きが煩雑だったりパフォーマンスが良くなかったりして問題点が多いとされています。*1

そこで、Project PanamaではJavaとネイティブ間のやり取りにおける新たな枠組みとしてJEP 191: Foreign Function Interface(Java FFI)が検討されています。今回は、JNIでJavaからC++のコードを呼び出すサンプルを実装後、Java FFIの概要を確認します。

JNIでJavaからC++を呼び出すための実装手順

今回は、Java側から引数が文字列のC++の関数を呼び出し、C++側でその文字列に"Hello World" を結合して返すという例を取り上げます。

環境

実装手順の概要は以下の通りです。
1. Javaでnative修飾子を付けたメソッドを宣言する
2. Javaソースファイルをコンパイルし、classファイルを生成する
3. classファイルからCヘッダーファイルを生成する
4. C++ソースファイルを実装する
5. C++ソースファイルをコンパイルし、共有ライブラリを作成する
6. Javaで共有ライブラリを読み込み、コンパイル・実行する

Javaでnative修飾子を付けたメソッド宣言

まずはJavaでnativeメソッドを宣言します。処理の中身はC++で実装するため定義のみ記述します。

public class HelloWorldJNI {

    private native String helloWorld(String string);

}
Javaソースファイルからclassファイル生成

ここで一旦Javaソースファイルをコンパイルし、classファイルを生成します。

javac HelloWorldJNI.java
classファイルからCヘッダーファイル生成

classファイルが生成されたことを確認し、javahコマンドでCヘッダーファイルを生成します。

javah HelloWorldJNI

下記のようなヘッダーファイル(HelloWorldJNI.h)が自動生成されます。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorldJNI */

#ifndef _Included_HelloWorldJNI
#define _Included_HelloWorldJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloWorldJNI
 * Method:    helloWorld
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_HelloWorldJNI_helloWorld
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif
C++ソースファイル実装

生成されたヘッダーファイルを読み込み、C++のソースファイルを実装します。JNIEnv構造体に定義された関数でC++に渡されたJavaオブジェクトを扱うことができます。今回は、"Hello World "の末尾にJavaから渡された文字列を結合して返します。なお、GetStringUTFChars関数で取得した文字列は不要になった段階でReleaseStringUTFChars関数で解放する必要があります。

#include "HelloWorldJNI.h"
#include <cstring>

JNIEXPORT jstring JNICALL Java_HelloWorldJNI_helloWorld
  (JNIEnv *env, jobject obj, jstring string) {
      char buf[256] = "Hello World ";
      const char* src = env->GetStringUTFChars(string, 0);
      
      strcat(buf, src);
      
      env->ReleaseStringUTFChars(string, src);
      
      return env->NewStringUTF(buf);
}
C++ソースファイルから共有ライブラリ作成

C++のソースファイルをコンパイルし、共有ライブラリ(OS Xでは.dylib, Windowsでは.dll, Linuxでは.so)を作成します。下記のように、-Iオプションでjni.hおよびjni_md.hのパスを指定する必要があります。

g++ -shared -I /Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/include  -I /Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/include/darwin  HelloWorldJNI.cpp -o libHelloWorldJNI.dylib
Javaで共有ライブラリを読み込み、コンパイル・実行

Javaソースファイルにライブラリの読み込み処理(System#loadLibrary)を記述します。

public class HelloWorldJNI {
    private native String helloWorld(String string);

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

    public static void main(String... args) {
        System.out.println(new HelloWorldJNI().helloWorld("Java"));
        System.out.println(new HelloWorldJNI().helloWorld("C++"));
    }
}

再度コンパイル後、-Djava.library.pathで共有ライブラリのパスを指定し、実行します。

javac HelloWorldJNI.java
java -Djava.library.path=. HelloWorldJNI

実行結果は下記の通りです。

Hello World Java
Hello World C++

JNIの問題点

上記のJNIによるJavaからネイティブコードの呼び出しについては、次のような実装上および性能上の問題点があります。

  • C/C++のコードを書く必要がある
  • ネイティブコードにおけるメモリ管理を適切に実施する必要がある
  • ネイティブのアプリケーションに比べて一般的にパフォーマンスが著しく劣る

そこで、Project Panamaが提唱され、Javaコードとネイティブコード間の連携の新たな仕組みとしてJava FFIが検討されています。

Project PanamaのJava FFI概要

Java FFIではJDKレベルにおけるネイティブコードとの連携について検討されています。具体的にはJNR(Java Native Runtime)として実装が進められており、ネイティブコードの呼び出しやネイティブメモリの管理などについて最適化が図られています。将来的にはJSR(Java Specification Requests)として検討され、Java SE 10で導入予定です。

下記でサンプルが公開されているので、改めて詳細に確認したいと思います。

jnr · GitHub

*1:他にJNA(Java Native Access)というライブラリを使用することもできますが、これもパフォーマンスの面で問題があるとされています