跳到主要内容

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.classcn_ussshenzhou_madparticle_util_Native.hNative.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的内容如下,你可以直接复制,记得改groupversion

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;
}
}