Files
LuaCangjia_api/doc/api.md
JianFeeeee 4af121c1f3 refactor!: 错误码体系重构 删除特殊平台语境残留
BREAKING CHANGE: 错误码编号全部变更, 外部依赖旧编号的代码需按映射表更新

- 删除 10 个从未使用的错误码:
  JSON/MCP 系列(5001/5002/5006/5007/5008) + 其余死码(5004/5009/5013/5018/5021)
- 重排为连续 5001~5016
- 宏前缀 NAPI_(鸿蒙 NAPI 语境残留) 改为 LUAERR_
- 同步更新: errors.h / bridge.cj getErrorMessage+typed抛出点 /
  lua_runner_test.cj 断言 / api.md 对照表 / README.md / GTest 宏名
- 清理注释残留: 鸿蒙 el2 沙盒路径 TODO / 沙盒内路径描述
- 版本号 0.2.1 -> 0.2.2

验证: GTest 44/44 通过; cjpm build 成功; 独立程序运行时 9/9 通过
(含新编号 5016/5013 错误路径)
2026-08-25 09:29:48 +08:00

253 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 文件、编译成 chunk、压入虚拟栈并记录栈位置。`name` 仅作为内部标识。与管道模式配套使用。 | 实例本身 (`This`) |
| `loadFunction(path: String, name: String): This` | **预加载函数到内存**。加载文件 → 执行顶层代码(仅执行一次) → 捕获返回的函数对象 → 存入 Lua Registry。与 `load` 完全独立的两套存储(不共用 `pkgs` 栈),被加载的 Lua 文件顶层必须 `return` 一个函数。 | 实例本身 (`This`) |
| `callFunction(name: String, arg: String): String` | **按名称调用预加载的函数**。从 Registry 取出函数后 `pcall`**不消耗该函数**,可多次重复调用。与管道模式互不干扰。 | 函数执行的字符串结果 |
| `callFunctionInt(name: String, val: Int64): Int64` | **typed 直通调用**。传 Int64 参数(映射为 Lua integer返回整数结果。结果非数值时抛 5016。 | 整数结果 |
| `callFunctionNum(name: String, val: Float64): Float64` | **typed 直通调用**。传 Float64 参数(映射为 Lua number返回浮点结果。结果非数值时抛 5016。 | 浮点结果 |
| `callFunctionBool(name: String, val: Bool): Bool` | **typed 直通调用**。传 Bool 参数(映射为 Lua boolean返回布尔结果。结果为 nil/string 时抛 5016。 | 布尔结果 |
| `runScriptInt(path: String, val: Int64): Int64` | 运行脚本并传 Int64 参数,返回整数结果。独立调用(执行后弹出返回值、保持虚拟栈清洁),非管道模式。 | 整数结果 |
| `runScriptNum(path: String, val: Float64): Float64` | 同上Float64 版本。 | 浮点结果 |
| `runScriptBool(path: String, val: Bool): Bool` | 同上Bool 版本。 | 布尔结果 |
| `resultType(): Int32` | 获取最近一次调用的结果类型:`0`=nil `1`=bool `2`=int `3`=num `4`=str | 类型常量 |
| `unloadFunction(name: String): This` | 卸载预加载的函数(释放 Registry 引用)。 | 实例本身 (`This`) |
| `unload(name: String): This` | 从内部栈中卸载由 `name` 标识的块(仅适用于 `load` 加载的块)。 | 实例本身 (`This`) |
| `runScript(path: String, arg: String): String` | **核心执行方法**。 - 若 `path` 非空:加载并执行该脚本,`arg` 作为单参数传入。 - 若 `path` 为空:触发**管道模式**,调用栈顶函数,并按内部栈布局自动计算参数数量。 | 脚本执行的字符串结果 |
| `doString(target: String): String` | 执行单条 Lua 代码字符串。`target` 为合法的 Lua 代码片段,必须返回字符串类型结果。 | 执行结果的字符串 |
| `clear(): This` | 清理 Lua 状态机,清空全局变量和所有加载的库,恢复到初始状态。 | 实例本身 (`This`) |
| `result(): String` | 获取最近一次 `runScript``doString` 成功执行的返回值。 | 字符串 |
| `error(): String` | 获取最近一次错误的原始描述信息。 | 字符串 |
## 异常类:`LuaError`
所有 Lua 相关操作失败时抛出的异常。
- **属性**:
- `code: Int32`:错误码,用于程序逻辑判断。
- **方法**:
- `getMessage(): String`:获取错误的详细描述。
### 错误码对照表
错误码连续编号 `5001~5016`,宏前缀为 `LUAERR_`
| 错误码 | 宏定义 | 描述 |
| :----- | :--------------------- | :------------------------------------------------------- |
| 5001 | `LUAERR_LOAD_FILE` | 文件加载错误,包含路径、权限以及脚本编译期语法错误。 |
| 5002 | `LUAERR_LUA_STATE` | Lua 状态机初始化失败或已损坏。 |
| 5003 | `LUAERR_SCRIPT_ERROR` | 脚本内部错误,请通过 `result()` 获取详细信息。 |
| 5004 | `LUAERR_SCRIPT_BAD_RET` | 脚本返回值类型不符合要求(如返回非字符串且无法转换)。 |
| 5005 | `LUAERR_UNLOADLIB_FAIL` | 尝试卸载未加载的库。 |
| 5006 | `LUAERR_LUA_STACK` | 函数调用时栈布局与约定不符。 |
| 5007 | `LUAERR_INIT_FAIL` | Lua 状态机初始化错误。 |
| 5008 | `LUAERR_LIB_OVERFLOW` | 加载的库数量超过上限(最大 20 个)。 |
| 5009 | `LUAERR_CLASS_LOST` | 底层 LuaRunner 对象丢失。 |
| 5010 | `LUAERR_RESET_INPUT_FILE` | 重置输入文件失败,可能导致输入污染。 |
| 5011 | `LUAERR_CALLBACK` | 回调函数执行过程中发生错误(含 `doString` 语法错误)。 |
| 5012 | `LUAERR_NOCHUNK_FOUND` | 没有找到待调用的函数。 |
| 5013 | `LUAERR_FUNCTION_NOT_FOUND` | `callFunction` 调用时未找到指定名称的预加载函数。 |
| 5014 | `LUAERR_FUNC_OVERFLOW` | 预加载函数数量超过上限(最大 20 个)。 |
| 5015 | `LUAERR_FUNCTION_INVALID` | 预加载的文件顶层返回的不是函数。 |
| 5016 | `LUAERR_RESULT_TYPE` | typed interop 中请求的结果类型与实际返回类型不兼容(如将 string 读作 int。 |
## 预加载函数模式 (Preload Function Mode)
与管道模式不同,预加载模式适合“加载一次、多次调用”的场景。
### 调用规范
1. **脚本编写**:被预加载的 Lua 文件**必须在顶层 `return` 一个函数**,函数的闭包状态(如计数器、配置)在预加载时初始化,后续调用不会重置。
```lua
-- counter.lua
local count = 0 -- 预加载时初始化一次
return function(arg)
count = count + 1
return "arg=" .. arg .. " count=" .. count
end
```
2. **预加载**`runner.loadFunction("counter.lua", "counter")`
3. **多次调用**`runner.callFunction("counter", "x")` 可重复调用,`count` 持续累加。
4. **卸载**`runner.unloadFunction("counter")` 释放函数。
### 与管道模式的区别
| 维度 | `load` (管道模式) | `loadFunction` (预加载模式) |
| :--- | :--- | :--- |
| 存储位置 | 虚拟栈(`pkgs[]`,相对索引) | Lua Registry`funcs[]`,绝对引用) |
| 顶层代码 | 每次调用都重新执行 | 预加载时执行一次 |
| 调用方式 | `runScript("", arg)` 触发 | `callFunction(name, arg)` |
| 生命周期 | 调用后消耗(`pkg_cont--` | 不消耗,可重复调用 |
| 与另一方关系 | 互不干扰(存储完全解耦) | 互不干扰(存储完全解耦) |
### 示例
```cangjie
let runner = LuaRunner()
// 预加载:顶层代码执行一次
runner.loadFunction("./scripts/counter.lua", "counter")
// 多次调用:计数器持续累加
println(runner.callFunction("counter", "a")) // arg=a count=1
println(runner.callFunction("counter", "b")) // arg=b count=2
println(runner.callFunction("counter", "c")) // arg=c count=3
// 卸载
runner.unloadFunction("counter")
```
## 类型化交互 (Typed Interop) — v0.2.2
除字符串形式外,仓颉侧的 `Int64`/`Float64`/`Bool` 可与 Lua 的 `integer`/`number`/`boolean` **直接互转**,不经过字符串序列化。
### 支持的类型映射
| 仓颉 | Lua | 说明 |
| :--- | :--- | :--- |
| `Int64` | `integer` | 64 位整数,可跨 `2^53` 保真 |
| `Float64` | `number` | 64 位浮点 |
| `Bool` | `boolean` | 布尔 |
| `String` | `string` | 原 callFunction/runScript 路径 |
### 调用规范
```cangjie
let runner = LuaRunner()
// 预加载inc.lua 顶层返回 function(n) return n + 1 end
runner.loadFunction("./scripts/inc.lua", "inc")
// typed 调用:参数与结果均为整数,不转字符串
let n = runner.callFunctionInt("inc", 41) // n == 42 (Int64)
let d = runner.callFunctionNum("mul", 4.0) // Float64
let b = runner.callFunctionBool("neg", true) // Bool
// 脚本直通:脚本直接返回入参(如 return (...)
let r = runner.runScriptInt("./scripts/echo.lua", 777) // 777
// 查询结果类型0=nil 1=bool 2=int 3=num 4=str
let t = runner.resultType()
```
### 类型校验规则
- `callFunctionInt`/`runScriptInt`:接受 Lua `integer`CJT_INT与 `number`CJT_NUM结果其余nil/string/bool/table抛 **5016**。
- `callFunctionNum`/`runScriptNum`:同上,接受 int/num 结果。
- `callFunctionBool`/`runScriptBool`:接受 bool/int/num 结果nil 与 string 抛 **5016**。
- 现有字符串 API`callFunction`/`runScript`)行为不变;每次调用都会同步更新类型化结果字段,因此 `callFunction("x", "10")` 之后 `resultType()` 返回 `4`str
### 与管道模式的关系
`runScriptInt/Num/Bool` 是**独立调用**:执行后弹出返回值、保持虚拟栈清洁,不参与管道链。管道模式请继续使用字符串版 `runScript(path, arg)`(其结果留在栈上供下一节点消费)。
### 内部实现
- C 层新增 `callfunction_int/num/bool/void`、`run_int/num/bool/void`、`result_type/int/num/bool` 共 11 个 FFI 函数。
- C++ 层 `Lua_runner` 新增 `last_result_type/int/num/bool` 字段,`capture_result()` 读取 Lua 栈顶并记录类型,`finish_call()` 计算 `lua_pcall` 实际返回值数量(`nres = post_top - pre_top + nargs + 1`)以正确处理无返回值场景。
- typed 结果同时同步到字符串缓冲区 `self->result`,因此 `result()`/`getresult` 与 typed getter 可以混用。
## 管道模式 (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` 调用。
### 运行时语义说明
- 当 `path` 非空时,脚本会先完成编译并压入内部栈,再进入执行阶段。
- 若脚本在编译阶段发生语法错误,当前实现返回 `LUAERR_LOAD_FILE` (5001),详细信息可通过 `result()` 获取。
- 在 `require` / `package.path` 场景下,`runScript` 返回当前 Lua 栈顶字符串结果;若栈顶内容是模块解析路径,则会直接返回该路径字符串。
### 管道模式示例
```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` 回调。您需要在回调中准备并提供输入数据(例如从文件读取或返回模拟数据),供底层读取。
## doString 使用说明
`doString` 允许直接执行 Lua 代码字符串,而无需写入文件。
### 基本用法
```cangjie
let runner = LuaRunner()
// 执行简单表达式
let res = runner.doString("return 1 + 1")
println(res) // 输出: 2
// 设置全局变量并读取
let res2 = runner.doString("g = 42; return g")
println(res2) // 输出: 42
```
### 注意事项
- `doString` 要求代码执行后栈顶必须是**字符串类型**的返回值
- 若代码不返回字符串(如返回 `nil`、数字或无返回值),将抛出 `LUAERR_SCRIPT_BAD_RET` (5004) 错误
- 若代码存在语法错误,将抛出 `LUAERR_CALLBACK` (5011) 错误
- `doString` 与 `runScript` 共享同一个 Lua 状态机,全局变量和已加载的模块可互相访问