Skip to content
This repository was archived by the owner on Feb 18, 2025. It is now read-only.

Commit b6332c3

Browse files
brcolowsoc
authored andcommitted
Use Java 22's foreign function API for Windows
- Drop all existing mechanisms for retrieving this info on Windows - This increases the required Java version of the library to 22 Fixes #49.
1 parent c9cf98b commit b6332c3

File tree

2 files changed

+203
-3
lines changed

2 files changed

+203
-3
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@ jobs:
1111
strategy:
1212
matrix:
1313
os:
14+
- windows-latest
1415
- ubuntu-latest
1516

1617
runs-on: ${{ matrix.os }}
1718

1819
steps:
1920
- uses: actions/checkout@v4
20-
- name: Set up JDK 11
21+
- name: Set up JDK 22
2122
uses: actions/setup-java@v2
2223
with:
23-
java-version: '11'
24-
distribution: 'adopt'
24+
java-version: '22'
25+
distribution: 'temurin'
2526
- name: Run tests
2627
run: sbt test
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package dev.dirs.impl;
2+
3+
import dev.dirs.Constants;
4+
5+
import java.lang.foreign.AddressLayout;
6+
import java.lang.foreign.Arena;
7+
import java.lang.foreign.FunctionDescriptor;
8+
import java.lang.foreign.GroupLayout;
9+
import java.lang.foreign.Linker;
10+
import java.lang.foreign.MemoryLayout;
11+
import java.lang.foreign.MemorySegment;
12+
import java.lang.foreign.SymbolLookup;
13+
import java.lang.foreign.ValueLayout;
14+
import java.lang.invoke.MethodHandle;
15+
16+
import static dev.dirs.impl.Util.isNullOrEmpty;
17+
import static dev.dirs.impl.Util.stringLength;
18+
import static java.lang.foreign.ValueLayout.JAVA_BYTE;
19+
import static java.lang.foreign.ValueLayout.JAVA_CHAR;
20+
21+
public final class Windows {
22+
23+
private Windows() {}
24+
25+
static {
26+
if (Constants.operatingSystem == 'w') {
27+
System.loadLibrary("ole32");
28+
System.loadLibrary("shell32");
29+
}
30+
}
31+
32+
private static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup().or(Linker.nativeLinker().defaultLookup());
33+
private static final ValueLayout.OfByte C_CHAR = ValueLayout.JAVA_BYTE;
34+
private static final ValueLayout.OfShort C_SHORT = ValueLayout.JAVA_SHORT;
35+
private static final AddressLayout C_POINTER = ValueLayout.ADDRESS
36+
.withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, JAVA_BYTE));
37+
private static final ValueLayout.OfInt C_LONG = ValueLayout.JAVA_INT;
38+
39+
public static String getProfileDir() {
40+
return getDir("{5E6C858F-0E22-4760-9AFE-EA3317B67173}");
41+
}
42+
43+
public static String getMusicDir() {
44+
return getDir("{4BD8D571-6D19-48D3-BE97-422220080E43}");
45+
}
46+
47+
public static String getDesktopDir() {
48+
return getDir("{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}");
49+
}
50+
51+
public static String getDocumentsDir() {
52+
return getDir("{FDD39AD0-238F-46AF-ADB4-6C85480369C7}");
53+
}
54+
55+
public static String getDownloadsDir() {
56+
return getDir("{374DE290-123F-4565-9164-39C4925E467B}");
57+
}
58+
59+
public static String getPicturesDir() {
60+
return getDir("{33E28130-4E1E-4676-835A-98395C3BC3BB}");
61+
}
62+
63+
public static String getPublicDir() {
64+
return getDir("{DFDF76A2-C82A-4D63-906A-5644AC457385}");
65+
}
66+
67+
public static String getTemplatesDir() {
68+
return getDir("{A63293E8-664E-48DB-A079-DF759E0509F7}");
69+
}
70+
71+
public static String getVideosDir() {
72+
return getDir("{18989B1D-99B5-455B-841C-AB7C74E4DDFC}");
73+
}
74+
75+
public static String getRoamingAppDataDir() {
76+
return getDir("{3EB685DB-65F9-4CF6-A03A-E3EF65729F3D}");
77+
}
78+
79+
public static String getLocalAppDataDir() {
80+
return getDir("{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}");
81+
}
82+
83+
public static String applicationPath(String qualifier, String organization, String application) {
84+
StringBuilder buf = new StringBuilder(Math.max(stringLength(organization) + stringLength(application), 0));
85+
boolean orgPresent = !isNullOrEmpty(organization);
86+
boolean appPresent = !isNullOrEmpty(application);
87+
if (orgPresent) {
88+
buf.append(organization);
89+
if (appPresent)
90+
buf.append('\\');
91+
}
92+
if (appPresent)
93+
buf.append(application);
94+
return buf.toString();
95+
}
96+
97+
private static String getDir(String folderId) {
98+
try (var arena = Arena.ofConfined()) {
99+
MemorySegment guidSegment = arena.allocate(GUID_LAYOUT);
100+
if (CLSIDFromString(createSegmentFromString(folderId, arena), guidSegment) != 0) {
101+
throw new AssertionError("failed converting string " + folderId + " to KnownFolderId");
102+
}
103+
MemorySegment path = arena.allocate(C_POINTER);
104+
SHGetKnownFolderPath(guidSegment, 0, MemorySegment.NULL, path);
105+
return createStringFromSegment(path.get(C_POINTER, 0));
106+
}
107+
}
108+
109+
/**
110+
* Creates a memory segment as a copy of a Java string.
111+
* <p>
112+
* The memory segment contains a copy of the string (null-terminated, UTF-16/wide characters).
113+
* </p>
114+
*
115+
* @param str the string to copy
116+
* @param arena the arena for the memory segment
117+
* @return the resulting memory segment
118+
*/
119+
private static MemorySegment createSegmentFromString(String str, Arena arena) {
120+
// allocate segment (including space for terminating null)
121+
var segment = arena.allocate(JAVA_CHAR, str.length() + 1L);
122+
// copy characters
123+
segment.copyFrom(MemorySegment.ofArray(str.toCharArray()));
124+
return segment;
125+
}
126+
127+
/**
128+
* Creates a copy of the string in the memory segment.
129+
* <p>
130+
* The string must be a null-terminated UTF-16 (wide character) string.
131+
* </p>
132+
*
133+
* @param segment the memory segment
134+
* @return copied string
135+
*/
136+
private static String createStringFromSegment(MemorySegment segment) {
137+
var len = 0;
138+
while (segment.get(JAVA_CHAR, len) != 0) {
139+
len += 2;
140+
}
141+
142+
return new String(segment.asSlice(0, len).toArray(JAVA_CHAR));
143+
}
144+
145+
private static MemorySegment findOrThrow(String symbol) {
146+
return SYMBOL_LOOKUP.find(symbol)
147+
.orElseThrow(() -> new UnsatisfiedLinkError("unresolved symbol: " + symbol));
148+
}
149+
150+
private static final GroupLayout GUID_LAYOUT = MemoryLayout.structLayout(
151+
C_LONG.withName("Data1"),
152+
C_SHORT.withName("Data2"),
153+
C_SHORT.withName("Data3"),
154+
MemoryLayout.sequenceLayout(8, C_CHAR).withName("Data4"))
155+
.withName("_GUID");
156+
157+
/**
158+
* {@snippet lang=c :
159+
* extern HRESULT CLSIDFromString(LPCOLESTR lpsz, LPCLSID pclsid)
160+
* }
161+
*/
162+
private static int CLSIDFromString(MemorySegment lpsz, MemorySegment pclsid) {
163+
var handle = CLSIDFromString.HANDLE;
164+
try {
165+
return (int) handle.invokeExact(lpsz, pclsid);
166+
} catch (Throwable throwable) {
167+
throw new AssertionError("failed to invoke `CLSIDFromString`", throwable);
168+
}
169+
}
170+
171+
private static class CLSIDFromString {
172+
public static final FunctionDescriptor DESC = FunctionDescriptor.of(C_LONG, C_POINTER, C_POINTER);
173+
174+
public static final MethodHandle HANDLE = Linker.nativeLinker()
175+
.downcallHandle(findOrThrow("CLSIDFromString"), DESC);
176+
}
177+
178+
/**
179+
* {@snippet lang=c :
180+
* extern HRESULT SHGetKnownFolderPath(const KNOWNFOLDERID *const rfid, DWORD dwFlags, HANDLE hToken, PWSTR *ppszPath)
181+
* }
182+
*/
183+
private static int SHGetKnownFolderPath(MemorySegment rfid, int dwFlags, MemorySegment hToken, MemorySegment ppszPath) {
184+
var handle = SHGetKnownFolderPath.HANDLE;
185+
try {
186+
return (int) handle.invokeExact(rfid, dwFlags, hToken, ppszPath);
187+
} catch (Throwable throwable) {
188+
throw new AssertionError("failed to invoke `SHGetKnownFolderPath`", throwable);
189+
}
190+
}
191+
192+
private static class SHGetKnownFolderPath {
193+
public static final FunctionDescriptor DESC = FunctionDescriptor.of(C_LONG, C_POINTER, C_LONG, C_POINTER, C_POINTER);
194+
195+
public static final MethodHandle HANDLE = Linker.nativeLinker()
196+
.downcallHandle(findOrThrow("SHGetKnownFolderPath"), DESC);
197+
}
198+
199+
}

0 commit comments

Comments
 (0)