You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 11:34:31