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!