JNIガイド(Java Native Interface)

JNI(Java Native Interface)ガイド

1. 前書き

ご存じのとおり、Javaの主な長所の1つは移植性です。つまり、コードを記述してコンパイルすると、このプロセスの結果はプラットフォームに依存しないバイトコードになります。

簡単に言えば、これはJava仮想マシンを実行できる任意のマシンまたはデバイスで実行でき、期待どおりにシームレスに機能します。

ただし、we do actually need to use code that’s natively-compiled for a specific architectureの場合もあります。

ネイティブコードを使用する必要がある理由はいくつかあります。

  • いくつかのハードウェアを処理する必要性

  • 非常に要求の厳しいプロセスのパフォーマンスの改善

  • Javaで書き直すのではなく、再利用したい既存のライブラリ。

To achieve this, the JDK introduces a bridge between the bytecode running in our JVM and the native code(通常はCまたはC ++で記述されます)。

このツールはJava Native Interfaceと呼ばれます。 この記事では、それを使用してコードを作成する方法を説明します。

2. 使い方

2.1. ネイティブメソッド:JVMはコンパイルされたコードに適合

Javaは、メソッドの実装がネイティブコードによって提供されることを示すために使用されるnativeキーワードを提供します。

通常、ネイティブ実行可能プログラムを作成する場合、静的ライブラリまたは共有ライブラリを使用することを選択できます。

  • 静的ライブラリ-すべてのライブラリバイナリは、リンクプロセス中に実行可能ファイルの一部として含まれます。 したがって、ライブラリはもう必要ありませんが、実行可能ファイルのサイズが大きくなります。

  • 共有ライブラリ-最終的な実行可能ファイルには、コードへの参照ではなく、ライブラリへの参照のみが含まれます。 実行可能ファイルを実行する環境が、プログラムで使用されるライブラリのすべてのファイルにアクセスできることが必要です。

バイトコードとネイティブにコンパイルされたコードを同じバイナリファイルに混在させることはできないため、後者はJNIにとって意味のあることです。

したがって、共有ライブラリは、クラスの一部ではなく、ネイティブコードを.so/.dll/.dylibファイル内に個別に保持します(使用しているオペレーティングシステムによって異なります)。

nativeキーワードは、メソッドを一種の抽象メソッドに変換します。

private native void aNativeMethod();

主な違いはinstead of being implemented by another Java class, it will be implemented in a separated native shared libraryです。

すべてのネイティブメソッドの実装へのメモリ内のポインタを持つテーブルが構築され、Javaコードから呼び出すことができます。

2.2. 必要なコンポーネント

考慮する必要のある主要なコンポーネントについて簡単に説明します。 これらについては、この記事の後半で詳しく説明します

  • Javaコード-クラス。 それらには、少なくとも1つのnativeメソッドが含まれます。

  • ネイティブコード–通常はCまたはC ++でコーディングされたネイティブメソッドの実際のロジック。

  • JNIヘッダーファイル– C / C ++用のこのヘッダーファイル(JDKディレクトリへのinclude/jni.h)には、ネイティブプログラムで使用できるJNI要素のすべての定義が含まれています。

  • C/C++ Compiler – we can choose between GCC, Clang, Visual Studio, or any other we like as far as it’s able to generate a native shared library for our platform.

2.3. コード内のJNI要素(JavaおよびC / C ++)

Java要素:

  • 「native」キーワード–すでに説明したように、nativeとしてマークされたメソッドはすべて、ネイティブの共有ライブラリに実装する必要があります。

  • System.loadLibrary(String libname) –共有ライブラリをファイルシステムからメモリにロードし、エクスポートされた関数をJavaコードで使用できるようにする静的メソッド。

C/C++ elements (many of them defined within jni.h)

  • JNIEXPORT-関数を共有ライブラリにエクスポート可能としてマークし、関数テーブルに含まれます。したがって、JNIはそれを見つけることができます

  • JNICALL –JNIEXPORTと組み合わせると、JNIフレームワークでメソッドを使用できるようになります

  • JNIEnv –ネイティブコードを使用してJava要素にアクセスできるメソッドを含む構造

  • JavaVM –実行中のJVMを操作(または新しいJVMを起動)して、スレッドを追加したり、破壊したりできる構造

3. Hello World JNI

次に、let’s look at how JNI works in practice.

このチュートリアルでは、Cを母国語として使用し、Gをコンパイラーとリンカーとして使用します。

他の好みのコンパイラを使用できますが、Ubuntu、Windows、MacOSにG ++をインストールする方法は次のとおりです。

  • Ubuntu Linux –ターミナルでコマンド“sudo apt-get install build-essential”を実行します

  • Windows –Install MinGW

  • MacOS –ターミナルでコマンド“g++”を実行します。まだ存在しない場合は、インストールされます。

3.1. Javaクラスの作成

古典的な「HelloWorld」を実装して、最初のJNIプログラムの作成を始めましょう。

まず、作業を実行するネイティブメソッドを含む次のJavaクラスを作成します。

package com.example.jni;

public class HelloWorldJNI {

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

    public static void main(String[] args) {
        new HelloWorldJNI().sayHello();
    }

    // Declare a native method sayHello() that receives no arguments and returns void
    private native void sayHello();
}

ご覧のとおり、we load the shared library in a static blockです。 これにより、必要なときにいつでも必要なときに準備できるようになります。

または、この簡単なプログラムでは、ネイティブメソッドを呼び出す直前にライブラリをロードすることもできます。これは、他の場所ではネイティブライブラリを使用していないためです。

3.2. C ++でのメソッドの実装

次に、C ++でネイティブメソッドの実装を作成する必要があります。

C ++内では、定義と実装は通常、それぞれ.hファイルと.cppファイルに格納されます。

まず、to create the definition of the method, we have to use the -h flag of the Java compiler

javac -h . HelloWorldJNI.java

これにより、クラスに含まれるすべてのネイティブメソッドがパラメーターとして渡されたcom_example_jni_HelloWorldJNI.hファイルが生成されます。この場合は、次の1つだけです。

JNIEXPORT void JNICALL Java_com_example_jni_HelloWorldJNI_sayHello
  (JNIEnv *, jobject);

ご覧のとおり、関数名は完全修飾パッケージ、クラス、およびメソッド名を使用して自動的に生成されます。

また、興味深いことに、関数に2つのパラメーターが渡されています。現在のJNIEnv;へのポインタと、メソッドがアタッチされているJavaオブジェクト、HelloWorldJNIクラスのインスタンス。

ここで、sayHello関数を実装するための新しい.cppファイルを作成する必要があります。 This is where we’ll perform actions that print “Hello World” to console.

.cppファイルに、ヘッダーを含む.hファイルと同じ名前を付け、次のコードを追加してネイティブ関数を実装します。

JNIEXPORT void JNICALL Java_com_example_jni_HelloWorldJNI_sayHello
  (JNIEnv* env, jobject thisObject) {
    std::cout << "Hello from C++ !!" << std::endl;
}

3.3. コンパイルとリンク

この時点で、必要なすべてのパーツが配置され、それらが接続されています。

C ++コードから共有ライブラリを構築して実行する必要があります!

そのためには、G ++コンパイラnot forgetting to include the JNI headers from our Java JDK installationを使用する必要があります。

Ubuntuバージョン:

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o

Windows版:

g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o

MacOSバージョン。

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin com_example_jni_HelloWorldJNI.cpp -o com_example_jni_HelloWorldJNI.o

プラットフォーム用にコードをファイルcom_example_jni_HelloWorldJNI.oにコンパイルしたら、それを新しい共有ライブラリに含める必要があります。 Whatever we decide to name it is the argument passed into the method System.loadLibrary.

これを「ネイティブ」と名付け、Javaコードの実行時にロードします。

次に、GリンカーはCオブジェクトファイルをブリッジライブラリにリンクします。

Ubuntuバージョン:

g++ -shared -fPIC -o libnative.so com_example_jni_HelloWorldJNI.o -lc

Windows版:

g++ -shared -o native.dll com_example_jni_HelloWorldJNI.o -Wl,--add-stdcall-alias

MacOSバージョン:

g++ -dynamiclib -o libnative.dylib com_example_jni_HelloWorldJNI.o -lc

以上です!

これで、コマンドラインからプログラムを実行できます。

ただし、we need to add the full path to the directory containing the library we’ve just generated.このようにして、Javaはネイティブライブラリを探す場所を認識します。

java -cp . -Djava.library.path=/NATIVE_SHARED_LIB_FOLDER com.example.jni.HelloWorldJNI

コンソール出力:

Hello from C++ !!

4. 高度なJNI機能の使用

挨拶するのは良いことですが、あまり役に立ちません。 Usually, we would like to exchange data between Java and C++ code and manage this data in our program.

4.1. ネイティブメソッドにパラメーターを追加する

ネイティブメソッドにいくつかのパラメータを追加します。 パラメータと異なるタイプの戻り値を使用して、2つのネイティブメソッドを使用してExampleParametersJNIという新しいクラスを作成しましょう。

private native long sumIntegers(int first, int second);

private native String sayHelloToMe(String name, boolean isFemale);

次に、手順を繰り返して、以前と同様に「javac -h」を使用して新しい.hファイルを作成します。

次に、新しいC ++メソッドの実装を使用して、対応する.cppファイルを作成します。

...
JNIEXPORT jlong JNICALL Java_com_example_jni_ExampleParametersJNI_sumIntegers
  (JNIEnv* env, jobject thisObject, jint first, jint second) {
    std::cout << "C++: The numbers received are : " << first << " and " << second << std::endl;
    return (long)first + (long)second;
}
JNIEXPORT jstring JNICALL Java_com_example_jni_ExampleParametersJNI_sayHelloToMe
  (JNIEnv* env, jobject thisObject, jstring name, jboolean isFemale) {
    const char* nameCharPointer = env->GetStringUTFChars(name, NULL);
    std::string title;
    if(isFemale) {
        title = "Ms. ";
    }
    else {
        title = "Mr. ";
    }

    std::string fullName = title + nameCharPointer;
    return env->NewStringUTF(fullName.c_str());
}
...

タイプJNIEnvのポインタ*envを使用して、JNI環境インスタンスによって提供されるメソッドにアクセスしました。

JNIEnvを使用すると、この場合、JavaStringsをC ++コードに渡して、実装について心配することなく元に戻すことができます。

JavaタイプとCJNIタイプの同等性をOracle official documentation.にチェックできます

コードをテストするには、前のHelloWorldの例のすべてのコンパイル手順を繰り返す必要があります。

4.2. オブジェクトの使用とネイティブコードからのJavaメソッドの呼び出し

この最後の例では、Javaオブジェクトを操作してネイティブC ++コードにする方法を見ていきます。

ユーザー情報を保存するために使用する新しいクラスUserDataの作成を開始します。

package com.example.jni;

public class UserData {

    public String name;
    public double balance;

    public String getUserInfo() {
        return "[name]=" + name + ", [balance]=" + balance;
    }
}

次に、UserData型のオブジェクトを管理するいくつかのネイティブメソッドを使用して、ExampleObjectsJNIという別のJavaクラスを作成します。

...
public native UserData createUser(String name, double balance);

public native String printUserData(UserData user);

もう一度、.hヘッダーを作成してから、新しい.cppファイルにネイティブメソッドのC ++実装を作成しましょう。

JNIEXPORT jobject JNICALL Java_com_example_jni_ExampleObjectsJNI_createUser
  (JNIEnv *env, jobject thisObject, jstring name, jdouble balance) {

    // Create the object of the class UserData
    jclass userDataClass = env->FindClass("com/example/jni/UserData");
    jobject newUserData = env->AllocObject(userDataClass);

    // Get the UserData fields to be set
    jfieldID nameField = env->GetFieldID(userDataClass , "name", "Ljava/lang/String;");
    jfieldID balanceField = env->GetFieldID(userDataClass , "balance", "D");

    env->SetObjectField(newUserData, nameField, name);
    env->SetDoubleField(newUserData, balanceField, balance);

    return newUserData;
}

JNIEXPORT jstring JNICALL Java_com_example_jni_ExampleObjectsJNI_printUserData
  (JNIEnv *env, jobject thisObject, jobject userData) {

    // Find the id of the Java method to be called
    jclass userDataClass=env->GetObjectClass(userData);
    jmethodID methodId=env->GetMethodID(userDataClass, "getUserInfo", "()Ljava/lang/String;");

    jstring result = (jstring)env->CallObjectMethod(userData, methodId);
    return result;
}

ここでも、JNIEnv *envポインターを使用して、実行中のJVMから必要なクラス、オブジェクト、フィールド、およびメソッドにアクセスしています。

通常、Javaクラスにアクセスするには完全なクラス名、またはオブジェクトメソッドにアクセスするには正しいメソッド名と署名を提供するだけです。

ネイティブコードでクラスcom.example.jni.UserDataのインスタンスを作成しています。 Once we have the instance, we can manipulate all its properties and methods in a way similar to Java reflection.

JNIEnvの他のすべてのメソッドをOracle official documentationにチェックインできます。

4. JNIを使​​用する場合の欠点

JNIブリッジには落とし穴があります。

主な欠点は、基盤となるプラットフォームへの依存です。 Javaのwe essentially lose the “write once, run anywhere”機能。 つまり、サポートするプラットフォームとアーキテクチャの新しい組み合わせごとに、新しいライブラリを構築する必要があります。 Windows、Linux、Android、MacOSをサポートした場合、これがビルドプロセスに与える影響を想像してください…

JNIは、プログラムに複雑さの層を追加するだけではありません。 JVMで実行されているコードとネイティブコードの間のIt also adds a costly layer of communication:マーシャリング/アンマーシャリングプロセスで、JavaとC ++の間で双方向で交換されるデータを変換する必要があります。

タイプ間で直接変換すらできない場合もあるため、同等のものを作成する必要があります。

5. 結論

特定のプラットフォーム向けにコードをコンパイルすると(通常)、バイトコードを実行するよりも高速になります。

これにより、要求の厳しいプロセスを高速化する必要がある場合に役立ちます。 また、デバイスを管理するライブラリを使用する必要がある場合など、他に選択肢がない場合もあります。

ただし、サポートするプラットフォームごとに追加のコードを維持する必要があるため、これには代償が伴います。

そのため、通常はonly use JNI in the cases where there’s no Java alternativeを使用することをお勧めします。

いつものように、この記事のコードはover on GitHubで利用できます。