# Lua Runner for Cangjie [![License](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](LICENSE) [![Lua](https://img.shields.io/badge/Lua-5.4-blue)](https://www.lua.org/) [![Cangjie](https://img.shields.io/badge/Cangjie-SDK-orange)](https://cangjie-lang.cn/) Lua Runner for Cangjie 是一个专为仓颉语言设计的轻量级、高性能 Lua 脚本执行引擎。它通过 C FFI 桥接 C++,提供了稳定且易用的 Lua 虚拟机管理能力。 **注:本项目是开发原生鸿蒙应用时产生的副产物,当前版本依然存在局限性与不足,请详细检查后再使用。** 除了基础的脚本嵌入功能外,该引擎的核心特色在于支持一种独特的 **“栈式管道执行模式”**,能够实现脚本间的隐式参数传递,非常适合构建数据处理管道、游戏脚本系统或插件化架构。同时,它提供了完善的异常处理机制和灵活的 I/O 重定向功能。 ## 特性 - **轻量级集成**:基于 Lua 5.4,通过 FFI 直接与仓颉语言交互,性能损耗小。 - **栈式管道模式**:支持脚本间基于栈的数据隐式传递,实现类似函数式管道的调用链。 - **I/O 重定向**:可自定义 Lua 标准输入/输出(`print`, `io.read`)的回调函数,支持文件交换目录重定向。 - **模块化管理**:提供 `load` 和 `unload` 方法,支持按名称动态加载和卸载 Lua 脚本模块,避免全局污染。 - **完善的异常处理**:定义了详细的错误码体系,通过 `LuaError` 类统一抛出,便于调试和逻辑控制。 - **链式调用**:大部分操作方法返回实例本身,支持流畅的调用风格。 ## 快速开始 ### 环境要求 - Cangjie 语言环境 - 底层 C++ 库(需要预先编译好与 Lua 5.4 链接的动态库) ### 基本用法 ```cangjie 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`)**: ```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) 这是该引擎的核心特性,允许数据在多个脚本间自动流转,无需在仓颉层进行手动传递。 ### 调用规范 1. **脚本编写**:作为管道节点的 Lua 脚本,必须使用 Lua 的变长参数语法 `...` 来接收上一个节点传递的数据。 ```lua -- node_b.lua local input = ... return "处理结果: " .. input ``` 2. **加载顺序(逆序)**:利用栈的后进先出(LIFO)特性,需按照执行顺序的**逆序**进行加载。 - 期望的执行顺序:`A` -> `B` -> `C` - 仓颉侧加载顺序: 1. `runner.load("C.lua", "C")` 2. `runner.load("B.lua", "B")` 3. `runner.load("A.lua", "A")` 3. **触发执行**: - 首先执行初始脚本,将其返回值压入栈顶。 - 然后调用 `runScript("", "")`,引擎将自动调用栈顶下方的函数(即最后加载的脚本 `B`),并将栈顶数据作为参数传入。 - `B` 执行完毕后的返回值成为新的栈顶,等待下一次 `runScript` 调用。 ### 管道模式示例 ```cangjie // 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 重定向依赖于文件系统交换,相对于纯内存交互存在微小的性能开销。 ## 许可证 本项目基于 GPLv3 协议开源。这意味着您可以自由地使用、修改和分发本软件,但任何衍生作品也必须以相同的 GPLv3 协议开源。详情请参阅 [LICENSE](LICENSE) 文件。