如何用Jshell运行包含JNI,Native的项目?

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
相关推荐
用户741883523937 个月前
4、LibGDX 游戏循环:深入理解与实现
游戏开发·libgdx
村口曹大爷1 年前
processing完整教程
java·开发语言·processing·libgdx
little_fat_sheep2 年前
【libGDX】使用Mesh绘制圆形
libgdx
little_fat_sheep2 年前
【libGDX】使用Mesh绘制矩形
libgdx