Skip to content

Commit

Permalink
Merge pull request #6 from coursier/foreign-api-jdk-23
Browse files Browse the repository at this point in the history
Add upstream repo support for Windows API calls via JDK foreign API stuff
  • Loading branch information
alexarchambault authored Jan 6, 2025
2 parents edeeaa5 + 1a0225c commit 2c96f4d
Show file tree
Hide file tree
Showing 15 changed files with 593 additions and 184 deletions.
134 changes: 130 additions & 4 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import $ivy.`de.tototec::de.tobiasroeser.mill.vcs.version::0.4.1`

import de.tobiasroeser.mill.vcs.version.VcsVersion
import mill._
import mill.define.ModuleRef
import mill.scalalib._
import mill.scalalib.publish._

object directories extends JavaModule with PublishModule {
import scala.util.Properties

trait DirectoriesPublishModule extends PublishModule {
def pomSettings = PomSettings(
description = "directories-jvm",
organization = "io.get-coursier.util",
Expand Down Expand Up @@ -33,7 +36,9 @@ object directories extends JavaModule with PublishModule {
}
else value
}
}

object directories extends JavaModule with DirectoriesPublishModule {
def javacOptions = super.javacOptions() ++ Seq(
"--release", "8"
)
Expand All @@ -45,14 +50,135 @@ object directories extends JavaModule with PublishModule {
Seq(PathRef(T.workspace / "src/main"))
}

def jdk23ClassesResources = T {
val destDir = T.dest / "META-INF/versions/23"
os.makeDir.all(destDir)
for (elem <- os.list(jdk23.compile().classes.path))
os.copy(elem, destDir / elem.last)
PathRef(T.dest)
}

def resources = T {
T.sources(super.resources() ++ Seq(jdk23ClassesResources()))
}
def manifest = T {
super.manifest().add("Multi-Release" -> "true")
}

def localRepo = Task {
val dest = Task.dest

new LocalIvyPublisher(T.dest).publishLocal(
jar = jar().path,
sourcesJar = sourceJar().path,
docJar = docJar().path,
pom = pom().path,
ivy = ivy().path,
artifact = artifactMetadata(),
extras = extraPublish()
)

PathRef(dest)
}
}

object `directories-jni` extends JavaModule with DirectoriesPublishModule {
def moduleDeps = Seq(directories)
def ivyDeps = Agg(
ivy"io.get-coursier.jniutils:windows-jni-utils:0.3.3"
)
def javacOptions = super.javacOptions() ++ Seq(
"--release", "8"
)

def localRepo = Task {
val dest = Task.dest

new LocalIvyPublisher(T.dest).publishLocal(
jar = jar().path,
sourcesJar = sourceJar().path,
docJar = docJar().path,
pom = pom().path,
ivy = ivy().path,
artifact = artifactMetadata(),
extras = extraPublish()
)

PathRef(dest)
}
}

object jdk23 extends JavaModule {
def moduleDeps = Seq(directories)
def javacOptions = super.javacOptions() ++ Seq(
"--release", "23"
)
}

object java8ZincWorker extends ZincWorkerModule {
override def jvmId =
if (Properties.isMac) "zulu:8"
else "8"
}

trait Tests extends Cross.Module[String] with JavaModule {
def zincWorker = crossValue match {
case "8" => ModuleRef(java8ZincWorker)
case "default" => super.zincWorker
}

def ivyDeps = Agg(
ivy"${directories.pomSettings().organization}:directories:${directories.publishVersion()}"
)
def repositoriesTask = Task.Anon {
Seq(
coursier.parse.RepositoryParser.repository(
"ivy:" + directories.localRepo().path.toNIO.toUri.toASCIIString + "[defaultPattern]"
).fold(err => throw new Exception(err), x => x)
) ++ super.repositoriesTask()
}

object test extends JavaTests {
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"junit:junit:4.13",
ivy"com.novocode:junit-interface:0.11"
)
def testFramework = "com.novocode.junit.JUnitFramework"
}
}

trait TestsJni extends Cross.Module[String] with JavaModule {
def zincWorker = crossValue match {
case "8" => ModuleRef(java8ZincWorker)
case "default" => super.zincWorker
}

def ivyDeps = Agg(
ivy"${`directories-jni`.pomSettings().organization}:directories-jni:${`directories-jni`.publishVersion()}"
)
def repositoriesTask = Task.Anon {
Seq(
coursier.parse.RepositoryParser.repository(
"ivy:" + directories.localRepo().path.toNIO.toUri.toASCIIString + "[defaultPattern]"
).fold(err => throw new Exception(err), x => x),
coursier.parse.RepositoryParser.repository(
"ivy:" + `directories-jni`.localRepo().path.toNIO.toUri.toASCIIString + "[defaultPattern]"
).fold(err => throw new Exception(err), x => x)
) ++ super.repositoriesTask()
}

object test extends JavaTests {
def sources = T.sources {
Seq(PathRef(T.workspace / "src/test"))
}
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"junit:junit:4.13",
ivy"com.novocode:junit-interface:0.11"
)
def testFramework = "com.novocode.junit.JUnitFramework"
}
}

object tests extends Cross[Tests]("8", "default")

object `tests-jni` extends Cross[TestsJni](
if (Properties.isWin) Seq("8", "default")
else Seq.empty[String]
)
32 changes: 32 additions & 0 deletions directories-jni/src/dev/dirs/jni/WindowsJni.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package dev.dirs.jni;

import coursier.jniutils.WindowsKnownFolders;
import dev.dirs.impl.Windows;

import java.util.function.Supplier;

public final class WindowsJni implements Windows {

public String[] winDirs(String... folderIds) {
String[] dirs = new String[folderIds.length];
for (int i = 0; i < folderIds.length; i++) {
dirs[i] = WindowsKnownFolders.knownFolderPath("{" + folderIds[i] + "}");
}
return dirs;
}

public static Supplier<Windows> getJdkAwareSupplier() {
String javaVersion = System.getProperty("java.version", "0");
if (javaVersion.substring(0, "1.".length()).equals("1."))
javaVersion = javaVersion.substring("1.".length());
int dotIdx = javaVersion.indexOf('.');
if (dotIdx >= 0)
javaVersion = javaVersion.substring(0, dotIdx);
int jdkVersion = Integer.parseInt(javaVersion);
if (jdkVersion >= 23)
return Windows.getDefaultSupplier();
else
return () -> new WindowsJni();
}

}
11 changes: 11 additions & 0 deletions jdk23/src/dev/dirs/impl/WindowsDefault.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dev.dirs.impl;

import java.util.function.Supplier;

final class WindowsDefault {

static Supplier<Windows> getDefaultSupplier() {
return () -> new WindowsForeign();
}

}
146 changes: 146 additions & 0 deletions jdk23/src/dev/dirs/impl/WindowsForeign.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package dev.dirs.impl;

import dev.dirs.Constants;

import java.lang.foreign.AddressLayout;
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.GroupLayout;
import java.lang.foreign.Linker;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;

import static java.lang.foreign.ValueLayout.JAVA_BYTE;
import static java.lang.foreign.ValueLayout.JAVA_CHAR;

public final class WindowsForeign implements Windows {

public WindowsForeign() {}

static {
if (Constants.operatingSystem == 'w') {
System.loadLibrary("ole32");
System.loadLibrary("shell32");
}
}

private static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup().or(Linker.nativeLinker().defaultLookup());
private static final ValueLayout.OfByte C_CHAR = ValueLayout.JAVA_BYTE;
private static final ValueLayout.OfShort C_SHORT = ValueLayout.JAVA_SHORT;
private static final AddressLayout C_POINTER = ValueLayout.ADDRESS
.withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, JAVA_BYTE));
private static final ValueLayout.OfInt C_LONG = ValueLayout.JAVA_INT;

private static String getDir(String folderId) {
try (var arena = Arena.ofConfined()) {
MemorySegment guidSegment = arena.allocate(GUID_LAYOUT);
if (CLSIDFromString(createSegmentFromString(folderId, arena), guidSegment) != 0) {
throw new AssertionError("failed converting string " + folderId + " to KnownFolderId");
}
MemorySegment path = arena.allocate(C_POINTER);
SHGetKnownFolderPath(guidSegment, 0, MemorySegment.NULL, path);
return createStringFromSegment(path.get(C_POINTER, 0));
}
}

public String[] winDirs(String... folderIds) {
String[] values = new String[folderIds.length];
for (int i = 0; i < folderIds.length; i += 1)
values[i] = getDir("{" + folderIds[i] + "}");
return values;
}

/**
* Creates a memory segment as a copy of a Java string.
* <p>
* The memory segment contains a copy of the string (null-terminated, UTF-16/wide characters).
* </p>
*
* @param str the string to copy
* @param arena the arena for the memory segment
* @return the resulting memory segment
*/
private static MemorySegment createSegmentFromString(String str, Arena arena) {
// allocate segment (including space for terminating null)
var segment = arena.allocate(JAVA_CHAR, str.length() + 1L);
// copy characters
segment.copyFrom(MemorySegment.ofArray(str.toCharArray()));
return segment;
}

/**
* Creates a copy of the string in the memory segment.
* <p>
* The string must be a null-terminated UTF-16 (wide character) string.
* </p>
*
* @param segment the memory segment
* @return copied string
*/
private static String createStringFromSegment(MemorySegment segment) {
var len = 0;
while (segment.get(JAVA_CHAR, len) != 0) {
len += 2;
}

return new String(segment.asSlice(0, len).toArray(JAVA_CHAR));
}

private static MemorySegment findOrThrow(String symbol) {
return SYMBOL_LOOKUP.find(symbol)
.orElseThrow(() -> new UnsatisfiedLinkError("unresolved symbol: " + symbol));
}

private static final GroupLayout GUID_LAYOUT = MemoryLayout.structLayout(
C_LONG.withName("Data1"),
C_SHORT.withName("Data2"),
C_SHORT.withName("Data3"),
MemoryLayout.sequenceLayout(8, C_CHAR).withName("Data4"))
.withName("_GUID");

/**
* {@snippet lang=c :
* extern HRESULT CLSIDFromString(LPCOLESTR lpsz, LPCLSID pclsid)
* }
*/
private static int CLSIDFromString(MemorySegment lpsz, MemorySegment pclsid) {
var handle = CLSIDFromString.HANDLE;
try {
return (int) handle.invokeExact(lpsz, pclsid);
} catch (Throwable throwable) {
throw new AssertionError("failed to invoke `CLSIDFromString`", throwable);
}
}

private static class CLSIDFromString {
public static final FunctionDescriptor DESC = FunctionDescriptor.of(C_LONG, C_POINTER, C_POINTER);

public static final MethodHandle HANDLE = Linker.nativeLinker()
.downcallHandle(findOrThrow("CLSIDFromString"), DESC);
}

/**
* {@snippet lang=c :
* extern HRESULT SHGetKnownFolderPath(const KNOWNFOLDERID *const rfid, DWORD dwFlags, HANDLE hToken, PWSTR *ppszPath)
* }
*/
private static int SHGetKnownFolderPath(MemorySegment rfid, int dwFlags, MemorySegment hToken, MemorySegment ppszPath) {
var handle = SHGetKnownFolderPath.HANDLE;
try {
return (int) handle.invokeExact(rfid, dwFlags, hToken, ppszPath);
} catch (Throwable throwable) {
throw new AssertionError("failed to invoke `SHGetKnownFolderPath`", throwable);
}
}

private static class SHGetKnownFolderPath {
public static final FunctionDescriptor DESC = FunctionDescriptor.of(C_LONG, C_POINTER, C_LONG, C_POINTER, C_POINTER);

public static final MethodHandle HANDLE = Linker.nativeLinker()
.downcallHandle(findOrThrow("SHGetKnownFolderPath"), DESC);
}

}
12 changes: 9 additions & 3 deletions src/main/java/dev/dirs/BaseDirectories.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import dev.dirs.impl.Util;
import dev.dirs.impl.Windows;

import java.util.function.Supplier;

/** {@code BaseDirectories} provides paths of user-invisible standard directories, following the conventions of the operating system the library is running on.
* <p>
* To compute the location of cache, config or data directories for individual projects or applications, use {@link ProjectDirectories} instead.
Expand Down Expand Up @@ -245,10 +247,14 @@ public final class BaseDirectories {
* @return A new {@code BaseDirectories} instance.
*/
public static BaseDirectories get() {
return new BaseDirectories();
return new BaseDirectories(Windows.getDefaultSupplier());
}

public static BaseDirectories get(Supplier<Windows> windows) {
return new BaseDirectories(windows);
}

private BaseDirectories() {
private BaseDirectories(Supplier<Windows> windows) {
switch (Constants.operatingSystem) {
case Constants.LIN:
case Constants.BSD:
Expand All @@ -275,7 +281,7 @@ private BaseDirectories() {
runtimeDir = null;
break;
case Constants.WIN:
String[] winDirs = Windows.getWinDirs("5E6C858F-0E22-4760-9AFE-EA3317B67173", "3EB685DB-65F9-4CF6-A03A-E3EF65729F3D", "F1B32785-6FBA-4FCF-9D55-7B8E7F157091");
String[] winDirs = windows.get().winDirs("5E6C858F-0E22-4760-9AFE-EA3317B67173", "3EB685DB-65F9-4CF6-A03A-E3EF65729F3D", "F1B32785-6FBA-4FCF-9D55-7B8E7F157091");
homeDir = winDirs[0];
dataDir = winDirs[1];
dataLocalDir = winDirs[2];
Expand Down
Loading

0 comments on commit 2c96f4d

Please sign in to comment.