Java 22 FFM在Windows平台创建含结构体与指针参数的Upcall时的内存会话异常问题
Java 22 FFM在Windows平台创建含结构体与指针参数的Upcall时的内存会话异常问题
我最近在Windows平台上使用Java 22的Foreign Function & Memory (FFM) API创建Upcall存根时遇到了一个跨平台不一致的问题:当回调函数同时包含大于指针大小的结构体和指针参数时,接收指针参数的MemorySegment会被绑定到一个受限的内存会话,而Linux下的JVM则没有这个问题。
最小复现示例(MWE)
C端代码(ccb.c)
#include <stdio.h> #include <stdint.h> typedef struct { char const *s1; char const *s2; } S; typedef void (*callback_fn)(S s, char const *data); #ifdef _MSC_VER #define EXPORT __declspec(dllexport) #else #define EXPORT __attribute__((visibility("default"))) #endif EXPORT extern void ccb(callback_fn fn) { char const *data = "Let's be together, forever, we are never gonna be apart."; fprintf(stderr, "(C) address of data = %p\n", (void*)data); fn( ((S) { "Shall I leave you be, Is it love if I can set you free?", "But even it's not reality,", }), data ); }
这里C侧的data是静态字符串,并非Java侧分配的内存。
Java端代码(CCB.java)
import java.lang.foreign.*; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; public final class CCB { static final StructLayout LAYOUT$S = MemoryLayout.structLayout( ValueLayout.ADDRESS.withTargetLayout(ValueLayout.JAVA_BYTE).withName("s1"), ValueLayout.ADDRESS.withTargetLayout(ValueLayout.JAVA_BYTE).withName("s2") ); static final FunctionDescriptor DESCRIPTOR$ccb = FunctionDescriptor.ofVoid(ValueLayout.ADDRESS.withName("fn")); static final FunctionDescriptor DESCRIPTOR$callback = FunctionDescriptor.ofVoid( LAYOUT$S.withName("s"), ValueLayout.ADDRESS.withTargetLayout(ValueLayout.JAVA_BYTE).withName("data") ); static final MethodHandle hCCB; static { System.loadLibrary("ccb"); Linker linker = Linker.nativeLinker(); SymbolLookup stdlibLookup = linker.defaultLookup(); SymbolLookup loaderLookup = SymbolLookup.loaderLookup(); MemorySegment pfnCCB = loaderLookup.find("ccb") .or(() -> stdlibLookup.find("ccb")) .orElse(MemorySegment.NULL); if (pfnCCB.equals(MemorySegment.NULL)) { throw new RuntimeException("Failed to find ccb symbol"); } hCCB = linker.downcallHandle(pfnCCB, DESCRIPTOR$ccb); } static final class Ref<T> { T value; } @FunctionalInterface interface MemorySegmentConsumer { void accept(MemorySegment segment); } static void callback( MemorySegmentConsumer consumer, MemorySegment s, MemorySegment data ) { for (int i = 0; i < 2; i++) { MemorySegment segment = s.getAtIndex(ValueLayout.ADDRESS, i).reinterpret(Long.MAX_VALUE); System.err.println("(J) callback: s->s" + (i + 1) + " = " + segment.getString(0)); } data = data.reinterpret(Long.MAX_VALUE); System.err.println("(J) callback: data = " + data.getString(0)); System.err.println("(J) callback: address of data = " + Long.toUnsignedString(data.address(), 16)); consumer.accept(data); } public static void main(String[] args) { Ref<MemorySegment> ref = new Ref<>(); MemorySegmentConsumer consumer = segment -> ref.value = segment; try (Arena arena = Arena.ofConfined()) { Linker linker = Linker.nativeLinker(); MethodHandle MH$callback = MethodHandles.lookup().findStatic( CCB.class, "callback", DESCRIPTOR$callback.toMethodType().insertParameterTypes(0, MemorySegmentConsumer.class) ); MemorySegment pfnCallback = linker.upcallStub( MH$callback.bindTo(consumer), DESCRIPTOR$callback, arena ); hCCB.invokeExact(pfnCallback); System.err.println("(J) main: data = " + ref.value.getString(0)); // <-- 错误发生在此处 } catch (Throwable e) { throw new RuntimeException(e); } } }
编译命令
# 使用GCC编译C动态库 gcc.exe ccb.c -shared -fPIC -o ccb.dll # 使用MSVC编译C动态库 cl.exe /utf-8 /nologo /LD /MD /DWIN32 /Zi ccb.c /Fe:ccb.dll /link User32.lib
程序输出与错误栈
(C) address of data = 00007FFFA5009000 (J) callback: s->s1 = Shall I leave you be, Is it love if I can set you free? (J) callback: s->s2 = But even it's not reality, (J) callback: data = Let's be together, forever, we are never gonna be apart. (J) callback: address of data = 7fffa5009000 Exception in thread "main" java.lang.RuntimeException: java.lang.IllegalStateException: Already closed at CCB.main(CCB.java:81) Caused by: java.lang.IllegalStateException: Already closed at java.base/jdk.internal.foreign.MemorySessionImpl.alreadyClosed(MemorySessionImpl.java:318) at java.base/jdk.internal.misc.ScopedMemoryAccess$ScopedAccessError.newRuntimeException(ScopedMemoryAccess.java:114) at java.base/jdk.internal.misc.ScopedMemoryAccess.getLongUnaligned(ScopedMemoryAccess.java:2574) at java.base/jdk.internal.foreign.StringSupport.strlenByte(StringSupport.java:142) at java.base/jdk.internal.foreign.StringSupport.readByte(StringSupport.java:71) at java.base/jdk.internal.foreign.StringSupport.read(StringSupport.java:54) at java.base/jdk.internal.foreign.AbstractMemorySegmentImpl.getString(AbstractMemorySegmentImpl.java:907)
问题核心分析
在回调函数中,我们已经尝试通过data.reinterpret(Long.MAX_VALUE)将MemorySegment从原会话中解绑,理论上之后这个Segment应该可以在任意会话中访问。但在Windows平台下,回到main方法访问保存的ref.value时依然触发了会话已关闭的异常,而Linux平台下运行相同代码则不会出现这个问题,说明Windows JVM在处理同时包含大结构体和指针参数的Upcall时,对指针参数的MemorySegment会话绑定逻辑存在跨平台差异。
内容来源于stack exchange
相关产品推荐
相关产品推荐

