本記事では、JEP 454: Foreign Function & Memory APIをJNIと比較しながら、Linux、Windowsでの基本的な使い方を紹介します。
概要
Foreign Function & Memory APIは頭文字をとってFFM APIと略されます。
FFM APIはJDK19から3度のプレビューを経てJDK22で正式導入されました。
FFM APIとしてJEPになったのはJDK17のJEP 412: Foreign Function & Memory API (Incubator)からですが、それ以前は2つのIncubatorとして開発されていました。
-
JEP 370: Foreign-Memory Access API (Incubator)
Javaヒープ外へアクセスでき、java.nio.ByteBufferやsun.misc.Unsafeの代替となるAPI。 -
JEP 389: Foreign Linker API (Incubator)
Java以外の言語で書かれた関数をJavaから呼び出すことができる、JNIの代替となるAPI。
そして、FFM APIはProject Panamaの成果です。
Project Panamaはざっくりですが、「Javaの世界」と「ネイティブの世界」のインターフェースを改善する活動をしています。
他には、Vector APIもやっています。(JDK23でEighth Incubator JEP469)
実行環境
Linux
OS
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.3 LTS"
JDK(OpenJDK jdk22を自分でビルドしたもの)
$ java --version
openjdk 22-internal 2024-03-19
OpenJDK Runtime Environment (build 22-internal-adhoc.tabatad.jdk22)
OpenJDK 64-Bit Server VM (build 22-internal-adhoc.tabatad.jdk22, mixed mode, sharing)
gcc
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Windows
OS
>ver
Microsoft Windows [Version 10.0.22631.3447]
JDK
$ java --version
openjdk 22 2024-03-19
OpenJDK Runtime Environment Temurin-22+36 (build 22+36)
OpenJDK 64-Bit Server VM Temurin-22+36 (build 22+36, mixed mode, sharing)
gcc
$ gcc --version
gcc.exe (x86_64-win32-seh-rev1, Built by MinGW-Builds project) 13.1.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
FFM APIとJNIを比較
CでHello world
と表示する関数を用意し、それをJavaから呼び出します。
まずは、LinuxでのJNI、FFM APIを比較し、そのあとにWindowsでのJNI、FFM APIを比較します。
Linux
JNI
public class JNI {
private native void hello();
static {
System.loadLibrary("hello_jni");
}
public static void main(String[] args) {
new JNI().hello();
}
}
#include <stdio.h>
#include "JNI.h"
JNIEXPORT void JNICALL Java_JNI_hello (JNIEnv *, jobject) {
printf("Hello world\n");
}
$ javac -h . JNI.java
$ gcc -shared -o libhello_jni.so -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux hello_jni.c
$ java -Djava.library.path=. JNI
Hello world
Foreign Function API
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
public class FFM {
public static void main(String[] args) {
Linker linker = Linker.nativeLinker();
try (Arena offHeap = Arena.ofConfined()) {
SymbolLookup lib = SymbolLookup.libraryLookup("libhello_ffm.so", offHeap);
MethodHandle hello = linker.downcallHandle(lib.find("hello").orElseThrow(), FunctionDescriptor.ofVoid());
hello.invokeExact();
} catch(Throwable t) {
t.printStackTrace();
}
}
}
#include <stdio.h>
void hello() {
printf("Hello world\n");
}
$ gcc -shared -o libhello_ffm.so -fPIC hello_ffm.c
$ java -Djava.library.path=. --enable-native-access=ALL-UNNAMED FFM
java.lang.IllegalArgumentException: Cannot open library: libhello_ffm.so
at java.base/java.lang.foreign.SymbolLookup.libraryLookup(SymbolLookup.java:314)
at java.base/java.lang.foreign.SymbolLookup.libraryLookup(SymbolLookup.java:266)
at FFM.main(FFM.java:9)
$ export LD_LIBRARY_PATH=`pwd`
$ java --enable-native-access=ALL-UNNAMED FFM.java
Hello world
$ java FFM
WARNING: A restricted method in java.lang.foreign.SymbolLookup has been called
WARNING: java.lang.foreign.SymbolLookup::libraryLookup has been called by FFM in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
Hello world
ポイント1
libraryLookupメソッドがライブラリを探す場所はOSごとに異なり、posixではdlopen関数の仕様に依存します。
dlopenはLD_LIBRARY_PATH
に定義されているパスからライブラリを検索するので、LD_LIBRARY_PATH
にライブラリ(今回はlibhello_ffm.so)がある場所を定義しておく必要があります。
なお、JNIのように、実行時に-Djava.library.path=
でライブラリのパスを指定する方法では例外が発生します。
ポイント2
--enable-native-access=ALL-UNNAMED
FFM APIの一部のメソッドは使い方を誤るとJVMをクラッシュさせる可能性があるなど、危険な側面を持っています。なので、そういったメソッドを使用している場合、上記のオプションをつけなければ警告が出るようになっています。
参考:使用が制限されるメソッド
ポイント3
Arenaというクラスでヒープ外のメモリ領域の管理を行います。詳しくは後日別の記事に書くかもしれません。
ポイント4
JNIと比較してFFM APIは、
- ライブラリの関数を独自の書き方をする必要がない
-
javac -h
でヘッダファイルを作成する必要がない
というメリットがあります。
Windows
JNI
public class JNI {
private native void hello();
static {
System.loadLibrary("hello_jni");
}
public static void main(String[] args) {
new JNI().hello();
}
}
#include <stdio.h>
#include "JNI.h"
JNIEXPORT void JNICALL Java_JNI_hello (JNIEnv *env, jobject jo) {
printf("Hello world\n");
}
$ javac -h . JNI.java
$ gcc -shared -o hello_jni.dll -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/win32 hello_jni.c
$ java -Djava.library.path=. JNI
Hello world
Foreign Function API
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
public class FFM {
public static void main(String[] args) {
Linker linker = Linker.nativeLinker();
try (Arena offHeap = Arena.ofConfined()) {
SymbolLookup lib = SymbolLookup.libraryLookup("hello_ffm.dll", offHeap);
MethodHandle hello = linker.downcallHandle(lib.find("hello").orElseThrow(), FunctionDescriptor.ofVoid());
hello.invokeExact();
} catch(Throwable t) {
t.printStackTrace();
}
}
}
#include <stdio.h>
void hello() {
printf("Hello world\n");
}
$ gcc -shared -o hello_ffm.dll -fPIC hello_ffm.c
$ java --enable-native-access=ALL-UNNAMED FFM.java
Hello world
ポイント1
libraryLookupメソッドがライブラリを探す場所はOSごとに異なり、WindowsではLoadLibrary関数の仕様に依存します。
LoadLibrary関数はカレントディレクトリにライブラリのファイル(今回はhello_ffm.dll)があれば読み込みます。
ポイント2
jniの場合はLinux、Windowsに関わらずjavaのソースは全く同じで実行できますが、FFMの場合、ライブラリをロードするときのファイル名の指定方法がOSで異なります。
Linuxの場合、ライブラリのファイル名を省略せずに指定する必要があります。
SymbolLookup lib = SymbolLookup.libraryLookup("libhello_ffm.so", offHeap);
Windowsの場合、拡張子を省略してもライブラリを読み込むことができます。
SymbolLookup lib = SymbolLookup.libraryLookup("hello_ffm", offHeap);
// もしくは
SymbolLookup lib = SymbolLookup.libraryLookup("hello_ffm.dll", offHeap);
このようなことが起こるのは、ポイント1で述べたように、libraryLookupメソッドが行うライブラリのロードはOS毎の関数に依存しているためだと考えられます。
つまり、posixのdlopen関数は拡張子やファイル名の頭のlib
の省略を許さず、WindowsのLoadLibrary関数は拡張子の省略を許しています。
まとめ
本記事では、JEP 454: Foreign Function & Memory APIをJNIと比較しながら、Linux、Windowsでの基本的な使い方を紹介しました。ご参考になれば幸いです。
また、細かい表現の誤りや誤った内容を記載している可能性があります。
その際はご指摘いただけますと幸いです。