Jshell提供了类似kotlin即时运行测试代码的功能,非常适合进行一些快速测试
然而运行涉及jni的部分时 (版本为 21.0.4) 经常会有一个贴心的报错:
java.lang.UnsatisfiedLinkError

尤其是这种涉及到渲染和调用Native的部分
本文以Libgdx为例,分析原理从而运行测试代码。
首先回顾一下Jshell这两个参数
jshell的终端工具在运行时会启动两个jvm, 其中一个负责与用户交互,另一个负责运行测试代码。
scss
"-R value " 这个参数用于向远程Jvm(负责运行代码的那一个)传递参数,例如"-R -Dcustom.value"
scss
"-J value " 这个参数则向运行Jvm(负责终端交互那一个)传递参数
为远程机Jvm配置Native文件路径
ini
// 使用时可以用环境变量进行配置
-R -Djava.library.path=./natives
-R -Dorg.lwjgl.librarypath=./natives
这样加载的时候就可以从当前目录下natives中寻找了。
当导入包不能自动加载JNI时,往往会想到使用什么呢?
lua
System.load("绝对路径");
然而这里暗藏陷阱,在使用多个ClassLoader的时候,使用这个方法往往不会直接出现报错,却没有正常完成JNI的配置,当使用native调用时就会出现诡异的UnsatisfiedLinkError :
java.lang.UnsatisfiedLinkError:'void com.badlogic.gdx.math.Matrix4.prj(float[], float[], int, int, int)' //此处由JNI调用native层处理运算,可以发现没有正常解析。
究其原因,ClassLoader以形似凭证的方式参与了类文件与NativeLibrary的加载与解析,加载到的相应信息都被托管这里。

图像清晰度可观
在这个过程中指定了与Native挂载内容的ClassLoader
因此,不单单是类与ClassLoader有对应关系,同样也涉及到了 System.load...
vbnet
/**
* Native libraries are loaded via {@link System#loadLibrary(String)},
* {@link System#load(String)}, {@link Runtime#loadLibrary(String)} and
* {@link Runtime#load(String)}. They are caller-sensitive.
*
* Each class loader has a NativeLibraries instance to register all of its
* loaded native libraries. System::loadLibrary (and other APIs) only
* allows a native library to be loaded by one class loader, i.e. one
* NativeLibraries instance. Any attempt to load a native library that
* has already been loaded by a class loader with another class loader
* will fail.
*/
jdk.internal.loader.NativeLibraries的注释
调用加载方法的类所在的ClassLoader和调用JNI的导入包所在的ClassLoader不一致便会出现这个问题
毕竟如果存在重复类,JNI怎么进行方法绑定呢?
既然如此,可以手动指定类加载器
javascript
import java.lang.invoke.*;
var lookupClassLoader = MethodHandles.privateLookupIn(java.lang.ClassLoader.class,MethodHandles.lookup());
var handleStaticLib = lookupClassLoader.findStatic(java.lang.ClassLoader.class,"loadLibrary", MethodType.methodType(Class.forName("jdk.internal.loader.NativeLibrary"), java.lang.Class.class,File.class));
//handleStaticLib.invoke(targetClass, lib);
这些参数帮助使用反射
csharp
-R --add-exports=java.base/jdk.internal.loader=ALL-UNNAMED
-R --add-opens=java.base/java.lang=ALL-UNNAMED
-R --add-opens=java.base/jdk.internal.loader=ALL-UNNAMED
简单测试
为了测试运行Libgdx,需要使用的文件包括
diff
Classes 这里放入./modules
- gdx - gdx-backend-lwjgl (选择自己平台的后端,这里使用lwjgl)
- gdx-jnigen-loader (防止使用额外扩展时报错)
- lwjgl - lwjgl-glfw
- lwjgl-jemalloc
- lwjgl-opengl
- lwjgl-stb
- lwjgl-openal
Native 放入 ./natives
- gdx (有64位标签)
- lwjgl - glfw
- jemalloc
- lwjgl_opengl
- lwjgl_stb
- OpenAL
准备好这些文件以后,复制启动参数,➫
jshell --% --class-path ./modules/* -R -Djava.library.path=./natives -R -Dorg.lwjgl.librarypath=./natives -R --add-exports=java.base/jdk.internal.loader=ALL-UNNAMED -R --add-opens=java.base/java.lang=ALL-UNNAMED -R --add-opens=java.base/jdk.internal.loader=ALL-UNNAMED
--% 防止PS错误解析参数 zsh等不需要用这个 这里方便改用的 "/"

配置一下导入包
ini
import com.badlogic.gdx.*;
import com.badlogic.gdx.graphics.*;
import com.badlogic.gdx.graphics.g2d.*;
import com.badlogic.gdx.graphics.glutils.*;
import com.badlogic.gdx.backends.lwjgl3.*;
import com.badlogic.gdx.utils.SharedLibraryLoader;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.ScreenUtils;
import com.badlogic.gdx.utils.Disposable;
import java.lang.invoke.*;
测试代码
javascript
var orthographicCamera = new OrthographicCamera();
var lookupClassLoader = MethodHandles.privateLookupIn(java.lang.ClassLoader.class,MethodHandles.lookup());
var handleStaticLib = lookupClassLoader.findStatic(java.lang.ClassLoader.class,"loadLibrary",MethodType.methodType(Class.forName("jdk.internal.loader.NativeLibrary"),java.lang.Class.class,File.class));
var nativeLibPath = Path.of(System.getProperty("user.dir"),"natives").toFile();
Stream.of(nativeLibPath.listFiles()).filter(s -> s.getName().endsWith(".dll")).forEach(s -> {
try {
handleStaticLib.invoke(Gdx.class,s);
} catch(Throwable T) {
T.printStackTrace();
}
});
SharedLibraryLoader.setLoaded("gdx");
private static Lwjgl3ApplicationConfiguration getDefaultConfiguration() {
Lwjgl3ApplicationConfiguration configuration = new Lwjgl3ApplicationConfiguration();
configuration.setTitle("my-gdx-game");
configuration.useVsync(true);
configuration.setForegroundFPS(Lwjgl3ApplicationConfiguration.getDisplayMode().refreshRate);
configuration.setWindowedMode(640, 480);
return configuration;
}
abstract interface TR {
public void run() throws Throwable;
}
class Apl extends Game {
volatile boolean loaded;
public ArrayDeque<TR> runnablesSequence = new ArrayDeque<TR>();
public void create () {
loaded = true;
}
public void render () {
if (runnablesSequence.size() > 0) {
for (TR runnable : runnablesSequence) {
try {
runnable.run();
} catch (Throwable t) {
t.printStackTrace();
}
}
runnablesSequence.clear();
}
super.render();
}
}
var app = new Apl();
abstract interface TC {
public void run(float num) throws Throwable;
}
abstract interface TBC {
public void run(float w,float h) throws Throwable;
}
ArrayDeque<Disposable> onExit = new ArrayDeque<Disposable>();
class AplScreen implements Screen
{
public TC render = new TC() {
public void run(float num) throws Throwable {
ScreenUtils.clear(Color.SKY);
}
};
public TBC resize;
public TR resume,pause,show,hide,dispose;
public void render(float delta) {
if (render != null) {
try {
render.run(delta);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
public void resize(int width, int height) {
if (resize != null) {
try {
resize.run(width, height);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
public void resume() {
if (resume != null) {
try {
resume.run();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
public void pause() {
if (pause != null) {
try {
pause.run();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
public void show() {
if (show != null) {
try {
show.run();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
public void hide() {
if (hide != null) {
try {
hide.run();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
public void dispose() {
if (dispose != null) {
try {
dispose.run();
} catch (Throwable e) {
e.printStackTrace();
}
}
try {
for (var call : onExit) {
try {
call.dispose();
} catch (Throwable T) {
T.printStackTrace();
}
}
} catch(Throwable T) {
T.printStackTrace();
}
}
}
var apl = new AplScreen();
var lookupApl = MethodHandles.privateLookupIn(Game.class,MethodHandles.lookup());
var setter = lookupApl.findSetter(Game.class, "screen", Screen.class);
setter.bindTo(app).invoke(apl);
Lwjgl3Application laucher;
Executors.newSingleThreadExecutor().submit(() -> {
try {
laucher = new Lwjgl3Application(app,getDefaultConfiguration());
} catch (Throwable T) {
T.printStackTrace();
}
});
void singleGL(TR runnable) {
synchronized (app) {
app.runnablesSequence.push(runnable);
}
}
class ArrayRenderSequence implements TC {
public ArrayDeque<TC> seq = new ArrayDeque<TC>();
public void run(float num) throws Throwable {
for (var call : seq) {
try {
call.run(num);
} catch (Throwable T) {
T.printStackTrace();
}
}
}
public ArrayRenderSequence add(TC call) {
seq.add(call);
return this;
}
}
void render(TC call) {
synchronized (apl) {
if (apl.render != null) {
if (apl.render instanceof ArrayRenderSequence sequenceArray) {
sequenceArray.add(call);
} else {
apl.render = new ArrayRenderSequence().add(apl.render).add(call);
}
} else {
apl.render = call;
}
}
System.out.println("updated render sequence");
}
/*{
int count = 0;
long last = System.currentTimeMillis();
while(!app.loaded) {
long current = System.currentTimeMillis();
count += current > last ? current - last:0;
last = current;
}
}*/
ShapeRenderer shape;
Vector3 testc1 = new Vector3();
singleGL(() -> {
shape = new ShapeRenderer();
onExit.push(shape);
testc1.set(220,120,90);
render(n -> {
shape.begin(ShapeRenderer.ShapeType.Filled);
shape.circle(testc1.x,testc1.y,testc1.z);
shape.end();
});
Gdx.input.setInputProcessor(new InputAdapter() {
public boolean mouseMoved(int screenX, int screenY) {
testc1.set(screenX,Gdx.graphics.getHeight()-screenY,testc1.z);
return true;
}
});
});
这下就不会出现 java.lang.UnsatisfiedLinkError: 'java.nio.ByteBuffer com.badlogic.gdx.utils.BufferUtils.newDisposableByteBuffer(int)'
了。

关前记得把这个窗口关上哦
彩蛋,可以用这个方式设置编辑器
/set -retain editor vim
如果想使用vscode
arduino
//code -w
/set -retain "安装路径" -w