Lua ❤ Java:OSGi 环境中的嵌入式脚本
Lua 是一种轻量级、高效、可嵌入的脚本语言,它具有简单的语法和强大的扩展性。Lua 的设计目标是作为一种扩展语言来嵌入到其他应用程序中,以提供灵活的脚本能力。它的特点可以归纳到如下几点:
-
简洁和灵活的语法:Lua 的语法非常简洁和易于学习,它采用了类似于 Pascal 的语法风格,使用关键字和符号来表示不同的语法结构。这使得 Lua 代码易于编写和阅读。同时,Lua 还支持面向过程和函数式编程风格,可以根据需要进行灵活的编程。
-
轻量级和高效:Lua 的设计非常注重轻量级和高效性。它的核心库非常小巧,只包含了一些基本的数据结构和操作,这使得 Lua 解释器的内存占用和启动时间都非常低。同时,Lua 的执行速度也非常快,这使得它在嵌入式系统和游戏开发等领域得到广泛应用。
-
嵌入性:Lua 的设计初衷就是作为一种嵌入式脚本语言,它可以轻松地嵌入到其他应用程序中。通过 Lua 的 C API,开发者可以将 Lua 解释器嵌入到自己的应用程序中,并通过调用 Lua 函数和访问 Lua 变量来实现脚本扩展能力。这种可嵌入性使得 Lua 成为许多应用程序的脚本语言选择。
-
扩展性:Lua 提供了丰富的扩展机制,开发者可以通过编写 C 代码来扩展 Lua 的功能。Lua 的扩展机制包括通过 C API 添加新的原生函数、创建新的 Lua 模块、修改 Lua 解释器的行为等。这使得开发者可以根据自己的需求来扩展 Lua,使其适应不同的应用场景。
这些特点是基于 ANSI C 开发的 Lua 所具有的。为了和 JVM 生态整合,luaj 项目则提供了基于 Lua 语法的编译器和工具集,能够实现和 JVM 环境的互操作。得益于 Lua table 数据结构极强的正交性,luaj 生成的字节码短小高效,编译速度快且运行效率高,整个 luaj jar 包才不到 300kb,作为对比,clojure jar 包 ~4Mb。
luaj 的 Github 项目参见此处,README.md 中详细介绍了其用法、Java 侧的数据类型 LuaValue,多入参和返回值 Varargs 的处理方法,标准库的实现等。其支持 JSR223,但也提供了自己的 API:
Globals globals = JsePlatform.standardGlobals();
LuaValue sc = globals.loadfile("port_status_check.lua");
LuaValue res = sc.call();
System.out.println(res);
虽然可以为 luaj 提供模块,以便直接 require "some_java_module"
引用和使用,如下所示:
public class hyperbolic extends TwoArgFunction {
public hyperbolic() {}
public LuaValue call(LuaValue modname, LuaValue env) {
LuaValue library = tableOf();
library.set( "sinh", new sinh() );
library.set( "cosh", new cosh() );
env.set( "hyperbolic", library );
return library;
}
static class sinh extends OneArgFunction {
public LuaValue call(LuaValue x) {
return LuaValue.valueOf(Math.sinh(x.checkdouble()));
}
}
static class cosh extends OneArgFunction {
public LuaValue call(LuaValue x) {
return LuaValue.valueOf(Math.cosh(x.checkdouble()));
}
}
}
require 'hyperbolic'
print('sinh', hyperbolic.sinh)
print('sinh(1.0)', hyperbolic.sinh(1.0))
但更简单的方法是直接调用 Java 类,创建实例,调用静态或实例方法,luaj 项目中提供了一个 Swing App 展示了互操作。
u = luajava.bindClass('com.abc.LuaUtils')
print(u:ssh('172.20.1.123','admin','pass',true,false,{'show running-config'}))
t = luajava.bindClass('java.time.LocalDateTime')
print(t:now():toString())
在 OSGi 环境中,由于每个 Bundle 都具有独立的类加载器,因此这种 Java 互操作很可能加载不到目标类 —— luaj 使用系统默认的类加载器,为此可重写 luaj 类加载的逻辑,并提供我们自己的 Globals:
public static Globals adaptOSGiGlobals() {
Globals globals = new Globals();
globals.load(new JseBaseLib());
globals.load(new PackageLib());
globals.load(new Bit32Lib());
globals.load(new TableLib());
globals.load(new StringLib());
globals.load(new CoroutineLib());
globals.load(new JseMathLib());
globals.load(new JseIoLib());
globals.load(new JseOsLib());
globals.load(new OsgiBundleLib());
LoadState.install(globals);
LuaC.install(globals);
return globals;
}
public static class OsgiBundleLib extends LuajavaLib {
@Override
protected Class classForName(String name) {
ClassLoader classLoader = SomeClassLoadInThisBundle.class.getClassLoader();
try {
Class clazz = Class.forName(name, true, classLoader);
return clazz;
} catch (ClassNotFoundException e) {
log.error("class not found", e);
return Object.class;
}
}
}
//usage:
Globals globals = adaptOSGiGlobals();
LuaValue scriptFunc = globals.load(scriptContent);
scriptFunc.call();
下面是和一个庞大的 OSGi 工程集成的 Lua 函数管理器以及动态实验游乐场:
注意,实际接受不受信任脚本时,可能需要 SandBox 特性,限制可访问的标准库、Java 类库、编译和 Debug 能力,甚至限制脚本执行的指令数,luaj 在这方面充分发挥了 lua 的嵌入灵活性,参见文档,下面是一个简单的示例:
public class SampleSandboxed {
static Globals server_globals;
public static void main(String[] args) {
// for script compile
server_globals = new Globals();
server_globals.load(new JseBaseLib());
server_globals.load(new PackageLib());
server_globals.load(new JseStringLib());
server_globals.load(new JseMathLib());
LoadState.install(server_globals);
LuaC.install(server_globals);
// make string safe
LuaString.s_metatable = new ReadOnlyLuaTable(LuaString.s_metatable);
runScriptInSandbox( "return 'foo'" );
runScriptInSandbox( "return ('abc'):len()" );
runScriptInSandbox( "return getmetatable('abc')" );
runScriptInSandbox( "return getmetatable('abc').len" );
runScriptInSandbox( "return getmetatable('abc').__index" );
runScriptInSandbox( "return setmetatable('abc', {})" );
runScriptInSandbox( "getmetatable('abc').len = function() end" );
runScriptInSandbox( "getmetatable('abc').__index = {}" );
runScriptInSandbox( "getmetatable('abc').__index.x = 1" );
runScriptInSandbox( "while true do print('loop') end" );
// custom: allows booleans to be added to numbers
runScriptInSandbox( "return 5 + 6, 5 + true, false + 6" );
LuaBoolean.s_metatable = new ReadOnlyLuaTable(LuaValue.tableOf(new LuaValue[] {
LuaValue.ADD, new TwoArgFunction() {
public LuaValue call(LuaValue x, LuaValue y) {
return LuaValue.valueOf(
(x == TRUE ? 1.0 : x.todouble()) +
(y == TRUE ? 1.0 : y.todouble()) );
}
},
}));
runScriptInSandbox( "return 5 + 6, 5 + true, false + 6" );
}
// Run a script in a lua thread and limit it to a certain number
// of instructions by setting a hook function.
// Give each script its own copy of globals, but leave out libraries
// that contain functions that can be abused.
static void runScriptInSandbox(String script) {
Globals user_globals = new Globals();
user_globals.load(new JseBaseLib());
user_globals.load(new PackageLib());
user_globals.load(new Bit32Lib());
user_globals.load(new TableLib());
user_globals.load(new JseStringLib());
user_globals.load(new JseMathLib());
// not allow those libs:
// user_globals.load(new LuajavaLib());
// user_globals.load(new CoroutineLib());
// user_globals.load(new JseIoLib());
// user_globals.load(new JseOsLib());
// LoadState.install(user_globals);
// LuaC.install(user_globals);
// not abllow use debug hook
user_globals.load(new DebugLib());
LuaValue sethook = user_globals.get("debug").get("sethook");
user_globals.set("debug", LuaValue.NIL);
// use server_globals compile script, and run in user_gloabls
// call in luaThread with limit instruction count
LuaValue chunk = server_globals.load(script, "main", user_globals);
LuaThread thread = new LuaThread(user_globals, chunk);
LuaValue hookfunc = new ZeroArgFunction() {
public LuaValue call() {
throw new Error("Script overran resource limits.");
}
};
final int instruction_count = 20;
sethook.invoke(LuaValue.varargsOf(new LuaValue[] { thread, hookfunc,
LuaValue.EMPTYSTRING, LuaValue.valueOf(instruction_count) }));
Varargs result = thread.resume(LuaValue.NIL);
System.out.println("[["+script+"]] -> "+result);
}
static class ReadOnlyLuaTable extends LuaTable {
public ReadOnlyLuaTable(LuaValue table) {
presize(table.length(), 0);
for (Varargs n = table.next(LuaValue.NIL); !n.arg1().isnil(); n = table
.next(n.arg1())) {
LuaValue key = n.arg1();
LuaValue value = n.arg(2);
super.rawset(key, value.istable() ? new ReadOnlyLuaTable(value) : value);
}
}
public LuaValue setmetatable(LuaValue metatable) { return error("table is read-only"); }
public void set(int key, LuaValue value) { error("table is read-only"); }
public void rawset(int key, LuaValue value) { error("table is read-only"); }
public void rawset(LuaValue key, LuaValue value) { error("table is read-only"); }
public LuaValue remove(int pos) { return error("table is read-only"); }
}
}
Happy hacking!