JNI
这或许是一个非常奇怪的需求。在涉及性能的场合,通常在Java内部进行优化就已经足够;而涉及到底层硬件之类的——为什么MC需要这玩意?
我个人的经验是,从原理上优化往往比换个语言更有效。
本章的重点是在MC中使用JNI,而不是JNI教程。
为简便起见,我们只考虑编译为win32 dll。
我们将以MadParticle曾经作过的JNI尝试——快速清除光照缓存为例,向你讲解如何在Mod中使用JNI。
JNI本身
首先我们需要写出我们的native方法:
public class Native {
static {
System.loadLibrary("native");
}
public static native void reset(byte[][][] array);
public static native void resetByte(byte[] array);
}
此处的Native
不是特定的,你可以使用任何你想使用的名字。
然后切换到命令行(终端),生成头文件:
PS H:\Minecraft Develop\MadParticle\src\main\java\cn\ussshenzhou\madparticle\util> javac -h . Native.java
你可以看到生成 了一个Native.class
和cn_ussshenzhou_madparticle_util_Native.h
。Native.class
不再需要,可以删掉。头文件的名称也不是固定的,你可以将其改为任意你喜欢的名字。我们这里将其改为nativeUtil.h
,放在一边。
JNI与Gradle
首先打开你的settings.gradle
,在末尾添加
include 'native'
然后在你的项目根目录下建立如下文件夹和文件(显然nativeUtil.h
则使用复制):
MadParticle/
└── native/
├── build.gradle
├── CMakeLists.txt
└── src/
└── main/
└── cpp/
├── library.cpp
├── library.h
├── nativeUtil.cpp
└── nativeUtil.h
CMakeLists.txt
,library.cpp
,library.h
只有在你使用Clion编写C++代码时才是必要的。当你使用VSC或者VS时,可以手动地在编辑器设置中添加JNI头文件目录。
cmake_minimum_required(VERSION 3.28)
project(native)
set(CMAKE_CXX_STANDARD 17)
include_directories()
add_library(native SHARED src/main/cpp/library.cpp
src/main/cpp/nativeUtil.h
src/main/cpp/nativeUtil.cpp)
find_package(JNI REQUIRED)
include_directories(${JAVA_INCLUDE_PATH})
include_directories(${JAVA_INCLUDE_PATH2})
library.cpp
只需要#include "library.h"
即可,仅作为占位,没有实际意义。
library.h
只需要:
#ifndef NATIVE_LIBRARY_H
#define NATIVE_LIBRARY_H
#endif
build.gradle
的内容如下,你可以直接复制,记得改group
和version
:
import org.gradle.internal.jvm.Jvm
plugins {
id 'cpp-library'
}
group 'cn.ussshenzhou'
version '0.5.3'
library {
binaries.configureEach { CppBinary binary ->
def compileTask = binary.compileTask.get()
compileTask.includes.from("${Jvm.current().javaHome}/include")
def osFamily = binary.targetPlatform.targetMachine.operatingSystemFamily
if (osFamily.macOs) {
compileTask.includes.from("${Jvm.current().javaHome}/include/darwin")
} else if (osFamily.linux) {
compileTask.includes.from("${Jvm.current().javaHome}/include/linux")
} else if (osFamily.windows) {
compileTask.includes.from("${Jvm.current().javaHome}/include/win32")
}
compileTask.source.from fileTree(dir: "src/main/cpp", include: "**/*.cpp")
def toolChain = binary.toolChain
if (toolChain instanceof VisualCpp) {
compileTask.compilerArgs.addAll(["/std:c17", "/O2"])
} else if (toolChain instanceof GccCompatibleToolChain) {
compileTask.compilerArgs.addAll(["-std=c17", "-O2"])
}
}
}
然后在nativeUtil.cpp
写具体实现:
#include "jni.h"
#include "nativeUtil.h"
void Java_cn_ussshenzhou_madparticle_util_Native_reset(JNIEnv *env, jclass clazz, jobjectArray array) {
...
}
void Java_cn_ussshenzhou_madparticle_util_Native_resetByte(JNIEnv *env, jclass clazz, jbyteArray array) {
...
}
此处自动生成的的方法名称不能改变。
MC环境
为了在开发环境运行时找到库文件,构建时自动编译C++以及打包jar时加入.dll,我们需要在根项目的build.gradle
添加以下内容:
minecraft {
...
runs {
client {
...
property "java.library.path", file("${project(":native").buildDir}/lib/main/debug").absolutePath
...
}
server {
...
property "java.library.path", file("${project(":native").buildDir}/lib/main/debug").absolutePath
...
}
gameTestServer {
...
property "java.library.path", file("${project(":native").buildDir}/lib/main/debug").absolutePath
...
}
...
}
}
...
compileJava.dependsOn(":native:assembleDebug").dependsOn(":native:assembleRelease")
processResources {
from("${project(":native").buildDir}/lib/main/release/") {
include("*.dll")
}
}
最后就是处理dll被打包入jar的情况,我们补充原先的Native.java
:
public class Native {
static {
if (!FMLLoader.isProduction()) {
System.loadLibrary("native");
} else {
loadFromJar();
}
}
private static void loadFromJar() {
try {
NativeUtils.loadLibraryFromJar("/native.dll");
} catch (IOException e1) {
throw new RuntimeException(e1);
}
}
public static native void reset(byte[][][] array);
public static native void resetByte(byte[] array);
}
NativeUtils
来自https://github.com/adamheinrich/native-utils ,你可以直接复制到你的项目内(Forge不会在生产环境带上它):
/**
* Copied from {@link ca.weblite.nativeutils.NativeUtils} under MIT license for convenience.
* @author Adam Heirnich <a href="mailto:mailto:adam@adamh.cz"></a>, <a href="http://www.adamh.cz"></a>
*/
public class NativeUtils {
/**
* Private constructor - this class will never be instanced
*/
private NativeUtils() {
}
/**
* Loads a library from current JAR archive, using the class loader of the {@code NativeUtils}
* class to find the resource in the JAR.
*
* @param path a {@link java.lang.String} object.
* @throws java.io.IOException if any.
* @throws UnsatisfiedLinkError if loading the native library fails.
*/
public static void loadLibraryFromJar(String path) throws IOException {
loadLibraryFromJar(path, NativeUtils.class);
}
/**
* Loads a library from current JAR archive.
*
* The file from JAR is copied into system temporary directory and then loaded. The temporary file is deleted after exiting.
* Method uses String as filename because the pathname is "abstract", not system-dependent.
*
* @throws java.lang.IllegalArgumentException If the path is not absolute or if the filename is shorter than three characters (restriction of @see File#createTempFile(java.lang.String, java.lang.String)).
* @param path a {@link java.lang.String} object.
* @param source {@code Class} whose class loader should be used to look up the resource in the JAR file
* @throws java.io.IOException if any.
* @throws UnsatisfiedLinkError if loading the native library fails.
*/
public static void loadLibraryFromJar(String path, Class<?> source) throws IOException, UnsatisfiedLinkError {
// Finally, load the library
System.load(extractFromJar(path, source).toAbsolutePath().toString());
}
/**
* Extracts a resource from the JAR and stores it as temporary file
* in the file system.
*
* @param path path of the resource, must begin with {@code '/'}, see {@link Class#getResourceAsStream(String)}
* @param source {@code Class} whose class loader should be used to look up the resource in the JAR file
* @return file path of the temporary file extracted from this JAR
* @throws java.io.IOException if any.
*/
public static Path extractFromJar(String path, Class<?> source) throws IOException {
if (!path.startsWith("/")) {
throw new IllegalArgumentException("The path has to be absolute (start with '/').");
}
String filename = path.substring(path.lastIndexOf('/') + 1);
// Split filename to prefix and suffix (extension)
String prefix;
String suffix;
int lastDot = filename.lastIndexOf('.');
if (lastDot == -1) {
// No file extension; use complete filename as prefix
prefix = filename;
suffix = null;
} else {
prefix = filename.substring(0, lastDot);
suffix = filename.substring(lastDot);
}
// Check if the filename is okay
if (prefix.length() < 3) {
throw new IllegalArgumentException("The filename has to be at least 3 characters long.");
}
// Prepare temporary file
Path temp = Files.createTempFile(prefix, suffix);
temp.toFile().deleteOnExit();
// Open and check input stream
InputStream is = source.getResourceAsStream(path);
if (is == null) {
throw new FileNotFoundException("File " + path + " was not found inside JAR.");
}
try (is; OutputStream out = Files.newOutputStream(temp, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
is.transferTo(out);
}
return temp;
}
}