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