SCX FFI 是一个基于 JDK Foreign Function & Memory API 的轻量 FFI 封装库。
它允许你用 Java 接口声明 native 函数,然后通过 ScxFFI.createFFI(...) 创建代理对象,最终像调用普通 Java 方法一样调用本机函数。
SCX FFI 本身不负责提供 C 标准库、Windows API 或其它 native library。它只负责把 Java 接口方法映射到 native symbol,并在调用前后完成参数的内存转换和必要的回写。
当前版本为 0.3.0。
<dependency>
<groupId>dev.scx</groupId>
<artifactId>scx-ffi</artifactId>
<version>0.3.0</version>
</dependency>
SCX FFI 依赖 JDK 的 java.lang.foreign API,因此使用时需要运行在支持该 API 的 JDK 上。
SCX FFI 中最核心的概念包括:
ScxFFI 创建 FFI 代理对象的入口
FFMProxy Java 动态代理 InvocationHandler
FFMMapper Java 对象和 MemorySegment 之间的转换器
SymbolName 指定 Java 方法对应的 native symbol 名称
FFIStruct 表示 native 结构体
FFICallback 表示 native 回调函数
EncodedString 指定编码的字符串
ByteRef 等 用于模拟 C 指针出参
它们之间的关系可以简单理解为:
Java 接口方法
↓
ScxFFI.createFFI(...)
↓
FFMProxy
↓
查找 native symbol
↓
创建 downcall MethodHandle
↓
调用时转换参数
↓
调用 native 函数
↓
必要时回写参数
也就是说:
接口负责声明 native 函数
ScxFFI 负责创建代理
FFMProxy 负责拦截调用
FFMMapper 负责对象和 MemorySegment 的相互转换
以 C 标准库中的 strlen 为例,可以先定义一个 Java 接口:
import dev.scx.ffi.ScxFFI;
public interface C {
C C = ScxFFI.createFFI(C.class);
long strlen(String str);
}
然后直接调用:
long length = C.C.strlen("abc123456");
System.out.println(length);
如果 native symbol 可以在默认 lookup 中找到,可以使用:
ScxFFI.createFFI(C.class)
如果函数在指定动态库中,可以使用:
ScxFFI.createFFI(User32.class, "user32")
或者使用动态库路径:
ScxFFI.createFFI(MyLib.class, Path.of("/path/to/libxxx.so"))
SCX FFI 的主要使用方式是定义一个接口。
例如:
import dev.scx.ffi.ScxFFI;
public interface C {
C C = ScxFFI.createFFI(C.class);
long strlen(String str);
int abs(int x);
double sqrt(double x);
}
然后:
long a = C.C.strlen("hello");
int b = C.C.abs(-123);
double c = C.C.sqrt(88);
接口中的抽象方法会被映射为 native symbol。
默认情况下,Java 方法名就是 native symbol 名:
strlen -> strlen
abs -> abs
sqrt -> sqrt
ScxFFI 提供了四个入口方法。
public static <T> T createFFI(Class<T> clazz)
示例:
var c = ScxFFI.createFFI(C.class);
这种方式使用:
nativeLinker().defaultLookup()
适合调用默认可见的本机符号。
public static <T> T createFFI(Class<T> clazz, String libraryName)
示例:
var user32 = ScxFFI.createFFI(User32.class, "user32");
这种方式内部使用:
SymbolLookup.libraryLookup(libraryName, Arena.global())
适合按库名称加载 native library。
例如 Windows 上:
ScxFFI.createFFI(User32.class, "user32")
public static <T> T createFFI(Class<T> clazz, Path libraryPath)
示例:
var myLib = ScxFFI.createFFI(MyLib.class, Path.of("/opt/lib/libxxx.so"));
这种方式适合通过明确路径加载动态库。
public static <T> T createFFI(Class<T> clazz, SymbolLookup symbolLookup)
示例:
var myLib = ScxFFI.createFFI(MyLib.class, customSymbolLookup);
这种方式适合需要自己控制 symbol 查找逻辑的场景。
默认情况下,SCX FFI 使用 Java 方法名查找 native symbol。
如果 native symbol 名称不适合作为 Java 方法名,或者希望 Java 方法名和 native symbol 名分离,可以使用 @SymbolName。
示例:
import dev.scx.ffi.annotation.SymbolName;
public interface C {
@SymbolName("abs")
int javaAbs(int x);
}
调用时:
int value = C.C.javaAbs(-123);
实际查找的 native symbol 是:
abs
不是:
javaAbs
这适合下面这些场景:
FFI 接口中可以定义默认方法。
默认方法不会映射到 native symbol,而是作为普通 Java 默认方法调用。
示例:
public interface C {
C C = ScxFFI.createFFI(C.class);
@SymbolName("abs")
int javaAbs(int x);
default int abs(int x) {
return javaAbs(x);
}
}
调用:
int value = C.C.abs(-123);
这里:
abs(...) 是 Java 默认方法。javaAbs(...) 才会映射到 native symbol abs。FFI 接口可以继承其它接口。
父接口中的抽象方法也会被扫描并映射为 native symbol。
示例:
public interface CBase {
double sin(double x);
}
public interface C extends CBase {
C C = ScxFFI.createFFI(C.class);
long strlen(String str);
double sqrt(double x);
}
调用:
double a = C.C.sin(12);
double b = C.C.sqrt(88);
这里 sin(...) 来自父接口,但仍然可以被 FFI 代理处理。
SCX FFI 当前支持下面这些参数类型:
byte
short
int
long
float
double
char
MemorySegment
ByteRef
ShortRef
IntRef
LongRef
FloatRef
DoubleRef
CharRef
String
EncodedString
byte[]
short[]
int[]
long[]
float[]
double[]
char[]
FFIStruct
FFICallback
FFMMapper
null
它们最终会被转换成下面两类 native 调用参数:
基本类型
MemorySegment
其中:
MemorySegment 会直接传给 FFM。MemorySegment。null 会被转换成 MemorySegment.NULL。SCX FFI 当前支持下面这些返回值类型:
void
byte
short
int
long
float
double
char
MemorySegment
例如:
int abs(int x);
double sqrt(double x);
MemorySegment GetStdHandle(int nStdHandle);
void someVoidFunction();
需要注意:
String。FFIStruct。FFICallback。boolean。MemorySegment 接收。常见类型映射可以理解为:
Java byte -> JAVA_BYTE
Java short -> JAVA_SHORT
Java int -> JAVA_INT
Java long -> JAVA_LONG
Java float -> JAVA_FLOAT
Java double -> JAVA_DOUBLE
Java char -> JAVA_CHAR
MemorySegment -> ADDRESS
对于参数来说,下面这些类型会被映射为地址:
ByteRef 等 Ref 类型 -> ADDRESS
String -> ADDRESS
EncodedString -> ADDRESS
基本类型数组 -> ADDRESS
FFIStruct -> ADDRESS
FFICallback -> ADDRESS
FFMMapper -> ADDRESS
null -> MemorySegment.NULL
对于返回值来说,MemorySegment 会映射为:
ADDRESS
String 参数会通过 StringFFMMapper 转换成 native 字符串内存。
示例:
long strlen(String str);
调用:
long length = C.C.strlen("abc123456");
String 是只读映射。
也就是说,native 函数如果修改了这块字符串内存,修改结果不会回写到原来的 Java String 中。
这是因为 Java String 本身是不可变对象。
如果需要指定字符串编码,可以使用 EncodedString。
示例:
import dev.scx.ffi.type.EncodedString;
import static java.nio.charset.StandardCharsets.UTF_16LE;
int MessageBoxW(MemorySegment hWnd,
EncodedString lpText,
EncodedString lpCaption,
int uType);
调用:
USER32.MessageBoxW(
null,
new EncodedString("MessageBoxW 测试中文内容", UTF_16LE),
new EncodedString("测试标题", UTF_16LE),
0
);
EncodedString 适合下面这些场景:
W 系列 API。String 的默认编码行为。和 String 一样,EncodedString 也是只读映射,不会从 native 内存回写到 Java 对象。
SCX FFI 提供了一组 Ref 类型,用来模拟 C 中的指针出参。
ByteRef
ShortRef
IntRef
LongRef
FloatRef
DoubleRef
CharRef
例如 C 风格函数:
int get_value(int* out);
可以声明为:
import dev.scx.ffi.type.IntRef;
int get_value(IntRef out);
调用:
var out = new IntRef();
int result = lib.get_value(out);
System.out.println(out.value());
Ref 类型的基本语义是:
value() 分配 native 内存。以 IntRef 为例:
var mode = new IntRef();
KERNEL32.GetConsoleMode(handle, mode);
int value = mode.value();
如果 native 函数修改了 int* 指向的值,调用结束后可以通过 mode.value() 读取。
Ref 类型有无参构造和带初始值构造。
var a = new IntRef();
var b = new IntRef(100);
无参构造默认值为对应基本类型的零值。
SCX FFI 支持基本类型数组作为参数。
byte[]
short[]
int[]
long[]
float[]
double[]
char[]
数组会被转换成一段连续 native 内存。
调用结束后,native 内存中的数据会回写到原 Java 数组对象中。
示例:
void fill(int[] array, int length);
调用:
var array = new int[10];
lib.fill(array, array.length);
System.out.println(Arrays.toString(array));
如果 native 函数修改了数组内容,Java 侧原数组会被更新。
测试中也使用了 qsort 对 int[] 进行排序:
void qsort(int[] base, long nmemb, long size, Compar compar);
调用:
var array = new int[]{2, 5, 7, 1, 4, 56, 12, 31, 99999, 90, 271, 2};
C.C.qsort(array, array.length, Integer.BYTES, (aAddr, bAddr) -> {
int a = aAddr.get(JAVA_INT, 0);
int b = bAddr.get(JAVA_INT, 0);
return Integer.compare(a, b);
});
调用后,array 会被 native qsort 修改为排序后的结果。
需要注意:
如果参数或返回值本来就是 native 地址,可以直接使用 MemorySegment。
示例:
MemorySegment GetStdHandle(int nStdHandle);
调用:
MemorySegment handle = KERNEL32.GetStdHandle(-11);
也可以作为参数传入:
int IsWindowVisible(MemorySegment hWnd);
int visible = USER32.IsWindowVisible(hWnd);
当需要表示空指针时,可以传入 null:
USER32.MessageBoxA(null, "内容", "标题", 0);
SCX FFI 会把 null 转换为:
MemorySegment.NULL
如果 native 函数需要结构体指针,可以使用 FFIStruct。
结构体类需要实现 FFIStruct,并使用 public 非 static 字段表示结构体字段。
示例:
import dev.scx.ffi.type.FFIStruct;
public static class POINT implements FFIStruct {
public int x;
public int y;
}
对应 C 结构体可以理解为:
typedef struct POINT {
int x;
int y;
} POINT;
然后可以声明 native 函数:
int GetCursorPos(POINT lpPoint);
调用:
var point = new POINT();
USER32.GetCursorPos(point);
System.out.println(point.x);
System.out.println(point.y);
调用流程是:
POINT 的 public 非 static 字段创建结构体内存布局。FFIStruct 当前支持下面这些字段类型:
byte
short
int
long
float
double
char
MemorySegment
FFIStruct
也就是说,结构体可以嵌套结构体。
示例:
public static class RECT implements FFIStruct {
public int left;
public int top;
public int right;
public int bottom;
}
默认情况下,结构体字段使用字段声明顺序。
如果需要自定义字段顺序,可以重写 fieldOrder()。
public static class RECT implements FFIStruct {
public int left;
public int top;
public int right;
public int bottom;
@Override
public String[] fieldOrder() {
return new String[]{
"left",
"top",
"right",
"bottom"
};
}
}
fieldOrder() 返回 null 表示使用默认顺序。
SCX FFI 只处理结构体中的:
public
非 static
字段
也就是说,下面这些字段不会参与结构体布局:
private int a;
protected int b;
public static int c;
结构体字段不允许为 null。
尤其是嵌套结构体字段和 MemorySegment 字段,需要在调用前初始化好。
例如:
public static class Outer implements FFIStruct {
public Inner inner = new Inner();
}
而不是:
public static class Outer implements FFIStruct {
public Inner inner;
}
SCX FFI 不支持递归嵌套结构体。
例如下面这种结构是不支持的:
public static class Node implements FFIStruct {
public int value;
public Node next;
}
因为它无法生成固定大小的结构体布局。
如果 native 结构体中包含指针字段,应使用 MemorySegment 表示指针:
public static class Node implements FFIStruct {
public int value;
public MemorySegment next;
}
如果 native 函数需要函数指针,可以使用 FFICallback。
回调对象需要实现 FFICallback,并提供一个回调方法。
默认回调方法名是:
callback
示例:
import dev.scx.ffi.type.FFICallback;
import java.lang.foreign.MemorySegment;
public interface Compar extends FFICallback {
int callback(MemorySegment aAddr, MemorySegment bAddr);
}
然后声明 native 函数:
void qsort(int[] base, long nmemb, long size, Compar compar);
调用:
import static java.lang.foreign.ValueLayout.JAVA_INT;
var array = new int[]{2, 5, 7, 1};
C.C.qsort(array, array.length, Integer.BYTES, (aAddr, bAddr) -> {
int a = aAddr.get(JAVA_INT, 0);
int b = bAddr.get(JAVA_INT, 0);
return Integer.compare(a, b);
});
这里 Java lambda 会被转换成 native function pointer,然后传给 qsort。
如果回调方法不叫 callback,可以重写 callbackMethodName()。
public interface MyCallback extends FFICallback {
int apply(int value);
@Override
default String callbackMethodName() {
return "apply";
}
}
SCX FFI 会查找名为 apply 的方法作为回调入口。
回调函数当前支持下面这些参数类型:
byte
short
int
long
float
double
char
MemorySegment
回调函数当前支持下面这些返回值类型:
void
byte
short
int
long
float
double
char
MemorySegment
当回调参数是 MemorySegment,并且希望它指向具体类型数据时,可以通过 parameterTargetLayouts() 提供目标内存布局。
示例:
import dev.scx.ffi.type.FFICallback;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import static java.lang.foreign.ValueLayout.JAVA_INT;
public interface Compar extends FFICallback {
int callback(MemorySegment aAddr, MemorySegment bAddr);
@Override
default MemoryLayout[] parameterTargetLayouts() {
return new MemoryLayout[]{
JAVA_INT,
JAVA_INT
};
}
}
这样 aAddr 和 bAddr 就可以按 JAVA_INT 指向的数据来读取:
int a = aAddr.get(JAVA_INT, 0);
int b = bAddr.get(JAVA_INT, 0);
如果 parameterTargetLayouts() 返回 null,则 MemorySegment 参数会按普通地址处理。
如果内置类型无法满足需求,可以实现 FFMMapper。
接口定义可以理解为:
public interface FFMMapper {
MemorySegment toMemorySegment(Arena arena) throws Exception;
void fromMemorySegment(MemorySegment memorySegment) throws Exception;
}
其中:
toMemorySegment(...) 在调用 native 函数前执行。fromMemorySegment(...) 在 native 函数调用结束后执行。fromMemorySegment(...) 可以什么都不做。示例:
import dev.scx.ffi.mapper.FFMMapper;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import static java.lang.foreign.ValueLayout.JAVA_INT;
public final class IntBoxMapper implements FFMMapper {
private final IntBox box;
public IntBoxMapper(IntBox box) {
this.box = box;
}
@Override
public MemorySegment toMemorySegment(Arena arena) {
return arena.allocateFrom(JAVA_INT, box.value);
}
@Override
public void fromMemorySegment(MemorySegment memorySegment) {
box.value = memorySegment.get(JAVA_INT, 0);
}
}
使用时,接口方法参数可以直接写成该 mapper 类型:
int get_value(IntBoxMapper out);
调用:
var box = new IntBox();
lib.get_value(new IntBoxMapper(box));
System.out.println(box.value);
一般情况下,不需要直接使用内置 mapper。
推荐使用:
IntRef
int[]
String
EncodedString
FFIStruct
FFICallback
只有在内置映射无法表达你的 native 类型时,再实现 FFMMapper。
一次 FFI 方法调用大致经过下面几个步骤:
1. 进入代理方法
2. 创建 Arena.ofConfined()
3. 把 Java 参数包装成基本类型、MemorySegment 或 FFMMapper
4. 把 FFMMapper 转换成 MemorySegment
5. 调用 native MethodHandle
6. 对 FFMMapper 参数执行 fromMemorySegment 回写
7. 关闭 Arena
8. 返回 native 调用结果
示意代码:
try (var arena = Arena.ofConfined()) {
var wrappedParameters = wrapParameters(args);
var nativeParameters = prepareNativeParameters(wrappedParameters, arena);
var result = methodHandle.invokeWithArguments(nativeParameters);
writeBackParameters(wrappedParameters, nativeParameters);
return result;
}
因此需要注意:
MemorySegment,这段内存的生命周期由 native 侧决定,SCX FFI 不负责释放。下面是一个简化的 C 标准库接口示例。
import dev.scx.ffi.ScxFFI;
import dev.scx.ffi.annotation.SymbolName;
import dev.scx.ffi.type.FFICallback;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import static java.lang.foreign.ValueLayout.JAVA_INT;
public interface C extends CBase {
C C = ScxFFI.createFFI(C.class);
long strlen(String str);
@SymbolName("abs")
int javaAbs(int x);
double sqrt(double x);
void qsort(int[] base, long nmemb, long size, Compar compar);
default int abs(int x) {
return javaAbs(x);
}
interface Compar extends FFICallback {
int callback(MemorySegment aAddr, MemorySegment bAddr);
@Override
default MemoryLayout[] parameterTargetLayouts() {
return new MemoryLayout[]{
JAVA_INT,
JAVA_INT
};
}
}
}
父接口:
public interface CBase {
double sin(double x);
}
调用:
import static java.lang.foreign.ValueLayout.JAVA_INT;
long length = C.C.strlen("abc123456");
int abs = C.C.abs(-123);
double sin = C.C.sin(12);
double sqrt = C.C.sqrt(88);
var array = new int[]{2, 5, 7, 1, 4, 56, 12, 31, 99999, 90, 271, 2};
C.C.qsort(array, array.length, Integer.BYTES, (aAddr, bAddr) -> {
int a = aAddr.get(JAVA_INT, 0);
int b = bAddr.get(JAVA_INT, 0);
return Integer.compare(a, b);
});
调用结束后,array 会被排序。
下面是一个 Windows Kernel32 示例。
import dev.scx.ffi.ScxFFI;
import dev.scx.ffi.type.IntRef;
import java.lang.foreign.MemorySegment;
public interface Kernel32 {
Kernel32 KERNEL32 = ScxFFI.createFFI(Kernel32.class, "kernel32");
MemorySegment GetStdHandle(int nStdHandle);
int GetConsoleMode(MemorySegment hConsoleHandle, IntRef lpMode);
int SetConsoleMode(MemorySegment hConsoleHandle, long dwMode);
}
调用:
var handle = Kernel32.KERNEL32.GetStdHandle(-11);
var mode = new IntRef();
int ok = Kernel32.KERNEL32.GetConsoleMode(handle, mode);
if (ok != 0) {
Kernel32.KERNEL32.SetConsoleMode(handle, mode.value());
}
这里 IntRef 用来接收 GetConsoleMode 写出的控制台模式。
下面是一个 Windows User32 示例。
import dev.scx.ffi.ScxFFI;
import dev.scx.ffi.type.EncodedString;
import java.lang.foreign.MemorySegment;
public interface User32 {
User32 USER32 = ScxFFI.createFFI(User32.class, "user32");
int MessageBoxA(MemorySegment hWnd, String lpText, String lpCaption, int uType);
int MessageBoxW(MemorySegment hWnd, EncodedString lpText, EncodedString lpCaption, int uType);
int GetCursorPos(POINT lpPoint);
int SetCursorPos(int x, int y);
}
结构体:
import dev.scx.ffi.type.FFIStruct;
public static class POINT implements FFIStruct {
public int x;
public int y;
}
调用 ANSI 版本:
User32.USER32.MessageBoxA(
null,
"MessageBoxA 测试中文内容",
"测试标题",
0
);
调用 Unicode 版本:
import dev.scx.ffi.type.EncodedString;
import static java.nio.charset.StandardCharsets.UTF_16LE;
User32.USER32.MessageBoxW(
null,
new EncodedString("MessageBoxW 测试中文内容", UTF_16LE),
new EncodedString("测试标题", UTF_16LE),
0
);
获取鼠标位置:
var point = new POINT();
User32.USER32.GetCursorPos(point);
System.out.println(point.x);
System.out.println(point.y);
SCX FFI 不使用额外的 IDL 文件,也不需要手写 MethodHandle。
你只需要声明 Java 接口:
public interface C {
long strlen(String str);
}
然后:
var c = ScxFFI.createFFI(C.class);
抽象方法会被映射为 native symbol。
默认情况下,方法名就是 symbol 名。
long strlen(String str);
会查找:
strlen
如果需要修改 symbol 名,使用 @SymbolName。
Object 方法会直接调用代理自身的方法。
例如:
toString()
hashCode()
equals(...)
接口默认方法也会直接作为 Java 默认方法执行。
只有抽象方法才会映射到 native symbol。
调用 native 函数之前,SCX FFI 会先把参数转换为内部统一形式:
基本类型
MemorySegment
FFMMapper
然后再把所有 FFMMapper 转换为 MemorySegment。
这样可以让不同高级类型共用同一套调用流程。
对于 Ref、数组、结构体等可写参数,调用结束后会从 native 内存回写。
例如:
IntRef
int[]
FFIStruct
对于只读参数,回写方法会忽略。
例如:
String
EncodedString
FFICallback
每次代理方法调用都会创建一个新的 Arena.ofConfined()。
这让一次调用中的临时内存能够自动释放。
但也意味着:
如果 native 侧需要保存指针或回调,需要自己设计更长生命周期的内存管理方式。
FFI 调用本质上仍然是 native 调用。
SCX FFI 可以简化接口声明和参数转换,但不能消除下面这些风险:
因此使用时仍然需要对照 native API 文档确认函数签名。
它更接近一个基于 JDK FFM API 的轻量封装。
它不使用 JNI 代码,也不是 JNA 的完整替代品。它的目标是让简单 native 函数可以通过 Java 接口快速调用。
是的。
ScxFFI.createFFI(...) 内部使用 Java 动态代理创建对象,因此传入类型应该是接口。
使用 @SymbolName。
@SymbolName("abs")
int javaAbs(int x);
可以。
默认方法不会查找 native symbol,而是直接执行 Java 默认方法。
可以。
父接口中的抽象方法也会被扫描并创建 native MethodHandle。
当前不支持。
参数和返回值支持的基本类型是:
byte
short
int
long
float
double
char
如果 native API 使用布尔值,通常可以根据平台 ABI 使用 int 或其它整数类型表达。
当前不支持。
如果 native 函数返回字符串指针,应使用 MemorySegment 接收,然后由调用者按正确编码读取。
可以。
String 会被转换为 native 字符串内存。
如果需要指定编码,使用 EncodedString。
不会。
Java String 是不可变对象,因此 String 参数是只读映射。
如果 native 函数需要写入字符串缓冲区,可以使用 byte[] 或 char[]。
传入 null 即可。
SCX FFI 会把 null 转换为:
MemorySegment.NULL
使用 Ref 类型。
例如:
var out = new IntRef();
lib.get_value(out);
int value = out.value();
直接使用基本类型数组。
var array = new int[]{1, 2, 3};
lib.process(array, array.length);
调用结束后,native 内存会回写到原数组。
不会。
SCX FFI 会按 Java 数组长度分配内存,但 native 函数如果写越界,仍然是 native 侧的问题。
实现 FFIStruct,并使用 public 非 static 字段。
public static class POINT implements FFIStruct {
public int x;
public int y;
}
不可以。
当前只处理 public 非 static 字段。
重写 fieldOrder()。
@Override
public String[] fieldOrder() {
return new String[]{"x", "y"};
}
支持非递归嵌套。
结构体字段可以继续是 FFIStruct。
不直接支持递归结构体。
链表这种自引用结构应使用 MemorySegment 表示指针字段。
定义一个继承 FFICallback 的接口,并提供 callback 方法。
public interface Compar extends FFICallback {
int callback(MemorySegment aAddr, MemorySegment bAddr);
}
然后把 lambda 或实现对象传给 native 函数。
默认是 callback。
如果不是,可以重写:
default String callbackMethodName() {
return "apply";
}
创建 FFI MethodHandle 时会查找对应 symbol。
如果找不到,会抛出 IllegalArgumentException。
每次调用结束后,代理方法内部创建的 Arena.ofConfined() 会关闭。
因此本次调用中分配的临时字符串、数组、结构体、回调 stub 等内存都会随之失效。
一般不应该保存由 SCX FFI 临时分配的指针。
这些指针只适合本次调用期间使用。
如果 native 侧需要长期保存指针,需要调用者自己管理生命周期更长的 native 内存。
不会。
如果 native 函数返回 MemorySegment,这块内存的生命周期由 native 侧决定。SCX FFI 不会自动释放它。
常见原因包括:
FFI 调用绕过了 Java 的大部分安全边界,因此签名必须和 native API 完全匹配。