【Java】native修飾子でC,Cpp,アセンブラを呼び出す

# 経緯

javaの修飾子`native`の存在を知ったので、実際にどのように使うのかを調べる。

![スクリーンショット 2023-03-04 13.14.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/777324/a798cd62-a1a1-f161-a5cb-c772fc47addf.png)

> 画像引用：https://www.tohoho-web.com/java/modifier.htm

# native修飾子とは

cやcppで書かれた関数（ネイティブメソッド）を呼び出すことができる。

これを使うことによって、

* **システムのパフォーマンスを向上。**
* **マシンレベル/メモリレベルの通信**を実現。
* **Java以外のコードも使用できる。**

みたい。

外部を呼び出すことは、同時にjavaのメリットである**クロスプラットフォーム**が損なわれる可能性があるので「パフォーマンスの向上」で言うとその時その時で変わってきそう。

https://www.geeksforgeeks.org/native-keyword-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.

https://openjdk.org/jeps/313

自分も実行しようとしてエラーが出て、そこで初めて知った。

```zsh:Terminal
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`を使って実装したい人は以下の記事で実装してる人がいたので参考に

https://qiita.com/tanakash/items/6cdd7652e893c4baab63


# 環境

| 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

## やり方

1. `native`修飾子が入ったJavaコード書く
2. 呼び出したい関数が入ったCコードを書く
3. Cのヘッダーファイル(.hファイル)を`javac`で作成
4. 共有ライブラリ(DLL)を作成
5. 実行

の手順でやります。



## 準備

以下`ex`パッケージ内で作業してますが、パッケージは必須ではないです。
パッケージを含むとややこしくなるけど勉強になるので、一例として進めていきます。

## Sample.java

```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`とする。

https://stackoverflow.com/questions/761639/why-am-i-getting-this-unsatisfiedlinkerror-with-native-code

## hello.c

外部メソッドを記述する

```c: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`)
```zsh:Terminal
javac Sample.java
```
---

## ヘッダーファイルの作成

カレントディレクトリに作成（`ex_Sample.h`）
```zsh:Terminal
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.

https://docs.oracle.com/en/java/javase/11/tools/javac.html#GUID-AEEC9F07-CB49-4E96-8BC7-BCC2C7F725C9

```Test_Sample.h
/* 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`も必要になる。

https://github.com/openjdk/jdk/blob/master/src/java.base/share/native/include/jni.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`以外は実行してないのでなるべく探してください。


```zsh:Terminal
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`生成物の名前を指定

http://www.asahi-net.or.jp/~wg5k-ickw/html/online/gcc-2.95.2/gcc_2.html

## hello world!

実行してみる

```zsh:Terminal
java -Djava.library.path=./ex ex.Sample 
```

* `ex`ディレクトリがある場所で実行
* `-Djava.library.path`に生成した共有ライブラリがあるディレクトリを指定

```zsh:実行結果
hello world!
```

javaからCの`printHello()`関数を呼び出すことができた。

# よくあるエラー

自分が出会ったエラーとその対策をいくつか

## fatal error: 'jni.h' file not found

パスが間違ってるか環境変数が正しくセットされていないかのどっちか

あとはもう`-I`オプションに無理やりぶち込むか
```zsh
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`をインストールした環境によってパスが異なるので注意が必要。

https://stackoverflow.com/questions/14529720/how-to-make-jni-h-be-found

## 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 

種類が多いので分けます

https://doc.bccnsoft.com/docs/jdk11-docs/api/java.base/java/lang/UnsatisfiedLinkError.html

###  no printHello in java.library.path: [...]

```zsh:error
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()`を使えば、絶対パスで実行もできます。

```java:Sample.java
System.load("//Users/sho/Test/ex/libprintHello.jnilib");
```

### 'void ex.Sample.printHello()'

エラーの情報量が少なくてめっちゃ困るんだけど、僕の場合は誤字でした。
宣言した関数名や実際に使っているパッケージ名、クラス名、関数名が**ちょっとでも違う**と例外が投げられる。

```zsh:error
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)
```

```c:hello.c
JNIEXPORT void JNICALL Java_ex_Samle_printHello(JNIEnv *env, jobject obj)
```

`Samle`を`Sample`にして無事実行できた。

また、共有ライブラリを自作せずに他から持ってきたものを使ってる時は、ビット数が原因かも。

共有ライブラリは上位互換性を備えてないらしく、32bitで作成されたライブラリは64bitのOSで動作しないんだとか。

> 試してないからほんとかどうかはわからないです。

```zsh:Terminal
file libprintHello.jnilib

>> libprintHello.jnilib: Mach-O 64-bit dynamically linked shared library x86_64
```

# Cpp

Cと流れはほぼ一緒です、当たり前っちゃ当たり前か

```java:SampleCpp.java
package ex;

public class SampleCpp {
    public native void printHelloCpp();

    public static void main(String[] args) {
        System.loadLibrary("printHelloCpp");
        SampleCpp sampleCpp = new SampleCpp();
        sampleCpp.printHelloCpp();
    }
}
```

```cpp:helloCpp.cpp
#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 ;
}
```

```zsh:Terminal
javac SampleCpp.java &&
javac -h ./ SampleCpp.java &&
g++ -shared -I$JAVA_INCLUDE_PATH -I$JAVA_INCLUDE_PATH/darwin helloCpp.cpp -o libprintHelloCpp.jnilib
```

```zsh:Terminal
java -Djava.library.path=./ex ex.SampleCpp

>> hello world!
```

# アセンブラ

システムコールにてこづったのでただの数字を返す関数になっちゃったけど呼び出しはできた。

```Number.java
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);
	}
}
```

```text:number.s
global _Java_ex_Number_number

section .text
_Java_ex_Number_number:
	mov rax, 12345678
	ret
```

* Macは通常通りエントリーポイントになる関数の先頭に「`_`」をつける

```zsh:Terminal
javac Number.java &&
javac -h ./ Number.java &&
nasm -f macho64 -o number.o number.s &&
gcc -shared -o libnumber.jnilib number.o
```

* `elf64`でなく`macho64`で実行

https://www.nasm.us/xdoc/2.09.10/html/nasmdoc7.html

このフォーマットの指定を間違えるとリンカがエラーを吐く

https://developer.apple.com/forums/thread/694062

# 異常終了する関数

ちなみにネイティブメソッドの方でセグフォやアボートが起こるとこんな感じで教えてくれる。

```zsh:error
#
# 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行くらいあった。

<details>
<summary>hs_err_pid13591.log</summary><div>

```text: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
...
...

```

</div>
</details>

> とても長いし、思ったより自分の環境設定むき出しに書かれていたので途中までしか載せてないです。

# 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:

https://luckytoilet.wordpress.com/2010/05/21/how-system-out-println-really-works/

# 感想

今のところ使い道は、なさそう