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

如何使用Java 22+ FFM API通过Windows API获取开始菜单与桌面路径?

使用Java 22+ FFM API获取Windows开始菜单和桌面位置

要替代JNA实现获取Windows已知文件夹路径,我们可以通过FFM API直接调用Windows原生API SHGetKnownFolderPath(来自shell32.dll),同时需要处理COM环境初始化和内存管理。以下是完整实现:

关键说明

  • 必须先初始化COM环境,SHGetKnownFolderPath依赖COM运行时
  • 调用API后需手动释放返回的字符串内存(使用CoTaskMemFree)
  • 需启用JDK预览特性才能使用FFM API

完整代码

import jdk.incubator.foreign.*;
import java.lang.invoke.MethodHandle;
import java.nio.charset.StandardCharsets;

public class KnownFolderFetcher {
    // GUID结构体的内存布局
    private static final StructLayout GUID_LAYOUT = MemoryLayout.structLayout(
            ValueLayout.JAVA_INT.withName("data1"),
            ValueLayout.JAVA_SHORT.withName("data2"),
            ValueLayout.JAVA_SHORT.withName("data3"),
            MemoryLayout.sequenceLayout(8, ValueLayout.JAVA_BYTE).withName("data4")
    ).withByteAlignment(8);

    // SHGetKnownFolderPath函数的描述符
    private static final FunctionDescriptor SH_GET_KNOWN_FOLDER_PATH_DESC = FunctionDescriptor.of(
            ValueLayout.JAVA_INT, // 返回值:HRESULT
            GUID_LAYOUT, // 参数1:已知文件夹ID的GUID
            ValueLayout.JAVA_LONG, // 参数2:标志位(传0即可)
            ValueLayout.ADDRESS, // 参数3:用户令牌(传0表示当前用户)
            ValueLayout.ADDRESS.withTargetLayout(ValueLayout.ADDRESS) // 参数4:输出路径指针的指针
    );

    public static void main(String[] args) throws Throwable {
        // 初始化COM环境
        int coInitResult = CoInitializeEx(0, 0);
        if (coInitResult < 0) {
            throw new RuntimeException("COM初始化失败,错误码:0x" + Integer.toHexString(coInitResult));
        }

        try {
            // 获取常用文件夹路径
            System.out.println("桌面路径:" + getKnownFolderPath("B4BFCC3A-DB2C-424C-B029-7FE99A87C641"));
            System.out.println("当前用户开始菜单:" + getKnownFolderPath("A77F5D77-2E2B-44C3-A6A2-ABA601054A51"));
            System.out.println("公共开始菜单:" + getKnownFolderPath("0139D44E-6AFE-49F2-8690-3DAFCAE6FFB8"));
            System.out.println("公共桌面路径:" + getKnownFolderPath("DFDF76A2-C82A-4D63-906A-5644AC457385"));
        } finally {
            // 释放COM环境
            CoUninitialize();
        }
    }

    private static String getKnownFolderPath(String guidStr) throws Throwable {
        // 解析GUID字符串为内存段
        MemorySegment guidSegment = parseGuid(guidStr);

        try (ResourceScope scope = ResourceScope.newConfinedScope()) {
            // 加载shell32.dll并获取函数句柄
            SymbolLookup shell32 = SymbolLookup.loaderLookup().lookup("shell32.dll").orElseThrow();
            MethodHandle shGetKnownFolderPath = CLinker.getInstance().downcallHandle(
                    shell32.lookup("SHGetKnownFolderPath").orElseThrow(),
                    SH_GET_KNOWN_FOLDER_PATH_DESC,
                    scope
            );

            // 分配内存存储返回的路径指针
            MemorySegment pathPtrSegment = MemorySegment.allocateNative(ValueLayout.ADDRESS, scope);

            // 调用Windows API
            int hResult = (int) shGetKnownFolderPath.invoke(guidSegment, 0L, 0L, pathPtrSegment);
            if (hResult != 0) { // S_OK的HRESULT值为0
                throw new RuntimeException("获取路径失败,错误码:0x" + Integer.toHexString(hResult));
            }

            // 读取返回的路径内存段
            MemorySegment pathSegment = pathPtrSegment.get(ValueLayout.ADDRESS, 0);
            if (pathSegment.address() == 0) {
                throw new RuntimeException("未获取到有效路径");
            }

            try {
                // 将WCHAR字符串转换为Java字符串
                return wideCharToString(pathSegment);
            } finally {
                // 释放API返回的内存
                CoTaskMemFree(pathSegment);
            }
        }
    }

    // 解析GUID字符串为内存结构
    private static MemorySegment parseGuid(String guidStr) {
        String[] parts = guidStr.split("-");
        if (parts.length != 5) {
            throw new IllegalArgumentException("无效的GUID格式:" + guidStr);
        }

        MemorySegment segment = MemorySegment.allocateNative(GUID_LAYOUT);
        segment.set(ValueLayout.JAVA_INT, 0, Integer.parseUnsignedInt(parts[0], 16));
        segment.set(ValueLayout.JAVA_SHORT, 4, Short.parseShort(parts[1], 16));
        segment.set(ValueLayout.JAVA_SHORT, 6, Short.parseShort(parts[2], 16));

        String data4 = parts[3] + parts[4];
        for (int i = 0; i < 8; i++) {
            byte b = (byte) Integer.parseUnsignedInt(data4.substring(i*2, i*2+2), 16);
            segment.set(ValueLayout.JAVA_BYTE, 8 + i, b);
        }
        return segment;
    }

    // 将WCHAR*转换为Java字符串
    private static String wideCharToString(MemorySegment segment) {
        int length = 0;
        // 查找字符串结束符(两个连续的0字节)
        while (segment.get(ValueLayout.JAVA_SHORT, length * 2) != 0) {
            length++;
        }
        byte[] bytes = segment.asSlice(0, length * 2).toByteArray();
        return new String(bytes, StandardCharsets.UTF_16LE);
    }

    // 调用CoInitializeEx初始化COM
    private static int CoInitializeEx(int pvReserved, int dwCoInit) throws Throwable {
        try (ResourceScope scope = ResourceScope.newConfinedScope()) {
            SymbolLookup ole32 = SymbolLookup.loaderLookup().lookup("ole32.dll").orElseThrow();
            MethodHandle handle = CLinker.getInstance().downcallHandle(
                    ole32.lookup("CoInitializeEx").orElseThrow(),
                    FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.JAVA_INT),
                    scope
            );
            return (int) handle.invoke(MemorySegment.NULL, dwCoInit);
        }
    }

    // 调用CoUninitialize释放COM
    private static void CoUninitialize() throws Throwable {
        try (ResourceScope scope = ResourceScope.newConfinedScope()) {
            SymbolLookup ole32 = SymbolLookup.loaderLookup().lookup("ole32.dll").orElseThrow();
            MethodHandle handle = CLinker.getInstance().downcallHandle(
                    ole32.lookup("CoUninitialize").orElseThrow(),
                    FunctionDescriptor.ofVoid(),
                    scope
            );
            handle.invoke();
        }
    }

    // 调用CoTaskMemFree释放内存
    private static void CoTaskMemFree(MemorySegment ptr) throws Throwable {
        try (ResourceScope scope = ResourceScope.newConfinedScope()) {
            SymbolLookup ole32 = SymbolLookup.loaderLookup().lookup("ole32.dll").orElseThrow();
            MethodHandle handle = CLinker.getInstance().downcallHandle(
                    ole32.lookup("CoTaskMemFree").orElseThrow(),
                    FunctionDescriptor.ofVoid(ValueLayout.ADDRESS),
                    scope
            );
            handle.invoke(ptr);
        }
    }
}

编译与运行

编译和运行时需要启用预览特性并添加FFM模块:

javac --enable-preview --add-modules jdk.incubator.foreign KnownFolderFetcher.java
java --enable-preview --add-modules jdk.incubator.foreign KnownFolderFetcher

常用Known Folder GUID对应表

  • 桌面:B4BFCC3A-DB2C-424C-B029-7FE99A87C641
  • 当前用户开始菜单:A77F5D77-2E2B-44C3-A6A2-ABA601054A51
  • 公共开始菜单:0139D44E-6AFE-49F2-8690-3DAFCAE6FFB8
  • 公共桌面:DFDF76A2-C82A-4D63-906A-5644AC457385

内容的提问来源于stack exchange,提问作者zebra

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 23:04:54