JNIガイド(Java Native Interface)
1前書き
ご存じのとおり、Javaの主な強みの1つは移植性です。つまり、コードを作成してコンパイルすると、このプロセスの結果はプラットフォームに依存しないバイトコードになります。
簡単に言うと、これはJava仮想マシンを実行できる任意のマシンまたはデバイスで実行でき、予想どおりシームレスに機能します。
ただし、場合によっては、** 特定のアーキテクチャ用にネイティブにコンパイルされたコードを実際に使用する必要がある場合があります。
ネイティブコードを使用する必要がある理由はいくつかあります。
-
いくつかのハードウェアを扱う必要性
非常に要求の厳しいプロセスでのパフォーマンス向上
-
書き換えるのではなく再利用したい既存のライブラリ
Javaです。
-
これを達成するために、JDKは私たちのJVMで実行されているバイトコードとネイティブコード** (通常CかCで書かれています)の間のブリッジを導入します。
このツールはJava Native Interfaceと呼ばれています。この記事では、それを使ってコードを書く方法を説明します。
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は、ネイティブの共有ライブラリに実装する必要があります。
-
System.loadLibrary(String libname)
– ロードする静的メソッド
ファイルシステムからメモリに共有ライブラリを作成し、エクスポートした関数をJavaコードで使用できるようにします。
C/C要素(それらの多くは
jni.h
内で定義されています)
-
JNIEXPORT – 共有ライブラリにエクスポート可能として関数をマークします。
関数テーブルに含まれるので、JNIはそれを見つけることができます。
** JNICALL –
JNIEXPORT
と組み合わせると、メソッドは確実に
JNIフレームワークで利用可能
** JNIEnv – ネイティブを使用できるメソッドを含む構造
Java要素にアクセスするためのコード
** JavaVM – 実行中のJVM(またはさらには
新しいスレッドを開始し、スレッドを追加したり、破棄したりします。
3ハローワールドJNI
次に、JNIが実際にどのように機能するのかを見てみましょう。
このチュートリアルでは、ネイティブ言語としてC、コンパイラとリンカとしてGを使用します。
私達は私達の好みの他のどのコンパイラーも使用することができますが、Ubuntu、Windows、そしてMacOSにGをインストールする方法は以下の通りです:
-
Ubuntu Linux – 実行コマンド
「sudo apt-get install build-essential」
端末
** Windows –
MinGWのインストール
-
MacOS – 端末でコマンド “g ++”を実行します。まだ存在しない場合は、
インストールします。
3.1. Javaクラスの作成
古典的な「Hello World」を実装することによって、私たちの最初の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
ファイルを作成する必要があります。これが「Hello World」をコンソールに表示するアクションを実行する場所です。
.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コンパイラを使用する必要があります。** Java JDKインストールから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 -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());
}
...
JNI環境インスタンスによって提供されるメソッドにアクセスするために
JNIEnv
型のポインタ
** env
を使用しました。
この場合、
JNIEnv
を使用すると、実装を気にせずにJavaの
Strings
をC ++コードに渡して取り消すことができます。
-
Java型とC JNI型の同等性はhttps://docs.oracle.com/javase/9/docs/specs/jni/types.html[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;
}
}
次に、
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__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
の方法はすべてhttps://docs.oracle.com/javase/9/docs/specs/jni/functions.html[Oracleの公式ドキュメント]で確認できます。
4. JNIを使用することのデメリット
JNIブリッジには落とし穴があります。
主な欠点は基盤となるプラットフォームへの依存です。 ** Javaの「1回書き込み、どこでも実行できる」機能は本質的に失われています。つまり、サポートしたいプラットフォームとアーキテクチャの新しい組み合わせごとに新しいライブラリを構築する必要があります。 Windows、Linux、Android、MacOSをサポートしている場合、これがビルドプロセスに及ぼす影響を想像してみてください。
JNIは私たちのプログラムに複雑さの層を追加するだけではありません。また、JVMに実行されるコードとネイティブコードとの間に通信のコストのかかるレイヤーを追加します。マーシャリング/アンマーシャリングプロセスで、JavaとC ++の間で双方向に交換されるデータを変換する必要があります。
-
型を直接変換できないこともあるので、同等のものを記述する必要があります。**
5結論
特定のプラットフォーム用にコードをコンパイルすると(通常)、バイトコードを実行するよりも速くなります。
これは、要求の厳しいプロセスをスピードアップする必要があるときに役立ちます。また、デバイスを管理するライブラリを使用する必要がある場合など、他の方法がない場合もあります。
ただし、サポートするプラットフォームごとに追加のコードを維持する必要があるため、これにはコストがかかります。
そのため、Javaの代替手段がない場合にのみJNIを使用することをお勧めします。
いつものように、この記事のコードはhttps://github.com/eugenp/tutorials/tree/master/jni[over on GitHub]から入手できます。