2026/4/27

FFMForeign Function and Memory (FFM) API

為了使用外部函式庫,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_DOUBLE

  • FunctionDescriptor:描述 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

沒有留言:

張貼留言