JNIの仕組みについて
前回でJVMのプロセス情報がファイルシステム上に格納されていて、それを解析することでJpsは色々情報を出力していることが分かりました。これでJVMのプロセス情報はどこを見れば良いのかというのが分かりました。
今回は、JVMがglibcなどのネイティブなライブラリを呼び出す仕組みについて調査していこうと思います。
JNIとは?
JNIは、Java Native Interfaceの略です。これは、JavaからCなどのネイティブコードを呼び出すための仕様、ネイティブ側からJavaを呼び出す際(JNIEnvとして渡ってくる引数)の仕様となっています。Oracleの文書はこちらです。Wikipediaの文書も親切で良い感じでした。
また、同種のJavaのネイティブコードを呼ぶ仕組みとしては、JNAというものもあります。
この技術は、libffiを利用してネイティブなコードをJavaから呼び出す技術になっています。これはJavaのコードを準備するだけで手軽にネイティブコードを呼び出すことが出来て便利そうです。ただ、 com.sun.java というパッケージにあるので昔は標準機能だったようですが、今は適宜インストールして使う形式になっているようです。
さらに、panamaというプロジェクトがあってネイティブコードの呼び出しを簡単にしようとしているようです。
いろいろネイティブなコードを呼び出す手段はあるようですが、基本的にJVMの内部でのネイティブコードの呼び出しはJNIを利用しているようです。なので、今回はJNIを利用した関数呼び出しがJVM上でどのように実現されているかを見ていきます。
ライブラリの読み込み
まず、呼び出す相手となる外部のライブラリ(.so)を読み込むところから見ていきます。System.loadLibrary 関数で読むようです。ここから読んで行きます。
jdk/src/java.base/share/classes/java/lang/System.javaのloadLibraryからjdk/src/java.base/share/classes/java/lang/Runtime.javaのloadLibrary0が呼ばれます- getRuntimeがRuntimeを返すので Runtimeの
loadLibrary0が呼ばれる - /src/java.base/share/classes/java/lang/Runtime.java#L839
- getRuntimeがRuntimeを返すので Runtimeの
jdk/src/java.base/share/classes/java/lang/ClassLoader.javaのloadLibraryが呼ばれます- ファイル版と文字列版の2種類があります
- /src/java.base/share/classes/java/lang/ClassLoader.java#L2401
fromClassが null ならBootLoaderから取得を試みます。そうでないなら文字列ならFileを取得して、NativeLibrariesのloadLibraryを呼びます。既に読み込み済みかのチェックもここで実施してそうです。
jdk/src/java.base/share/classes/jdk/internal/loader/NativeLibraries.javaのloadLibraryが呼ばれます- Fileが渡されているはずなので、その関数を読みます
AccessControllerで色々チェックが走るようですが今回は上手くいくパスだけ読みます- /src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java#L118
- 次に同じクラスの別の
loadLibraryが呼ばれます - ここで、他のclassLoaderによって読まれていないかを確認しながら、NativeLibraryImplを作って返す処理になっています
loadLibraryの中で、NativeLibraryImplが作られて、その後openが呼ばれます- /src/java.base/share/classes/jdk/internal/loader/NativeLibraries.java#L326
- そして、
private static native boolean loadが呼ばれます。JNIの仕組みを読んでたと思ったら、JNIが出てきてしまいました。呼び出す側なので気にしないことにします。BootLoaderで読んでいるならここまでのフローの中で既にreturnできているはずです。
jdk/src/java.base/share/native/libjava/NativeLibraries.cのJava_jdk_internal_loader_NativeLibraries_loadが呼ばれますjdk/src/hotspot/share/prims/jvm.cppのJVM_LoadLibraryが呼ばれますjdk/src/jdk.jpackage/unix/native/common/UnixDll.cppの os::loadLibrary が呼ばれています- linuxのdlopenが呼ばれています。ライブラリのロードが実施されそうですね
JNIを使っているところを除いたら順番に読んでいくことで、実際のネイティブコードまで辿りつくことが出来ました。
ネイティブコードの実行の流れ
実際にJNIを実行してみる
ここまでで、DLLのロードするまでの流れは把握できました。次は実際に関数を呼ぶところを見ていきましょう。
せっかくなので、実際に自分でJNIを実行してみましょう。
こういうJavaのコードを書いて、private native void hello(); という部分がnative関数ということですね。
public class JniJikken {
private native void hello();
public static void main(String[] args) {
System.loadLibrary("JniJikken");
System.out.println("Hello, Java World!");
JniJikken me = new JniJikken();
me.hello();
}
}
次に、Cのソースコードを書いていきます。javac -h ./ JniJikken でヘッダファイルは生成することが出来ます。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniJikken */
#ifndef _Included_JniJikken
#define _Included_JniJikken
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JniJikken
* Method: hello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JniJikken_hello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
実装は、以下のような感じです。Hello, C World と出力する関数を実装してみます。
#include "stdio.h"
#include "JniJikken.h"
JNIEXPORT
void JNICALL Java_JniJikken_hello
(JNIEnv *env, jobject me)
{
printf("Hello, C World\n");
}
実行は以下のような感じで実行します。
$ javac -h ./ JniJikken.java
$gcc -fPIC -shared -I ~/.sdkman/candidates/java/current/include -I ~/.sdkman/candidates/java/current/include/linux JniJikken.c -o libJniJikken.so
$ java -Djava.library.path=./CHeader JniJikken
Hello, Java World!
Hello, C World無事実行できました。なんとなくJNIのやりかたは分かりました。javac -h を使ってヘッダが自動で作られるのは驚きでした。同時にJVMでもC言語側のヘッダはされていて名前などに一定のルールがありそうですね。
JVMのコード上でJNIに関係がある部分
JVMがJavaコードを実際に実行しはじめる、JavaMain から順番に読んでいくのは道のりが遠すぎるので、今回は関係がある箇所を何箇所か把握しておくに留めることにします。
- ライブラリから関数を見つけている箇所
- ネイティブの関数を実行するためのエントリ関数
ライブラリから関数を見つけている箇所
- 以下の関数が、ネイティブな関数のための事前準備を行なっている関数です。
- そして、
NativeLookup::lookupで関数を探しにいきます - つぎに、
NativeLookup::lookup_baseを実行します NativeLookup::lookup_entryを実行して関数を探しはじめます- /src/hotspot/share/prims/nativeLookup.cpp#L317
- JNI short style、JNI long style、JNI short style without os prefix/suffix、JNI long style without os prefix/suffix
- この4種類の名前で関数を探しに行きます。prefixとかsuffixはWindows環境だと変化があるようです。
- JNIを自分で試した時に自動で生成された名前はこれを意識したもののようですね。
NativeLookup::lookup_styleでos::dll_lookupなどが実行されるようです- まず、システムクラスだと、
lookup_special_nativeで専用の検索関数が準備されているようです - それで見つからない場合は、
os::dll_lookupで探しにいくようです - 通常のユーザのクラスの場合は、
ClassLoaderのインスタンスを取得して、Java側のコードを呼び出して探すようですvmClasses::ClassLoader_klassなどはマクロで色々実現されていますが、java_lang_ClassLoaderを取得するコードに展開されますvmSymbols::classloader_string_long_signature()は(Ljava/lang/ClassLoader;Ljava/lang/String;)JになりますvmSymbols::findNative_name()はfindNameになりますJavaCalls::call_staticはここでは、ClassLoader.findName()を実行することになります。
- ここまでで見つからなければ、
os::dll_lookupを使って探しにいきます
- まず、システムクラスだと、
os::dll_lookupの実装はdlsymを呼ぶだけになっています- この結果を
methodHandleのset_native_functionを使って このMethodオブジェクトに対するエントリポイントとして指定します - ここまでで、ネイティブ関数のポインタが取得できています、最後に
SignatureHandlerというABIなどを良い感じに調整してくれるハンドラーの登録を実施する必要があって以下の関数で実施します- /src/hotspot/share/interpreter/interpreterRuntime.cpp#L1451
- /src/hotspot/share/interpreter/interpreterRuntime.cpp#L1305
- なおSignatureHandlerは例えば以下のようなもので、ほとんど全部がアセンブラで書かれています
ネイティブの関数を実行するためのエントリ関数
ここまでで、Java側の Method オブジェクトに関数ポインタなどの設定は完了しています。
ここでは、それを実際に呼び出すための準備のコードを見ていこうと思います。
以下のコードがネイティブの関数を実行する際に呼びだされる関数となります。
/src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp#L791
完全にアセンブラですね。正直読むのは辛いので精読はしません。
ざっと見てみると、signature handlerが呼ばれていたり
/src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp#L940call が呼ばれているので、ここで実際に関数が呼びだされるのでしょう
/src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp#L966
この辺りで返り値を処理するための、return handlerに対する処理もありそうです
/src/hotspot/cpu/x86/templateInterpreterGenerator_x86.cpp#L970
なお、上記の関数と実際の関数は、InstanceKlass::link_class_impl -> InstanceKlass::link_methods -> Method::link_method という流れで登録されるようです。
InstanceKlass::link_class_impl はここから辿っていけます。
/src/hotspot/share/oops/instanceKlass.cpp#L787
また、Interpreter::entry_for_method を経由して、Method::link_method の中で代入が行なわれています。
これは、Interpreterを作成する際に構築されます。TemplateInterpreterGenerator::generate_all という関数の
/src/hotspot/share/interpreter/templateInterpreterGenerator.cpp#L56
このあたりで登録されます
/src/hotspot/share/interpreter/templateInterpreterGenerator.cpp#L211
まとめ
なんとなく、JNIの仕組みが分かった気がしました。
ここまで読みすすめてくると、最初の記事に書いていた分かってないうちのいくつかは分かった気がします。
- classpathの取り扱い
- バイトコードからシステムコールなどのネイティブな世界にどうやってアクセスしているのか
- JVM自体がバイトコードをどのように解釈して実行しているのか
- jpsなどでJavaの情報が収集出来る仕組みがあるがどうやっているのか
このうち以下の2つはまぁある程度理解できたと思います。
- バイトコードからシステムコールなどのネイティブな世界にどうやってアクセスしているのか
- jpsなどでJavaの情報が収集出来る仕組みがあるがどうやっているのか
残りの2つ
- classpathの取り扱い
- JVM自体がバイトコードをどのように解釈して実行しているのか
このあたりは、これからという感じですね。classpathの取り扱いを理解するには、ClassLoaderの動作などを眺めていく必要がありそうです。
また、JVMがバイトコードを解釈する仕組みの一端は今回のJNIの中で出てきたTemplateInterpreterなどが中心的な存在のようでした。が、その詳細までは追えていません。今後読みすすめていけばもう少しは理解できてくると思います。
参考になったサイト
以下のサイトがとても参考になりました。
http://hsmemo.github.io/articles/noNisy_uNv.html