使用 JNA 调用本机 API (#3890)

This commit is contained in:
Glavo
2025-05-04 22:31:50 +08:00
committed by GitHub
parent f283254ca0
commit 1ab7ab0750
9 changed files with 301 additions and 34 deletions

View File

@@ -116,8 +116,14 @@ tasks.shadowJar {
exclude("META-INF/services/javax.imageio.spi.ImageReaderSpi")
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
listOf(
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*","freebsd-*", "linux-*", "darwin-*",
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
).forEach { exclude("com/sun/jna/$it/**") }
minimize {
exclude(dependency("com.google.code.gson:.*:.*"))
exclude(dependency("net.java.dev.jna:jna:.*"))
exclude(dependency("libs:JFoenix:.*"))
}

View File

@@ -38,6 +38,7 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
@@ -246,12 +247,13 @@ public final class Launcher extends Application {
LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
LOG.info("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory"));
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
LOG.info("Physical memory: " + OperatingSystem.TOTAL_MEMORY + " MB");
LOG.info("Physical Memory: " + OperatingSystem.TOTAL_MEMORY + " MB");
LOG.info("Metaspace: " + ManagementFactory.getMemoryPoolMXBeans().stream()
.filter(bean -> bean.getName().equals("Metaspace"))
.findAny()
.map(bean -> bean.getUsage().getUsed() / 1024 / 1024 + "MB")
.orElse("Unknown"));
LOG.info("Native Backend: " + (NativeUtils.USE_JNA ? "JNA" : "None"));
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE"));
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));

View File

@@ -15,5 +15,7 @@ dependencies {
api(libs.nanohttpd)
api(libs.jsoup)
api(libs.chardet)
api(libs.jna)
compileOnlyApi(libs.jetbrains.annotations)
}

View File

@@ -0,0 +1,85 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.platform;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Map;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class NativeUtils {
public static final boolean USE_JNA = useJNA();
public static <T extends Library> @Nullable T load(String name, Class<T> interfaceClass) {
return load(name, interfaceClass, Collections.emptyMap());
}
public static <T extends Library> @Nullable T load(String name, Class<T> interfaceClass, Map<String, ?> options) {
if (USE_JNA) {
try {
return Native.load(name, interfaceClass, options);
} catch (UnsatisfiedLinkError e) {
LOG.warning("Failed to load native library: " + name, e);
}
}
return null;
}
private static boolean useJNA() {
String backend = System.getProperty("hmcl.native.backend");
if (backend == null || "auto".equalsIgnoreCase(backend)) {
try {
if (Platform.isWindows()) {
String osVersion = System.getProperty("os.version");
// Requires Windows 7 or later (6.1+)
// https://learn.microsoft.com/windows/win32/sysinfo/operating-system-version
if (osVersion == null || osVersion.startsWith("5.") || osVersion.equals("6.0"))
return false;
// Currently we only need to use JNA on Windows
Native.getDefaultStringEncoding();
return true;
}
return false;
} catch (Throwable ignored) {
return false;
}
} else if ("jna".equalsIgnoreCase(backend)) {
// Ensure JNA is available
Native.getDefaultStringEncoding();
return true;
} else if ("none".equalsIgnoreCase(backend))
return false;
else
throw new Error("Unsupported native backend: " + backend);
}
private NativeUtils() {
}
}

View File

@@ -18,6 +18,8 @@
package org.jackhuang.hmcl.util.platform;
import org.jackhuang.hmcl.util.KeyValuePairProperties;
import org.jackhuang.hmcl.util.platform.windows.Kernel32;
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
import java.io.BufferedReader;
import java.io.File;
@@ -29,10 +31,7 @@ import java.nio.charset.UnsupportedCharsetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -159,47 +158,68 @@ public enum OperatingSystem {
if (CURRENT_OS == WINDOWS) {
String versionNumber = null;
int buildNumber = -1;
int codePage = -1;
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "ver"});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) {
Matcher matcher = Pattern.compile("(?<version>[0-9]+\\.[0-9]+\\.(?<build>[0-9]+)(\\.[0-9]+)?)]$")
.matcher(reader.readLine().trim());
Kernel32 kernel32 = Kernel32.INSTANCE;
if (matcher.find()) {
versionNumber = matcher.group("version");
buildNumber = Integer.parseInt(matcher.group("build"));
}
}
process.destroy();
} catch (Throwable ignored) {
// Get Windows version number
if (kernel32 != null) {
WinTypes.OSVERSIONINFOEXW osVersionInfo = new WinTypes.OSVERSIONINFOEXW();
if (kernel32.GetVersionExW(osVersionInfo)) {
int majorVersion = osVersionInfo.dwMajorVersion;
int minorVersion = osVersionInfo.dwMinorVersion;
buildNumber = osVersionInfo.dwBuildNumber;
versionNumber = majorVersion + "." + minorVersion + "." + buildNumber;
} else
System.err.println("Failed to obtain OS version number (" + kernel32.GetLastError() + ")");
}
if (versionNumber == null) {
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "ver"});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) {
Matcher matcher = Pattern.compile("(?<version>[0-9]+\\.[0-9]+\\.(?<build>[0-9]+)(\\.[0-9]+)?)]$")
.matcher(reader.readLine().trim());
if (matcher.find()) {
versionNumber = matcher.group("version");
buildNumber = Integer.parseInt(matcher.group("build"));
}
}
process.destroy();
} catch (Throwable ignored) {
}
}
if (versionNumber == null)
versionNumber = System.getProperty("os.version");
// Get Code Page
if (kernel32 != null)
codePage = kernel32.GetACP();
else {
try {
Process process = Runtime.getRuntime().exec(new String[]{"chcp.com"});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) {
Matcher matcher = Pattern.compile("(?<cp>[0-9]+)$")
.matcher(reader.readLine().trim());
if (matcher.find()) {
codePage = Integer.parseInt(matcher.group("cp"));
}
}
process.destroy();
} catch (Throwable ignored) {
}
}
String osName = System.getProperty("os.name");
// Java 17 or earlier recognizes Windows 11 as Windows 10
if (osName.equals("Windows 10") && buildNumber >= 22000) {
if (osName.equals("Windows 10") && buildNumber >= 22000)
osName = "Windows 11";
}
int codePage = -1;
try {
Process process = Runtime.getRuntime().exec(new String[]{"chcp.com"});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) {
Matcher matcher = Pattern.compile("(?<cp>[0-9]+)$")
.matcher(reader.readLine().trim());
if (matcher.find()) {
codePage = Integer.parseInt(matcher.group("cp"));
}
}
process.destroy();
} catch (Throwable ignored) {
}
SYSTEM_NAME = osName;
SYSTEM_VERSION = versionNumber;
@@ -443,4 +463,5 @@ public enum OperatingSystem {
public static final PhysicalMemoryStatus INVALID = new PhysicalMemoryStatus(0, -1);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.platform.windows;
import com.sun.jna.win32.StdCallLibrary;
import org.jackhuang.hmcl.util.platform.NativeUtils;
/**
* @author Glavo
*/
public interface Kernel32 extends StdCallLibrary {
Kernel32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows()
? NativeUtils.load("kernel32", Kernel32.class)
: null;
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror">GetLastError function</a>
*/
int GetLastError();
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/sysinfoapi/nf-sysinfoapi-getversionexw">GetVersionExW function</a>
*/
boolean GetVersionExW(WinTypes.OSVERSIONINFOEXW lpVersionInfo);
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/winnls/nf-winnls-getacp">GetACP function</a>
*/
int GetACP();
}

View File

@@ -0,0 +1,39 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.platform.windows;
/**
* @author Glavo
*/
public interface WinConstants {
// https://learn.microsoft.com/windows/win32/sysinfo/registry-key-security-and-access-rights
int KEY_READ = 0x20019;
// https://learn.microsoft.com/windows/win32/sysinfo/predefined-keys
long HKEY_CLASSES_ROOT = 0x80000000L;
long HKEY_CURRENT_USER = 0x80000001L;
long HKEY_LOCAL_MACHINE = 0x80000002L;
long HKEY_USERS = 0x80000003L;
long HKEY_PERFORMANCE_DATA = 0x80000004L;
long HKEY_PERFORMANCE_TEXT = 0x80000050L;
long HKEY_PERFORMANCE_NLSTEXT = 0x80000060L;
long HKEY_CURRENT_CONFIG = 0x80000005L;
long HKEY_DYN_DATA = 0x80000006L;
long HKEY_CURRENT_USER_LOCAL_SETTINGS = 0x80000007L;
}

View File

@@ -0,0 +1,63 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.platform.windows;
import com.sun.jna.*;
import java.util.Arrays;
import java.util.List;
/**
* @author Glavo
*/
public interface WinTypes {
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/winnt/ns-winnt-osversioninfoexw">OSVERSIONINFOEXW structure</a>
*/
final class OSVERSIONINFOEXW extends Structure {
public int dwOSVersionInfoSize;
public int dwMajorVersion;
public int dwMinorVersion;
public int dwBuildNumber;
public int dwPlatformId;
public char[] szCSDVersion;
public short wServicePackMajor;
public short wServicePackMinor;
public short wSuiteMask;
public byte wProductType;
public byte wReserved;
public OSVERSIONINFOEXW() {
szCSDVersion = new char[128];
dwOSVersionInfoSize = size();
}
@Override
protected List<String> getFieldOrder() {
return Arrays.asList(
"dwOSVersionInfoSize",
"dwMajorVersion", "dwMinorVersion", "dwBuildNumber",
"dwPlatformId",
"szCSDVersion",
"wServicePackMajor", "wServicePackMinor",
"wSuiteMask", "wProductType",
"wReserved"
);
}
}
}

View File

@@ -12,6 +12,7 @@ nanohttpd = "2.3.1"
jsoup = "1.19.1"
chardet = "2.5.0"
twelvemonkeys = "3.12.0"
jna = "5.17.0"
# plugins
shadow = "8.3.6"
@@ -31,6 +32,7 @@ nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
chardet = { module = "org.glavo:chardet", version.ref = "chardet" }
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
[plugins]
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }