1. 序章

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

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

ただし、特定のアーキテクチャ用にネイティブにコンパイルされたコードを実際に使用する必要がある場合もあります

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

  • 一部のハードウェアを処理する必要性
  • 非常に要求の厳しいプロセスのパフォーマンスの向上
  • Javaで書き直す代わりに再利用したい既存のライブラリ。

これを実現するために、JDKは、JVMで実行されているバイトコードとネイティブコード(通常はCまたはC ++で記述されている)の間にブリッジを導入します。

このツールは、JavaNativeInterfaceと呼ばれます。 この記事では、それを使ってコードを書く方法を見ていきます。

2. 使い方

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

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

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

  • 静的ライブラリ–すべてのライブラリバイナリは、リンクプロセス中に実行可能ファイルの一部として含まれます。 したがって、libsはもう必要ありませんが、実行可能ファイルのサイズが大きくなります。
  • 共有ライブラリ–最終的な実行可能ファイルには、コード自体ではなく、ライブラリへの参照のみが含まれます。 実行可能ファイルを実行する環境が、プログラムで使用されるライブラリのすべてのファイルにアクセスできる必要があります。

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

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

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

private native void aNativeMethod();

が別のJavaクラスによって実装されるのではなく、分離されたネイティブ共有ライブラリに実装されるという主な違いがあります。

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

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

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

  • Javaコード–私たちのクラス。 それらには、少なくとも1つのnativeメソッドが含まれます。
  • ネイティブコード–通常はCまたはC++でコード化されたネイティブメソッドの実際のロジック。
  • JNIヘッダーファイル– C / C ++用のこのヘッダーファイル( include / jni.h をJDKディレクトリに)には、ネイティブプログラムで使用できるJNI要素のすべての定義が含まれています。
  • C / C ++コンパイラ–プラットフォーム用のネイティブ共有ライブラリを生成できる限り、GCC、Clang、Visual Studio、またはその他の好きなものから選択できます。

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

Java要素:

  • 「native」キーワード–すでに説明したように、nativeとしてマークされたメソッドはすべて、ネイティブの共有ライブラリに実装する必要があります。
  • System.loadLibrary(String libname) –共有ライブラリをファイルシステムからメモリにロードし、エクスポートされた関数をJavaコードで使用できるようにする静的メソッド。

C / C ++要素(それらの多くは jni.h 内で定義されています)

  • JNIEXPORT-関数を共有ライブラリにエクスポート可能としてマークし、関数テーブルに含まれるようにします。これにより、JNIは関数を見つけることができます。
  • JNICALL – JNIEXPORT と組み合わせることで、JNIフレームワークでメソッドを使用できるようになります
  • JNIEnv –ネイティブコードを使用してJava要素にアクセスできるメソッドを含む構造
  • JavaVM –実行中のJVMを操作(または新しいJVMを開始)できる構造で、スレッドを追加したり、破壊したりします…

3. Hello World JNI

次に、JNIが実際にどのように機能するかを見てみましょう。

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

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

  • Ubuntu Linux –ターミナルでコマンド「sudoapt-getinstallbuild-essential」を実行します
  • Windows – MinGWをインストールします
  • MacOS –ターミナルでコマンド“ g ++” を実行します。まだ存在しない場合は、インストールされます。

3.1. Javaクラスの作成

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

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

package com.baeldung.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();
}

ご覧のとおり、共有ライブラリを静的ブロックにロードします。 これにより、必要なときに必要な場所からいつでも使用できるようになります。

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

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

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

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

まず、メソッドの定義を作成するには、Javaコンパイラの-hフラグを使用する必要があります。

javac -h . HelloWorldJNI.java

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

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello
  (JNIEnv *, jobject);

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

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

ここで、 sayHello 関数を実装するために、新しい.cppファイルを作成する必要があります。 ここで、「HelloWorld」をコンソールに出力するアクションを実行します。

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

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

3.3. コンパイルとリンク

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

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

そのためには、G++コンパイラを使用する必要があります。JavaJDKインストールからのJNIヘッダーを含めることを忘れないでください。

Ubuntuバージョン:

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

Windows版:

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

MacOSバージョン;

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

プラットフォーム用にコンパイルされたコードをファイルcom_baeldung_jni_HelloWorldJNI.oにまとめたら、それを新しい共有ライブラリに含める必要があります。 名前を付けることにしたものは何でも、メソッドSystem.loadLibraryに渡された引数です。

「ネイティブ」という名前を付け、Javaコードを実行するときにロードします。

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

Ubuntuバージョン:

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

Windows版:

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

MacOSバージョン:

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

以上です!

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

ただし、生成したライブラリを含むディレクトリへのフルパスを追加する必要があります。このようにして、Javaはネイティブライブラリを探す場所を認識します。

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

コンソール出力:

Hello from C++ !!

4. 高度なJNI機能の使用

こんにちはと言うのはいいですが、あまり役に立ちません。 通常、JavaとC ++コード間でデータを交換し、プログラムでこのデータを管理したいと思います。

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_baeldung_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_baeldung_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 を使用すると、この場合、実装を気にせずにJava StringsをC++コードに渡して元に戻すことができます。

JavaタイプとCJNIタイプの同等性をOracleの公式ドキュメントで確認できます。

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

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

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

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

package com.baeldung.jni;

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

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

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

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

JNIEXPORT jobject JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser
  (JNIEnv *env, jobject thisObject, jstring name, jdouble balance) {
  
    // Create the object of the class UserData
    jclass userDataClass = env->FindClass("com/baeldung/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_baeldung_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.baeldung.jni.UserDataのインスタンスを作成しています。 インスタンスを取得すると、Javaリフレクションと同様の方法ですべてのプロパティとメソッドを操作できます。

JNIEnv の他のすべてのメソッドは、Oracleの公式ドキュメントで確認できます。

4. JNIを使用するデメリット

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

主な欠点は、基盤となるプラットフォームへの依存です。 本質的に、Javaの「1回だけ書き込み、どこでも実行」機能を失います。 これは、サポートするプラットフォームとアーキテクチャの新しい組み合わせごとに新しいライブラリを構築する必要があることを意味します。 Windows、Linux、Android、MacOSをサポートした場合、これがビルドプロセスに与える可能性のある影響を想像してみてください…

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

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

5. 結論

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

これは、要求の厳しいプロセスをスピードアップする必要がある場合に役立ちます。 また、デバイスを管理するライブラリを使用する必要がある場合など、他に選択肢がない場合。

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

そのため、通常は、Java代替手段がない場合にのみJNIを使用することをお勧めします。

いつものように、この記事のコードはGitHubから入手できます。