Skip to content

Commit 1ab7ab0

Browse files
authored
使用 JNA 调用本机 API (#3890)
1 parent f283254 commit 1ab7ab0

9 files changed

Lines changed: 300 additions & 33 deletions

File tree

HMCL/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,14 @@ tasks.shadowJar {
116116
exclude("META-INF/services/javax.imageio.spi.ImageReaderSpi")
117117
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
118118

119+
listOf(
120+
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*","freebsd-*", "linux-*", "darwin-*",
121+
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
122+
).forEach { exclude("com/sun/jna/$it/**") }
123+
119124
minimize {
120125
exclude(dependency("com.google.code.gson:.*:.*"))
126+
exclude(dependency("net.java.dev.jna:jna:.*"))
121127
exclude(dependency("libs:JFoenix:.*"))
122128
}
123129

HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.jackhuang.hmcl.util.io.JarUtils;
3939
import org.jackhuang.hmcl.util.platform.Architecture;
4040
import org.jackhuang.hmcl.util.platform.CommandBuilder;
41+
import org.jackhuang.hmcl.util.platform.NativeUtils;
4142
import org.jackhuang.hmcl.util.platform.OperatingSystem;
4243

4344
import java.io.File;
@@ -246,12 +247,13 @@ public static void main(String[] args) {
246247
LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
247248
LOG.info("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory"));
248249
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
249-
LOG.info("Physical memory: " + OperatingSystem.TOTAL_MEMORY + " MB");
250+
LOG.info("Physical Memory: " + OperatingSystem.TOTAL_MEMORY + " MB");
250251
LOG.info("Metaspace: " + ManagementFactory.getMemoryPoolMXBeans().stream()
251252
.filter(bean -> bean.getName().equals("Metaspace"))
252253
.findAny()
253254
.map(bean -> bean.getUsage().getUsed() / 1024 / 1024 + "MB")
254255
.orElse("Unknown"));
256+
LOG.info("Native Backend: " + (NativeUtils.USE_JNA ? "JNA" : "None"));
255257
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
256258
LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE"));
257259
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));

HMCLCore/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@ dependencies {
1515
api(libs.nanohttpd)
1616
api(libs.jsoup)
1717
api(libs.chardet)
18+
api(libs.jna)
19+
1820
compileOnlyApi(libs.jetbrains.annotations)
1921
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Hello Minecraft! Launcher
3+
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
package org.jackhuang.hmcl.util.platform;
19+
20+
import com.sun.jna.Library;
21+
import com.sun.jna.Native;
22+
import com.sun.jna.Platform;
23+
import org.jetbrains.annotations.Nullable;
24+
25+
import java.util.Collections;
26+
import java.util.Map;
27+
28+
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
29+
30+
/**
31+
* @author Glavo
32+
*/
33+
public final class NativeUtils {
34+
public static final boolean USE_JNA = useJNA();
35+
36+
public static <T extends Library> @Nullable T load(String name, Class<T> interfaceClass) {
37+
return load(name, interfaceClass, Collections.emptyMap());
38+
}
39+
40+
public static <T extends Library> @Nullable T load(String name, Class<T> interfaceClass, Map<String, ?> options) {
41+
if (USE_JNA) {
42+
try {
43+
return Native.load(name, interfaceClass, options);
44+
} catch (UnsatisfiedLinkError e) {
45+
LOG.warning("Failed to load native library: " + name, e);
46+
}
47+
}
48+
49+
return null;
50+
}
51+
52+
private static boolean useJNA() {
53+
String backend = System.getProperty("hmcl.native.backend");
54+
if (backend == null || "auto".equalsIgnoreCase(backend)) {
55+
try {
56+
if (Platform.isWindows()) {
57+
String osVersion = System.getProperty("os.version");
58+
59+
// Requires Windows 7 or later (6.1+)
60+
// https://learn.microsoft.com/windows/win32/sysinfo/operating-system-version
61+
if (osVersion == null || osVersion.startsWith("5.") || osVersion.equals("6.0"))
62+
return false;
63+
64+
// Currently we only need to use JNA on Windows
65+
Native.getDefaultStringEncoding();
66+
return true;
67+
}
68+
69+
return false;
70+
} catch (Throwable ignored) {
71+
return false;
72+
}
73+
} else if ("jna".equalsIgnoreCase(backend)) {
74+
// Ensure JNA is available
75+
Native.getDefaultStringEncoding();
76+
return true;
77+
} else if ("none".equalsIgnoreCase(backend))
78+
return false;
79+
else
80+
throw new Error("Unsupported native backend: " + backend);
81+
}
82+
83+
private NativeUtils() {
84+
}
85+
}

HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
package org.jackhuang.hmcl.util.platform;
1919

2020
import org.jackhuang.hmcl.util.KeyValuePairProperties;
21+
import org.jackhuang.hmcl.util.platform.windows.Kernel32;
22+
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
2123

2224
import java.io.BufferedReader;
2325
import java.io.File;
@@ -29,10 +31,7 @@
2931
import java.nio.file.Files;
3032
import java.nio.file.Path;
3133
import java.nio.file.Paths;
32-
import java.util.Arrays;
33-
import java.util.Collections;
34-
import java.util.Locale;
35-
import java.util.Map;
34+
import java.util.*;
3635
import java.util.regex.Matcher;
3736
import java.util.regex.Pattern;
3837

@@ -159,48 +158,69 @@ public String getJavaExecutable() {
159158
if (CURRENT_OS == WINDOWS) {
160159
String versionNumber = null;
161160
int buildNumber = -1;
161+
int codePage = -1;
162162

163-
try {
164-
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "ver"});
165-
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) {
166-
Matcher matcher = Pattern.compile("(?<version>[0-9]+\\.[0-9]+\\.(?<build>[0-9]+)(\\.[0-9]+)?)]$")
167-
.matcher(reader.readLine().trim());
163+
Kernel32 kernel32 = Kernel32.INSTANCE;
168164

169-
if (matcher.find()) {
170-
versionNumber = matcher.group("version");
171-
buildNumber = Integer.parseInt(matcher.group("build"));
165+
// Get Windows version number
166+
if (kernel32 != null) {
167+
WinTypes.OSVERSIONINFOEXW osVersionInfo = new WinTypes.OSVERSIONINFOEXW();
168+
if (kernel32.GetVersionExW(osVersionInfo)) {
169+
int majorVersion = osVersionInfo.dwMajorVersion;
170+
int minorVersion = osVersionInfo.dwMinorVersion;
171+
172+
buildNumber = osVersionInfo.dwBuildNumber;
173+
versionNumber = majorVersion + "." + minorVersion + "." + buildNumber;
174+
} else
175+
System.err.println("Failed to obtain OS version number (" + kernel32.GetLastError() + ")");
176+
}
177+
178+
if (versionNumber == null) {
179+
try {
180+
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "ver"});
181+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) {
182+
Matcher matcher = Pattern.compile("(?<version>[0-9]+\\.[0-9]+\\.(?<build>[0-9]+)(\\.[0-9]+)?)]$")
183+
.matcher(reader.readLine().trim());
184+
185+
if (matcher.find()) {
186+
versionNumber = matcher.group("version");
187+
buildNumber = Integer.parseInt(matcher.group("build"));
188+
}
172189
}
190+
process.destroy();
191+
} catch (Throwable ignored) {
173192
}
174-
process.destroy();
175-
} catch (Throwable ignored) {
176193
}
177194

178-
if (versionNumber == null) {
195+
if (versionNumber == null)
179196
versionNumber = System.getProperty("os.version");
180-
}
181197

182-
String osName = System.getProperty("os.name");
198+
// Get Code Page
183199

184-
// Java 17 or earlier recognizes Windows 11 as Windows 10
185-
if (osName.equals("Windows 10") && buildNumber >= 22000) {
186-
osName = "Windows 11";
187-
}
188-
189-
int codePage = -1;
190-
try {
191-
Process process = Runtime.getRuntime().exec(new String[]{"chcp.com"});
192-
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) {
193-
Matcher matcher = Pattern.compile("(?<cp>[0-9]+)$")
194-
.matcher(reader.readLine().trim());
200+
if (kernel32 != null)
201+
codePage = kernel32.GetACP();
202+
else {
203+
try {
204+
Process process = Runtime.getRuntime().exec(new String[]{"chcp.com"});
205+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), NATIVE_CHARSET))) {
206+
Matcher matcher = Pattern.compile("(?<cp>[0-9]+)$")
207+
.matcher(reader.readLine().trim());
195208

196-
if (matcher.find()) {
197-
codePage = Integer.parseInt(matcher.group("cp"));
209+
if (matcher.find()) {
210+
codePage = Integer.parseInt(matcher.group("cp"));
211+
}
198212
}
213+
process.destroy();
214+
} catch (Throwable ignored) {
199215
}
200-
process.destroy();
201-
} catch (Throwable ignored) {
202216
}
203217

218+
String osName = System.getProperty("os.name");
219+
220+
// Java 17 or earlier recognizes Windows 11 as Windows 10
221+
if (osName.equals("Windows 10") && buildNumber >= 22000)
222+
osName = "Windows 11";
223+
204224
SYSTEM_NAME = osName;
205225
SYSTEM_VERSION = versionNumber;
206226
SYSTEM_BUILD_NUMBER = buildNumber;
@@ -443,4 +463,5 @@ public static double toGigaBytes(long bytes) {
443463

444464
public static final PhysicalMemoryStatus INVALID = new PhysicalMemoryStatus(0, -1);
445465
}
466+
446467
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Hello Minecraft! Launcher
3+
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
package org.jackhuang.hmcl.util.platform.windows;
19+
20+
import com.sun.jna.win32.StdCallLibrary;
21+
import org.jackhuang.hmcl.util.platform.NativeUtils;
22+
23+
/**
24+
* @author Glavo
25+
*/
26+
public interface Kernel32 extends StdCallLibrary {
27+
28+
Kernel32 INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows()
29+
? NativeUtils.load("kernel32", Kernel32.class)
30+
: null;
31+
32+
/**
33+
* @see <a href="https://learn.microsoft.com/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror">GetLastError function</a>
34+
*/
35+
int GetLastError();
36+
37+
/**
38+
* @see <a href="https://learn.microsoft.com/windows/win32/api/sysinfoapi/nf-sysinfoapi-getversionexw">GetVersionExW function</a>
39+
*/
40+
boolean GetVersionExW(WinTypes.OSVERSIONINFOEXW lpVersionInfo);
41+
42+
/**
43+
* @see <a href="https://learn.microsoft.com/windows/win32/api/winnls/nf-winnls-getacp">GetACP function</a>
44+
*/
45+
int GetACP();
46+
47+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Hello Minecraft! Launcher
3+
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
package org.jackhuang.hmcl.util.platform.windows;
19+
20+
/**
21+
* @author Glavo
22+
*/
23+
public interface WinConstants {
24+
25+
// https://learn.microsoft.com/windows/win32/sysinfo/registry-key-security-and-access-rights
26+
int KEY_READ = 0x20019;
27+
28+
// https://learn.microsoft.com/windows/win32/sysinfo/predefined-keys
29+
long HKEY_CLASSES_ROOT = 0x80000000L;
30+
long HKEY_CURRENT_USER = 0x80000001L;
31+
long HKEY_LOCAL_MACHINE = 0x80000002L;
32+
long HKEY_USERS = 0x80000003L;
33+
long HKEY_PERFORMANCE_DATA = 0x80000004L;
34+
long HKEY_PERFORMANCE_TEXT = 0x80000050L;
35+
long HKEY_PERFORMANCE_NLSTEXT = 0x80000060L;
36+
long HKEY_CURRENT_CONFIG = 0x80000005L;
37+
long HKEY_DYN_DATA = 0x80000006L;
38+
long HKEY_CURRENT_USER_LOCAL_SETTINGS = 0x80000007L;
39+
}

0 commit comments

Comments
 (0)