使用 JDK 的 Foreign Function & Memory API (FFM) 调用动态链接库示例

Foreign Function & Memory API (FFM) 是 Java 平台提供的一个用于与本地代码(非 Java 代码)和安全地访问堆外内存的 API。它是 Java 22 中的预览特性(JEP 454),旨在替代传统的 JNI(Java Native Interface),提供更高效、更安全的本地代码交互方式。

🔹1 FFM API 主要组成

FFM API 主要包含以下几个核心部分:

  1. 调用本地函数(Foreign Function)
    • 允许 Java 代码直接调用 C 语言库中的函数,而无需 JNI 绑定。
    • 使用 SymbolLookup 查找本地函数地址,结合 Linker 进行调用。
  2. 管理本机内存(Memory Access)
    • 允许 Java 代码分配、读取和写入堆外(Native)内存,而无需 Unsafe
    • 使用 MemorySegment 进行安全的内存操作,避免内存泄漏和越界访问。
  3. 处理数据布局(Memory Layout)
    • 通过 MemoryLayout 定义 C 结构体,使 Java 代码能安全访问本地数据结构。

🔹 2 FFM API 的核心类

类 / 接口作用
MemorySegment表示一个可管理的本机内存区域,可用于读写数据。
MemoryLayout定义结构化数据布局,帮助解析 C 结构体。
MemorySession管理 MemorySegment 的生命周期,确保安全释放。
SymbolLookup用于查找本地库中的函数或变量地址。
Linker负责将 Java 方法和本地函数绑定,以进行调用。

🔹 3 示例:调用 C 语言的 函数

c语言代码

helloLibrary.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 打印 "Hello, World!"
void hello_world() {
    printf("Hello, World!\n");
    return;
}

// 返回动态分配的字符串 "Hello, World!"
char* create_string() {
    char* str = (char*)malloc(15); // 分配内存以存储 "Hello, World!"
    if (str) {
        strcpy(str, "你好,世界!");
    }
    return str;
}

// 返回两个整数的和
int add_numbers(int a, int b) {
    return a + b;
}

使用gcc编译成动态链接库helloLibrary.dll

gcc:

gcc -shared -o helloLibrary.dll helloLibrary.c -static-libgcc -static-libstdc++

cl:

cl /LD helloLibrary.dll /o helloLibrary.dll

java 调用

FFMNative.java

package com.bingbaihanji;

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;

/**
 * 使用 JDK 的 Foreign Function & Memory API (FFM) 调用动态链接库示例。
 * <p>
 * 此代码演示如何在新版本 JDK (>= 22) 中使用 `java.lang.foreign` API 进行外部库函数调用。
 * <p>
 * 步骤说明:
 * 1. 获取 DLL 的路径。
 * 2. 通过 `Linker` 创建本地库链接器。
 * 3. 使用 `SymbolLookup` 查找动态链接库中的函数符号。
 * 4. 通过 `downcallHandle` 创建方法句柄。
 * 5. 通过 `invoke` 调用 DLL 中的 `hello_world`、`create_string` 和 `add_numbers` 函数。
 *
 * @author 冰白寒祭
 * @date 2025-03-25 16:05:29
 */
public class FFMNative {
    public static void main(String[] args) {
        try {
            // 获取动态链接库的路径
            URL url = ResourceUtils.getURL("classpath:gcc/helloLibrary.dll");
            if (!Objects.isNull(url)) {
                Path linkLibraryPath = Paths.get(url.toURI());

                // 创建本地库链接器
                Linker linker = Linker.nativeLinker();

                // 在全局 Arena(内存区域)内查找符号
                SymbolLookup lookup = SymbolLookup.libraryLookup(linkLibraryPath, Arena.global());

                // 1. 调用 hello_world 函数
                MemorySegment helloSegment = lookup.find("hello_world")
                        .orElseThrow(() -> new RuntimeException("函数 hello_world not 未找到"));
                MethodHandle helloHandle = linker.downcallHandle(helloSegment, FunctionDescriptor.ofVoid());
                helloHandle.invoke();

                // 2. 调用 create_string 函数,获取字符串
                MemorySegment stringSegment = lookup.find("create_string")
                        .orElseThrow(() -> new RuntimeException("函数 create_string 未找到"));
                MethodHandle stringHandle = linker.downcallHandle(
                        stringSegment,
                        FunctionDescriptor.of(ValueLayout.ADDRESS)
                );

                // 调用函数获取返回的指针
                MemorySegment stringPtr = (MemorySegment) stringHandle.invoke();

                // 将指针转换为实际的内存段(假设字符串最大长度为256)
                MemorySegment stringMemory = stringPtr.reinterpret(256);

                // 读取以null结尾的C字符串
                String resultString = stringMemory.getString(0, StandardCharsets.UTF_8);
                System.out.println("create_string 返回: " + resultString);

                // 3. 调用 add_numbers 函数,计算两个整数之和
                MemorySegment addSegment = lookup.find("add_numbers")
                        .orElseThrow(() -> new RuntimeException("函数 add_numbers 未找到"));
                MethodHandle addHandle = linker.downcallHandle(addSegment, FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT));

                int num1 = 10;
                int num2 = 20;
                int sum = (int) addHandle.invoke(num1, num2);
                System.out.println("add_numbers(" + num1 + ", " + num2 + ") 返回: " + sum);
            }
            System.out.println("DLL 路径: " + url.toString());
        } catch (Throwable e) {
            throw new RuntimeException("调用 DLL 失败", e);
        }
    }
}

运行结果如下:

create_string 返回: 你好,世界!
add_numbers(10, 20) 返回: 30
DLL 路径: file:/D:/javaProject/bingbaihanji/jdk23-ffmapi/target/classes/gcc/hellolibrary.dll
Hello, World!

**4🔹 FFM API 相比 JNI 的优势

特性FFM APIJNI
性能直接调用,无需中间代理层,更快需要中间层转换,调用开销大
安全性自动管理内存,减少泄漏风险需要手动管理本机内存
易用性纯 Java 代码,API 直观需要 C 代码、javah 生成绑定
可维护性代码更现代化,支持内存布局代码复杂,难以调试

5🔹FFM API 的应用场景

  • 调用 C 语言库(如 OpenGL、SQLite、libcurl)。
  • 高性能数据处理(映射本机共享内存)。
  • 系统编程(直接访问操作系统 API)。
  • 替代 JNI,减少代码复杂度和维护成本。

### 总结

FFM API 通过 MemorySegment 管理本机内存,Linker 绑定本地函数,提供了比 JNI 更简单、安全、性能更好的方式来与原生代码交互

人生不作安期生,醉入东海骑长鲸