9.7 KiB
Lua Runner for Cangjie
Lua Runner for Cangjie 是一个专为仓颉语言设计的轻量级、高性能 Lua 脚本执行引擎。它通过 C FFI 桥接 C++,提供了稳定且易用的 Lua 虚拟机管理能力。 注:本项目是开发原生鸿蒙应用时产生的副产物,当前版本依然存在局限性与不足,请详细检查后再使用。
安全与发布说明:仓库中不应提交任何中心仓访问令牌、私有凭据或其他敏感配置。发布前请使用本地环境变量、用户级配置或 CI 密钥注入方式提供认证信息,并检查待上传制品中不包含明文凭据。
除了基础的脚本嵌入功能外,该引擎的核心特色在于支持一种独特的 “栈式管道执行模式”,能够实现脚本间的隐式参数传递,非常适合构建数据处理管道、游戏脚本系统或插件化架构。同时,它提供了完善的异常处理机制和灵活的 I/O 重定向功能。
特性
- 轻量级集成:基于 Lua 5.4,通过 FFI 直接与仓颉语言交互,性能损耗小。
- 栈式管道模式:支持脚本间基于栈的数据隐式传递,实现类似函数式管道的调用链。
- I/O 重定向:可自定义 Lua 标准输入/输出(
print,io.read)的回调函数,支持文件交换目录重定向。 - 模块化管理:提供
load和unload方法,支持按名称动态加载和卸载 Lua 脚本模块,避免全局污染。 - 完善的异常处理:定义了详细的错误码体系,通过
LuaError类统一抛出,便于调试和逻辑控制。 - 链式调用:大部分操作方法返回实例本身,支持流畅的调用风格。
快速开始
环境要求
- Cangjie 语言环境
- 底层 C++ 库(需要预先编译好与 Lua 5.4 链接的动态库)
基本用法
import LuaCangjie_api.*
main(): Int64 {
try {
// 1. 创建 Lua 运行实例
let runner = LuaRunner()
// 2. 加载一个脚本模块,命名为 "myScript"
runner.load("./scripts/hello.lua", "myScript")
// 3. 执行脚本,传入参数 "World"
let result = runner.runScript("", "World")
println("Script result: ${result}") // 输出: Script result: Hello, World!
} catch (e: LuaError) {
println("Lua Error [${e.code}]: ${e.getMessage()}")
}
return 0
}
脚本示例 (hello.lua):
-- 使用 ... 接收从 runScript 传入的参数
local name = ...
return "Hello, " .. name
API 文档
类:LuaRunner
Lua 虚拟机的主要管理类。
构造函数
public init(input: IOCallback = defaultIO, output: IOCallback = defaultIO, pathio: String = "", pkgpath: String = "")
- 参数:
input:IOCallback类型。Lua 调用io.read时触发的回调函数。output:IOCallback类型。Lua 调用print或io.write时触发的回调函数。pathio:String类型。指定 I/O 重定向的文件交换目录路径。为空则不重定向。pkgpath:String类型。设置 Lua 的package.path,用于自定义模块搜索路径(如"./lib/?.lua")。
方法
| 方法 | 描述 | 返回值 |
|---|---|---|
load(path: String, name: String): This |
加载指定路径的 Lua 文件,编译后以 name 为标识压入内部栈。 |
实例本身 (This) |
unload(name: String): This |
从内部栈中卸载由 name 标识的模块。 |
实例本身 (This) |
runScript(path: String, arg: String): String |
核心执行方法。 - 若 path 非空:加载并执行该脚本,arg 作为参数传入。 - 若 path 为空:触发管道模式,调用栈顶函数,并将栈顶数据作为参数传入。 |
脚本执行的字符串结果 |
clear(): This |
清理 Lua 状态机,清空全局变量和所有加载的库,恢复到初始状态。 | 实例本身 (This) |
result(): String |
获取最近一次 runScript 成功执行的返回值。 |
字符串 |
error(): String |
获取最近一次错误的原始描述信息。 | 字符串 |
异常类:LuaError
所有 Lua 相关操作失败时抛出的异常。
- 属性:
code: Int32:错误码,用于程序逻辑判断。
- 方法:
getMessage(): String:获取错误的详细描述。
错误码对照表
| 错误码 | 宏定义 | 描述 |
|---|---|---|
| 5001 | NAPI_JSON_LOAD_FAIL |
JSON 文件加载失败。 |
| 5002 | NAPI_MALLOC_FAIL |
内存分配失败。 |
| 5003 | NAPI_LOAD_FILE_ERROR |
文件加载错误,可能由路径错误或权限不足引起。 |
| 5004 | NAPI_SCRIPT_RUNNER_INVALID |
脚本运行器初始化失败或实例无效。 |
| 5005 | NAPI_LUA_STATE_ERROR |
Lua 状态机初始化失败或已损坏。 |
| 5006 | NAPI_JSON_RESOLVE_FAIL |
JSON 解析错误,可能是格式问题。 |
| 5007 | NAPI_JSON_FORMATE_ERROR |
JSON 格式错误,不符合 MCP 常见格式。 |
| 5008 | NAPI_MISSING_JSON_ARG |
无法从 JSON 中解析到所需参数。 |
| 5009 | NAPI_LUA_STACK_NOSPACE |
Lua 栈空间不足。 |
| 5010 | NAPI_SCRIPT_ERROR |
脚本内部错误,请通过 result() 获取详细信息。 |
| 5011 | NAPI_SCRIPT_BAD_RET |
脚本返回值错误,当前版本仅支持字符串返回。 |
| 5012 | NAPI_UNLOADLIB_FAIL |
尝试卸载未加载的库。 |
| 5013 | NAPI_LUAFUN_NOFOUND |
调用不符合单输入单输出约定。 |
| 5014 | NAPI_LUA_STACK_ERROR |
函数调用时栈大小小于最小约定大小。 |
| 5015 | NAPI_LUA_INITFAIL |
Lua 状态机初始化错误。 |
| 5016 | NAPI_LUALIB_LOAD_OVER_STACK |
加载的库数量超过上限(最大 20 个)。 |
| 5017 | NAPI_LUA_CLASS_LOST |
底层 LuaRunner 对象丢失。 |
| 5018 | NAPI_LUA_MISS_REDIRECT |
缺少用于重定向的文件路径。 |
| 5019 | NAPI_RESET_INPUT_FILE_ERROR |
重置输入文件失败,可能导致输入污染。 |
| 5020 | NAPI_ERROR_FUNCS |
回调函数执行过程中发生错误。 |
管道模式 (Pipeline Mode)
这是该引擎的核心特性,允许数据在多个脚本间自动流转,无需在仓颉层进行手动传递。
调用规范
-
脚本编写:作为管道节点的 Lua 脚本,必须使用 Lua 的变长参数语法
...来接收上一个节点传递的数据。-- node_b.lua local input = ... return "处理结果: " .. input -
加载顺序(逆序):利用栈的后进先出(LIFO)特性,需按照执行顺序的逆序进行加载。
- 期望的执行顺序:
A->B->C - 仓颉侧加载顺序:
runner.load("C.lua", "C")runner.load("B.lua", "B")runner.load("A.lua", "A")
- 期望的执行顺序:
-
触发执行:
- 首先执行初始脚本,将其返回值压入栈顶。
- 然后调用
runScript("", ""),引擎将自动调用栈顶下方的函数(即最后加载的脚本B),并将栈顶数据作为参数传入。 B执行完毕后的返回值成为新的栈顶,等待下一次runScript调用。
管道模式示例
// 1. 按逆序加载:A -> B -> C
runner.load("./scripts/add_suffix.lua", "add_suffix") // 脚本 C: 添加后缀
runner.load("./scripts/to_upper.lua", "to_upper") // 脚本 B: 转大写
runner.load("./scripts/add_prefix.lua", "add_prefix") // 脚本 A: 添加前缀
// 2. 执行初始数据生成脚本
let initialData = runner.runScript("./scripts/generate_data.lua", "hello")
// 3. 依次触发管道节点
let afterA = runner.runScript("","")//数据经过A
let afterB = runner.runScript("", "") // 数据经过脚本 B (to_upper)
let finalResult = runner.runScript("", "") // 数据经过脚本 C (add_suffix)
println(finalResult) // 最终输出: [PREFIX] HELLO [SUFFIX]
I/O 重定向机制
当配置了 input、output 回调以及 pathio 路径时,Lua 的标准 I/O 将被重定向:
- 输出重定向:Lua 调用
print或io.write时,内容会写入{pathio}目录下的临时文件,并触发仓颉侧的output回调。您可以在回调中实现自己的输出逻辑(如写入日志、发送网络消息等)。 - 输入重定向:Lua 调用
io.read时,会触发仓颉侧的input回调。您需要在回调中准备并提供输入数据(例如从文件读取或返回模拟数据),供底层读取。
当前局限性
- 数据类型限制:当前版本在仓颉与 Lua 交互层仅支持字符串类型。虽然 Lua 内部可以处理复杂数据结构,但传递给仓颉或从仓颉传入时必须进行序列化/反序列化(如 JSON)。
- 容量限制:内部使用固定数组管理加载的库,上限为 20 个,超过将报错
5016。 - I/O 性能:I/O 重定向依赖于文件系统交换,相对于纯内存交互存在微小的性能开销。
第三方组件与合规说明
- 本项目仓库内包含 Lua 5.4.8 源码副本,用于构建底层原生运行时。
- Lua 5.4.8 使用 MIT License;其原始版权与许可声明可在
lib/lua/对应源码中查看。 - 本项目自身以
LGPL-3.0-or-later方式发布,使用或分发时请同时遵守项目本身以及所包含第三方组件的许可证要求。
许可证
本项目基于 LGPL-3.0-or-later 协议开源。仓库同时包含 Lua 5.4.8 的 MIT Licensed 源码副本,用于构建底层原生运行时。分发和集成时,请一并检查 LICENSE 以及 lib/lua/ 中随源码附带的许可声明。