LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-05

経緯

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

スクリーンショット 2023-03-04 13.14.43.png

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

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.

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

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

環境

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

hello.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)

Terminal
javac Sample.java

ヘッダーファイルの作成

カレントディレクトリに作成(ex_Sample.h

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.

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も必要になる。

ライブラリの生成

ヘッダーのパスを通して、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以外は実行してないのでなるべく探してください。

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

hello world!

実行してみる

Terminal
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: [...]

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

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

'void ex.Sample.printHello()'

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

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)
hello.c
JNIEXPORT void JNICALL Java_ex_Samle_printHello(JNIEnv *env, jobject obj)

SamleSampleにして無事実行できた。

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

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

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

Terminal
file libprintHello.jnilib

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

Cpp

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

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();
    }
}
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 ;
}
Terminal
javac SampleCpp.java &&
javac -h ./ SampleCpp.java &&
g++ -shared -I$JAVA_INCLUDE_PATH -I$JAVA_INCLUDE_PATH/darwin helloCpp.cpp -o libprintHelloCpp.jnilib
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);
	}
}
number.s
global _Java_ex_Number_number

section .text
_Java_ex_Number_number:
	mov rax, 12345678
	ret
  • Macは通常通りエントリーポイントになる関数の先頭に「_」をつける
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で実行

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

異常終了する関数

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

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行くらいあった。

hs_err_pid13591.log
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:

感想

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

0
0
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0