為了使用外部函式庫,java 需要跟原生的 native code 進行互動,傳統是使用 Java Native Interface JNI 進行開發,但 JNI 的開發過程繁複,需要定義 native method,然後從 Java Source Code 生成 C 的 header file,再用C 語言實作,然後才能編譯並連結原生開發的函式庫。
Foreign Function & Memory API(FFM API)參考了其他語言的方法,實作外部函式庫的 Interface,提供更安全有效率的方法,存取本地端的記憶體及函式,取代了 JNI 的功能。這是自 JDK 22 開始的一組新的 API,用來取代 JNI。能夠呼叫 C/C++/Rust 的非 Java 原生函式庫,可直接操作記憶體。
JNI sample
JNIDemo.java
public class JNIDemo {
static {
System.loadLibrary("JNIDemo");
}
public static void main(String[] args) {
new JNIDemo().printHelloWorld();
}
private native void printHelloWorld();
}
這裡面,有一個 private native void printHelloWorld(),這就是還沒有實作的原生函式。
用以下指令,編譯 class 並產生 header file
javac -h . JNIDemo.java
JNIDemo.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNIDemo */
#ifndef _Included_JNIDemo
#define _Included_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: JNIDemo
* Method: printHelloWorld
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JNIDemo_printHelloWorld
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
編譯 C library
因為在 macos 先查詢 JAVA_HOME 目錄
$ /usr/libexec/java_home
/opt/local/Library/Java/JavaVirtualMachines/jdk-25-azul-zulu.jdk/Contents/Home
編譯
JAVA_HOME=/opt/local/Library/Java/JavaVirtualMachines/jdk-25-azul-zulu.jdk/Contents/Home
gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -dynamiclib JNIDemo.c -o libJNIDemo.dylib
會產生 libJNIDemo.dylib
執行
$ java -cp . -Djava.library.path=. JNIDemo
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::loadLibrary has been called by JNIDemo in an unnamed module (file:/Users/charley/Downloads/)
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!
FFM
FFM API 能夠讓 Java 程式在不使用 JNI 的情況下
更安全地存取記憶體
直接呼叫 C function
呼叫 C/系統的 library
FFM 核心元件:
Linker:對應 C 語言呼叫約定的連結器,用來呼叫 native 函式
SymbolLookup: 在 native library 或 process 中查找符號(例如
printf)MemorySegment: 表示一塊連續的記憶體(on-heap 或 off-heap)
Arena:管理
MemorySegment的生命週期(自動釋放資源)ValueLayout:描述原生型別的記憶體布局,例如
JAVA_INT,C_DOUBLEFunctionDescriptor:描述 native 函式的簽章(參數與回傳型別)
測試1
測試呼叫 strlen
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.nio.charset.StandardCharsets;
public class FFMStrlenDemo {
public static void main(String[] args) throws Throwable {
// 1. 取得系統的 Linker(如 SystemV ABI 或 Windows ABI)
Linker linker = Linker.nativeLinker();
// 2. 找出 symbol (在 libc 裡)
SymbolLookup stdlib = Linker.nativeLinker().defaultLookup();
MemorySegment strlenAddr = stdlib.find("strlen").orElseThrow();
// 3. 建立 Java 對應的 MethodHandle
MethodHandle strlen = linker.downcallHandle(
strlenAddr,
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
// 4. 建立要傳入的字串記憶體
// try (Arena arena = Arena.ofConfined()) {
// MemorySegment str = arena.allocateUtf8String("Hello FFM API!");
// long len = (long) strlen.invoke(str);
// System.out.println("Length = " + len);
// }
try (Arena arena = Arena.ofConfined()) {
// 手動建立 UTF-8 null-terminated string
byte[] bytes = "Hello FFM API!".getBytes(StandardCharsets.UTF_8);
MemorySegment str = arena.allocate(bytes.length + 1, 1);
str.asSlice(0, bytes.length).copyFrom(MemorySegment.ofArray(bytes));
str.set(ValueLayout.JAVA_BYTE, bytes.length, (byte) 0); // '\0'
long len = (long) strlen.invoke(str);
System.out.println("Length = " + len);
}
// allocate set/get 測試
try (Arena arena = Arena.ofConfined()) {
MemorySegment seg = arena.allocate(ValueLayout.JAVA_INT);
seg.set(ValueLayout.JAVA_INT, 0, 42); // 寫入 42
int value = seg.get(ValueLayout.JAVA_INT, 0);
System.out.println("Read value: " + value);
} // arena 自動釋放記憶體
}
}
檢查 allocate API
標準 API 是 java.lang.foreign.Arena.allocateUtf8String(String)
但測試,結果是 allocate
# javap -classpath $JAVA_HOME/lib/modules java.lang.foreign.Arena | grep allocate
public abstract java.lang.foreign.MemorySegment allocate(long, long);
這表示目前這個 openjdk 還是使用舊版的 allocate,缺少 allocateUtf8String
編譯,執行
javac FFMStrlenDemo.java
java --enable-native-access=ALL-UNNAMED FFMStrlenDemo
結果
Length = 14
Read value: 42
測試2
sum.c
// sum.c
#include <stdio.h>
int sum(int a, int b) {
return a + b;
}
編譯為 library
## linux
# gcc -shared -fPIC -o libsum.so sum.c
## macos
gcc -shared -fPIC -o libsum.dylib sum.c
SumFFMDemo.java 放在同一個目錄
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
public class SumFFMDemo {
public static void main(String[] args) throws Throwable {
// 載入我們的動態函式庫
System.loadLibrary("sum"); // 對應 libsum.so / sum.dll
// 建立 linker
Linker linker = Linker.nativeLinker();
// 查找 symbol
SymbolLookup lookup = SymbolLookup.libraryLookup("libsum.dylib", Arena.global());
MemorySegment funcAddr = lookup.find("sum").orElseThrow();
// 建立函式描述: int (int, int)
FunctionDescriptor fd = FunctionDescriptor.of(
ValueLayout.JAVA_INT, // return type
ValueLayout.JAVA_INT, // param a
ValueLayout.JAVA_INT // param b
);
// 建立 method handle
MethodHandle sum = linker.downcallHandle(funcAddr, fd);
// 呼叫 native 函式
int result = (int) sum.invoke(12, 30);
System.out.println("sum(12, 30) = " + result);
}
}
編譯
javac SumFFMDemo.java
java --enable-native-access=ALL-UNNAMED SumFFMDemo
# java -Djava.library.path=. --enable-native-access=ALL-UNNAMED SumFFMDemo
結果
sum(12, 30) = 42
沒有留言:
張貼留言