経緯
javaの修飾子native
の存在を知ったので、実際にどのように使うのかを調べる。
native修飾子とは
cやcppで書かれた関数(ネイティブメソッド)を呼び出すことができる。
これを使うことによって、
- システムのパフォーマンスを向上。
- マシンレベル/メモリレベルの通信を実現。
- Java以外のコードも使用できる。
みたい。
外部を呼び出すことは、同時にjavaのメリットであるクロスプラットフォームが損なわれる可能性があるので「パフォーマンスの向上」で言うとその時その時で変わってきそう。
残念ながら
jdk10
以降はjavah
コマンドがjdk
から除外されてしまって使えない。
Summary
Remove the javah tool from the JDK.
...
...
Dependences
but just as users of the javah command are recommended to use javac -h, so too are users of these dependences are recommended to use instead the corresponding support provided for javac.
自分も実行しようとしてエラーが出て、そこで初めて知った。
javah
>> The operation couldn’t be completed. Unable to locate a Java Runtime that supports javah.
Please visit http://www.java.com for information on installing Java.
ただしjavac -h
とすることで同じことができるのでそれを使って進めていきます。
javah
を使って実装したい人は以下の記事で実装してる人がいたので参考に
環境
System | Version |
---|---|
OS | MacOS Monterey(12.1) |
zsh | zsh 5.8 (x86_64-apple-darwin21.0) |
jdk | 11.0.14 |
clang | 13.0.0 (clang-1300.0.29.30) |
nasm | 2.15.05 |
C
やり方
-
native
修飾子が入ったJavaコード書く - 呼び出したい関数が入ったCコードを書く
- Cのヘッダーファイル(.hファイル)を
javac
で作成 - 共有ライブラリ(DLL)を作成
- 実行
の手順でやります。
準備
以下ex
パッケージ内で作業してますが、パッケージは必須ではないです。
パッケージを含むとややこしくなるけど勉強になるので、一例として進めていきます。
Sample.java
package ex;
public class Sample {
public native void printHello();
public static void main(String[] args) {
System.loadLibrary("printHello");
Sample sample = new Sample();
sample.printHello();
}
}
-
native
修飾子をこれからCで実装する関数につけて宣言 -
System.loadLibrary()
でこれから生成する共有ライブラリをロード
共有ライブラリの名前はlibprintHello.jnilib
で生成するルールがあるため、先頭の「lib
」と拡張子の 「.jnilib
」を除いた名前を指定。
また、OS
によって共有ライブラリの名前を適宜変更する必要がある。
OS | name |
---|---|
Solaris | lib{関数名}.so |
Linux | lib{関数名}.so |
Windows | {関数名}.dll |
Mac | lib{関数名}.jnilib |
Mac
で実行するのでlib{関数名}.jnilib
とする。
hello.c
外部メソッドを記述する
#include "ex_Sample.h"
JNIEXPORT void JNICALL Java_ex_Sample_printHello(JNIEnv *env, jobject obj) {
(void) puts ("hello world!");
}
- 後ほど作るヘッダーファイルを
{package名}_{クラス名}.h
をインクルード - JNIは
Java Native Interface
の略 - 関数名は
Java_{package名}_{クラス名}_
を接頭辞につける
実行
クラスファイル生成
いつも通りコンパイル(Sample.class
)
javac Sample.java
ヘッダーファイルの作成
カレントディレクトリに作成(ex_Sample.h
)
javac -h ./ Sample.java
-
{package名}_{クラス名}.h
で作成される。 -
-h
の後に生成場所を指定
ドキュメント見てここで初めて知ったけど、定数もアノテーション(@Native)をつければ共有できるようになるみたい
Specifies where to place generated native header files.
When you specify this option, a native header file is generated for each class that contains native methods or that has one or more constants annotated with the java.lang.annotation.Native annotation. If the class is part of a package, then the compiler puts the native header file in a subdirectory that reflects the package name and creates directories as needed.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Test_Sample */
#ifndef _Included_Test_Sample
#define _Included_Test_Sample
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Test_Sample
* Method: printHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Test_Sample_printHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
このヘッダーはjni.h
というライブラリと、そのライブラリの中でjni_md.h
も必要になる。
ライブラリの生成
ヘッダーのパスを通して、Cファイルをコンパイル。
自分の場合は
-
jni.h
/Library/Java/JavaVirtualMachines/temurin-11.jdk/Contents/Home/include/jni.h
-
jni_md.h
/Library/Java/JavaVirtualMachines/temurin-11.jdk/Contents/Home/include/darwin/jni_md.h
の2つなのでJAVA_INCLUDE_PATH
で共通部分をまとめてexport
このdarwin
部分がまたOS
によって異なるかも
OS | path |
---|---|
Linux | .../linux/jni_md.h |
Windows(32 bit) | .../win32/jni_md.h |
Windows(64 bit) | .../win64/jni_md.h |
Mac | .../darwin/jni_md.h |
Mac
以外は実行してないのでなるべく探してください。
gcc -fPIC -shared -I$JAVA_INCLUDE_PATH -I$JAVA_INCLUDE_PATH/darwin hello.c -o libprintHello.jnilib
コンパイラドライバは
cc
でもg++
でもok
-
-shared
共有オブジェクトを生成するためのオプション -
-fPIC
共用ライブラリに使用するのに適した位置非依存コード(position-independent code)を生成。-shared
オプションを使うときに必須になる場合がある。 -
-o
生成物の名前を指定
hello world!
実行してみる
java -Djava.library.path=./ex ex.Sample
-
ex
ディレクトリがある場所で実行 -
-Djava.library.path
に生成した共有ライブラリがあるディレクトリを指定
hello world!
javaからCのprintHello()
関数を呼び出すことができた。
よくあるエラー
自分が出会ったエラーとその対策をいくつか
fatal error: 'jni.h' file not found
パスが間違ってるか環境変数が正しくセットされていないかのどっちか
あとはもう-I
オプションに無理やりぶち込むか
gcc -shared -I/Library/Java/JavaVirtualMachines/temurin-11.jdk/Contents/Home/include/ -I/Library/Java/JavaVirtualMachines/temurin-11.jdk/Contents/Home/include/darwin hello.c -o libprintHello.jnilib
jdk
をインストールした環境によってパスが異なるので注意が必要。
fatal error: 'jni_md.h' file not found
逆にjni.h
がうまく設定できてるってことなのでパスが違う可能性が高い
OS | path |
---|---|
Linux | .../linux/jni_md.h |
Windows(32 bit) | .../win32/jni_md.h |
Windows(64 bit) | .../win64/jni_md.h |
Mac | .../darwin/jni_md.h |
java.lang.UnsatisfiedLinkError
種類が多いので分けます
no printHello in java.library.path: [...]
java -Djava.library.path=./ex ex.Sample
Exception in thread "main" java.lang.UnsatisfiedLinkError: no printHelo in java.library.path: [./ex]
at java.base/java.lang.ClassLoader.loadLibrary(ClassLoader.java:2670)
at java.base/java.lang.Runtime.loadLibrary0(Runtime.java:830)
at java.base/java.lang.System.loadLibrary(System.java:1873)
at ex.Sample.main(Sample.java:7)
javaがCで定義した関数(もしくは共有ライブラリ自体)を見つけられてないです。
-Djava.library.path
の設定を見直すか、System.loadLibrary()
が正しく設定されているかを見直す必要があります。
System.load()
を使えば、絶対パスで実行もできます。
System.load("//Users/sho/Test/ex/libprintHello.jnilib");
'void ex.Sample.printHello()'
エラーの情報量が少なくてめっちゃ困るんだけど、僕の場合は誤字でした。
宣言した関数名や実際に使っているパッケージ名、クラス名、関数名がちょっとでも違うと例外が投げられる。
java -Djava.library.path=./ex ex.Sample
Exception in thread "main" java.lang.UnsatisfiedLinkError: 'void ex.Sample.printHello()'
at ex.Sample.printHello(Native Method)
at ex.Sample.main(Sample.java:9)
JNIEXPORT void JNICALL Java_ex_Samle_printHello(JNIEnv *env, jobject obj)
Samle
をSample
にして無事実行できた。
また、共有ライブラリを自作せずに他から持ってきたものを使ってる時は、ビット数が原因かも。
共有ライブラリは上位互換性を備えてないらしく、32bitで作成されたライブラリは64bitのOSで動作しないんだとか。
試してないからほんとかどうかはわからないです。
file libprintHello.jnilib
>> libprintHello.jnilib: Mach-O 64-bit dynamically linked shared library x86_64
Cpp
Cと流れはほぼ一緒です、当たり前っちゃ当たり前か
package ex;
public class SampleCpp {
public native void printHelloCpp();
public static void main(String[] args) {
System.loadLibrary("printHelloCpp");
SampleCpp sampleCpp = new SampleCpp();
sampleCpp.printHelloCpp();
}
}
#include <iostream>
#include "ex_SampleCpp.h"
using namespace std;
JNIEXPORT void JNICALL Java_ex_SampleCpp_printHelloCpp(JNIEnv *env, jobject obj) {
cout << "hello world!" << endl;
return ;
}
javac SampleCpp.java &&
javac -h ./ SampleCpp.java &&
g++ -shared -I$JAVA_INCLUDE_PATH -I$JAVA_INCLUDE_PATH/darwin helloCpp.cpp -o libprintHelloCpp.jnilib
java -Djava.library.path=./ex ex.SampleCpp
>> hello world!
アセンブラ
システムコールにてこづったのでただの数字を返す関数になっちゃったけど呼び出しはできた。
package ex;
public class Number {
public native int number();
public static void main(String[] args) {
System.loadLibrary("number");
Number number = new Number();
int n = number.number();
System.out.println(n);
}
}
global _Java_ex_Number_number
section .text
_Java_ex_Number_number:
mov rax, 12345678
ret
- Macは通常通りエントリーポイントになる関数の先頭に「
_
」をつける
javac Number.java &&
javac -h ./ Number.java &&
nasm -f macho64 -o number.o number.s &&
gcc -shared -o libnumber.jnilib number.o
-
elf64
でなくmacho64
で実行
このフォーマットの指定を間違えるとリンカがエラーを吐く
異常終了する関数
ちなみにネイティブメソッドの方でセグフォやアボートが起こるとこんな感じで教えてくれる。
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGBUS (0xa) at pc=0x000000010e1cb6e5, pid=13591, tid=8707
#
# JRE version: OpenJDK Runtime Environment Temurin-11.0.14+9 (11.0.14+9) (build 11.0.14+9)
# Java VM: OpenJDK 64-Bit Server VM Temurin-11.0.14+9 (11.0.14+9, mixed mode, tiered, compressed oops, g1 gc, bsd-amd64)
# Problematic frame:
# C 0x000000010e1cb6e5
#
# No core dump will be written. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /Users/sho/Test/hs_err_pid13591.log
#
# If you would like to submit a bug report, please visit:
# https://github.com/adoptium/adoptium-support/issues
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#
エラーログファイルも勝手に生成され、900行くらいあった。
hs_err_pid13591.log
--------------- S U M M A R Y ------------
Command Line: -Duser.language=en -Djava.library.path=./ex ex.SampleAsm
Host: MacBookAir9,1 x86_64 1200 MHz, 8 cores, 16G, Darwin 21.2.0
Time: Sun Mar 5 13:52:17 2023 JST elapsed time: 0.101478 seconds (0d 0h 0m 0s)
--------------- T H R E A D ---------------
Current thread (0x00007fec6300b000): JavaThread "main" [_thread_in_native, id=8707, stack(0x000070000e056000,0x000070000e156000)]
Stack: [0x000070000e056000,0x000070000e156000], sp=0x000070000e1530c0, free space=1012k
Native frames: (J=compiled Java code, A=aot compiled Java code, j=interpreted, Vv=VM code, C=native code)
C 0x000000010e1cb6e5
C 0x000000010e1ec43a
C 0x000000010e1ec4ba
C 0x000000010e1ec6d5
C 0x000000010e1ec381
C 0x000000010e1ec334
C 0x000000010e1cb6ad
C 0x000000010e1cb48a
C 0x000000010e1cee14
C 0x000000010e1daea2
C 0x000000010e1c1699
C 0x000000010e1da97b
V [libjvm.dylib+0x629cbb] os::dll_load(char const*, char*, int)+0x3f
V [libjvm.dylib+0x4415cb] JVM_LoadLibrary+0xd0
C [libjava.dylib+0x212c] Java_java_lang_ClassLoader_00024NativeLibrary_load0+0x78
j java.lang.ClassLoader$NativeLibrary.load0(Ljava/lang/String;Z)Z+0 java.base@11.0.14
j java.lang.ClassLoader$NativeLibrary.load()Z+53 java.base@11.0.14
j java.lang.ClassLoader$NativeLibrary.loadLibrary(Ljava/lang/Class;Ljava/lang/String;Z)Z+216 java.base@11.0.14
j java.lang.ClassLoader.loadLibrary0(Ljava/lang/Class;Ljava/io/File;)Z+46 java.base@11.0.14
j java.lang.ClassLoader.loadLibrary(Ljava/lang/Class;Ljava/lang/String;Z)V+356 java.base@11.0.14
j java.lang.Runtime.loadLibrary0(Ljava/lang/Class;Ljava/lang/String;)V+54 java.base@11.0.14
j java.lang.System.loadLibrary(Ljava/lang/String;)V+7 java.base@11.0.14
j ex.SampleAsm.main([Ljava/lang/String;)V+2
v ~StubRoutines::call_stub
V [libjvm.dylib+0x3b7c32] JavaCalls::call_helper
...
...
とても長いし、思ったより自分の環境設定むき出しに書かれていたので途中までしか載せてないです。
System.out.println
println
メソッドも、最終的にはnative
修飾子でcppを呼び出しているみたい。
C言語のシステムコールに落ち着くのかと思ったけどOSによってそうでもなさそう。
The code form JVM_Write is defined in the JVM itself. The code is not Java, nor C, but it’s C++. The method can be found in jvm.cpp:
感想
今のところ使い道は、なさそう