如何使用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
相关产品推荐
相关产品推荐

