feat: 实现loadFunction预加载函数能力 + AI辅助编程标识

- 新增 loadFunction/callFunction/unloadFunction 预加载函数 API
- 预加载语义:加载文件→顶层执行一次→返回函数存入Lua Registry→可多次调用
- 与管道模式(load/runScript)存储完全解耦:funcs[]+Registry vs pkgs[]+虚拟栈
- 新增错误码 5023/5024/5025
- 新增单元测试:仓颉侧 8 个 + C++ GTest 侧 11 个
- 本机以 Cangjie 1.1.0 实编译并通过功能运行验证
- README/doc 增加 AI 辅助编程标识及预加载函数模式说明
This commit is contained in:
JianFeeeee
2026-08-23 12:03:04 +08:00
parent 207082a1a0
commit 42e8b7676d
14 changed files with 759 additions and 6 deletions

View File

@ -3,9 +3,25 @@
[![License](https://img.shields.io/badge/License-LGPL%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/)
[![AI-Assisted](https://img.shields.io/badge/🤖_AI_Assisted-20%25-orange)](README.md#ai-%E8%BE%85%E5%8A%A9%E7%BC%96%E7%A8%8B%E6%A0%87%E8%AF%86)
![AigcAssets.png](https://raw.atomgit.com/user-images/assets/9445015/a1976a82-4d44-493a-8efd-fb12a104268e/AigcAssets.png "AigcAssets.png")
**v0.2.1版本更新**新增doString方法可执行单句lua更新了文档布局与readme优化了文档布局。
**v0.2.1版本更新**新增doString方法可执行单句lua新增loadFunction/callFunction预加载能力AI辅助更新了文档布局与readme优化了文档布局。
---
## 🤖 AI 辅助编程标识
> [!IMPORTANT]
> 本项目部分代码由 **AI 辅助生成**(包括但不限于:`loadFunction`/`callFunction`/`unloadFunction` 预加载函数能力、相关单元测试与文档)。
> AI 生成代码均经过人工审查与实机验证(本仓库在本机以 Cangjie 1.1.0 实编译并通过功能运行验证),但请在使用前自行复核。
| 模块 | AI 辅助程度 | 说明 |
| :--- | :--- | :--- |
| `loadFunction`/`callFunction`/`unloadFunction` | 高AI 主写,人工审查) | 预加载函数能力:加载文件→顶层执行一次→返回函数存入 Registry→可多次调用 |
| 单元测试(新增部分) | 中AI 主写,人工修正) | 预加载相关 GTest / 仓颉单测 |
| 文档(新增部分) | 低AI 起草,人工定稿) | README / api.md 预加载部分 |
---
Lua Runner for Cangjie 是一个专为仓颉语言设计的轻量级、高性能 Lua 脚本执行引擎。它通过 C FFI 桥接 C++,提供了稳定且易用的 Lua 虚拟机管理能力。
**注:本项目是开发原生鸿蒙应用时产生的副产物,当前版本依然存在局限性与不足,请详细检查后再使用。**
@ -18,6 +34,7 @@ Lua Runner for Cangjie 是一个专为仓颉语言设计的轻量级、高性能
- **轻量级集成**:基于 Lua 5.4,通过 FFI 直接与仓颉语言交互,性能损耗小。
- **栈式管道模式**:支持脚本间基于栈的数据隐式传递,实现类似函数式管道的调用链。
- **预加载函数**:提供 `loadFunction`/`callFunction`/`unloadFunction`,预加载一次 Lua 函数到内存Registry顶层代码仅执行一次后续可多次调用与管道模式互不干扰。
- **doString**:直接执行 Lua 代码字符串,无需写入文件。
- **I/O 重定向**:可自定义 Lua 标准输入/输出(`print`, `io.read`)的回调函数,支持文件交换目录重定向。
- **模块化管理**:提供 `load``unload` 方法,支持按名称动态加载和卸载 Lua 脚本模块,避免全局污染。

View File

@ -20,8 +20,11 @@ Lua 虚拟机的主要管理类。
| 方法 | 描述 | 返回值 |
| :--- | :--- | :--- |
| `load(path: String, name: String): This` | 加载指定路径的 Lua 文件编译后以 `name` 为标识压入内部栈。 | 实例本身 (`This`) |
| `unload(name: String): This` | 从内部栈中卸载由 `name` 标识的模块。 | 实例本身 (`This`) |
| `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`**不消耗该函数**,可多次重复调用。与管道模式互不干扰。 | 函数执行的字符串结果 |
| `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`) |
@ -63,6 +66,56 @@ Lua 虚拟机的主要管理类。
| 5020 | `NAPI_ERROR_FUNCS` | 回调函数执行过程中发生错误(含 `doString` 语法错误)。 |
| 5021 | `NAPI_CHDIR_ERROR` | 切换工作目录失败,可能导致包搜索错误,日志写入路径错误等。 |
| 5022 | `NAPI_NOCHUNK_FOUND` | 没有找到待调用的函数。 |
| 5023 | `NAPI_FUNCTION_NOT_FOUND` | `callFunction` 调用时未找到指定名称的预加载函数。 |
| 5024 | `NAPI_LOAD_FUNCTION_OVER` | 预加载函数数量超过上限(最大 20 个)。 |
| 5025 | `NAPI_FUNCTION_NOT_VALID` | 预加载的文件顶层返回的不是函数。 |
## 预加载函数模式 (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")
```
## 管道模式 (Pipeline Mode)

View File

@ -23,5 +23,8 @@
#define NAPI_ERROR_FUNCS 5020 //函数调用错误
#define NAPI_CHDIR_ERROR 5021 //切换工作目录失败,可能导致包搜索错误,日志写入路径错误等
#define NAPI_NOCHUNK_FOUND 5022 //没有找到待调用的函数
#define NAPI_FUNCTION_NOT_FOUND 5023 //通过callfunction调用时未找到指定函数
#define NAPI_LOAD_FUNCTION_OVER 5024 //预加载函数数量超过上限
#define NAPI_FUNCTION_NOT_VALID 5025 //预加载文件顶层执行后返回的不是函数
#endif

View File

@ -86,6 +86,65 @@ int run(void *selfd,const char *path,const char *arg)
return 0;
}
/// @brief 预加载lua文件到内存加载文件、执行顶层代码、捕获返回的函数并存入 Registry供后续 callfunction 多次调用
/// @param selfd init_lua_runner返回的void指针
/// @param path 需要加载的文件路径
/// @param name 函数名称callfunction/unloadfunction时使用
/// @return 0代表正常结束-1表示异常退出
int loadfunction(void *selfd,const char *path,const char *name)
{
lua_runner* self = (lua_runner*)selfd;
Lua_runner *runner = (Lua_runner*)self->lua_obj;//还原对象
if(runner == NULL)
{
errno = NAPI_LUA_STATE_ERROR;
return -1;
}
if(runner->loadfunction(path,name)==-1)
return -1;
return 0;
}
/// @brief 卸载预加载的函数(释放 Registry 引用)
/// @param selfd init_lua_runner返回的void指针
/// @param name loadfunction加载时的函数名称
/// @return 0代表正常结束-1表示异常退出
int unloadfunction(void *selfd,const char *name)
{
lua_runner* self = (lua_runner*)selfd;
Lua_runner *runner = (Lua_runner*)self->lua_obj;//还原对象
if(runner == NULL)
{
errno = NAPI_LUA_STATE_ERROR;
return -1;
}
if(runner->unloadfunction(name)==-1)
return -1;
return 0;
}
/// @brief 按名称调用已预加载的函数(从 Registry 取出,不消耗该函数,可重复调用)
/// @param selfd init_lua_runner返回的void指针
/// @param name loadfunction加载时的函数名称
/// @param arg 传递给函数的单参数可为NULL
/// @return 成功返回0,失败返回-1,并将lua出错原因拷贝至result中
int callfunction(void *selfd,const char *name,const char *arg)
{
lua_runner* self = (lua_runner*)selfd;
Lua_runner *runner = (Lua_runner*)self->lua_obj;
if(runner == NULL)
{
errno = NAPI_LUA_STATE_ERROR;
return -1;
}
if(runner->callfunction(name,arg)==-1){
snprintf(self->result,MAX_RESULT,"%s",runner->reslt.c_str());
return -1;
}
snprintf(self->result,MAX_RESULT,"%s",runner->reslt.c_str());
return 0;
}
/// @brief 执行单句lua语句
/// @param selfd init_lua_runner返回的void指针
/// @param target 执行语句(字符串类型)

View File

@ -55,6 +55,26 @@ int load_lib(void *selfd,const char *path,const char *name);
/// @return 成功返回0,失败返回-1
int unload_lib(void *selfd,const char *name);
/// @brief 预加载lua文件到内存加载文件、执行顶层代码、捕获返回的函数并存入 Registry供后续 callfunction 多次调用
/// @param selfd init_lua_runner返回的void指针
/// @param path 需要加载的文件路径
/// @param name 函数名称callfunction/unloadfunction时使用
/// @return 0代表正常结束-1表示异常退出
int loadfunction(void *selfd,const char *path,const char *name);
/// @brief 卸载预加载的函数(释放 Registry 引用)
/// @param selfd init_lua_runner返回的void指针
/// @param name loadfunction加载时的函数名称
/// @return 0代表正常结束-1表示异常退出
int unloadfunction(void *selfd,const char *name);
/// @brief 按名称调用已预加载的函数(从 Registry 取出,不消耗该函数,可重复调用)
/// @param selfd init_lua_runner返回的void指针
/// @param name loadfunction加载时的函数名称
/// @param arg 传递给函数的单参数可为NULL
/// @return 成功返回0,失败返回-1,并将lua出错原因拷贝至result中
int callfunction(void *selfd,const char *name,const char *arg);
/// @brief 运行lua脚本
/// @param selfd init_lua_runner返回的void指针
/// @param path 脚本路径

View File

@ -25,7 +25,7 @@ Lua_runner::Lua_runner(const char *path)
luaL_dostring(this->L, save_stdlib_code);
/*if(luaL_dostring(this->L,
"package.path = \";/data/storage/el2/luapkg/?/init.lua\"") != LUA_OK)//TODO 修改加载库的默认路径指向沙盒内路径
errno = NAPI_LUA_INITFAIL;//加载默认路径*/
errno = NAPI_LUA_INITFAIL;//加载默认路径*/
if(path !=NULL)
{
char buf[1024];
@ -36,6 +36,7 @@ Lua_runner::Lua_runner(const char *path)
errno = NAPI_LUA_INITFAIL;
}
this->pkg_cont = 0;
this->func_cont = 0;
}
Lua_runner::~Lua_runner()
@ -160,12 +161,161 @@ int Lua_runner::clean()//进行新一轮调用前一定要先clean清除上个
lua_remove(this->L,this->pkgs[i].ref);
}
this->pkg_cont = 0;
// 清理预加载函数
for(int i = 0; i < this->func_cont; i++)
{
luaL_unref(this->L, LUA_REGISTRYINDEX, this->funcs[i].ref);
}
this->func_cont = 0;
this->reslt ="nil";
lua_settop(this->L, 0);//清空栈
return 0;
}
//TODO 支持更多类型的参数
int Lua_runner::loadfunction(const char *path,const char *name)
{
if(!this->check_luastatue())
return -1;
if(!this->check_file_exists(path))
{
errno = NAPI_LOAD_FILE_ERROR;
return -1;
}
if(this->func_cont >= MAX_LUA_FUNC)
{
errno = NAPI_LOAD_FUNCTION_OVER;
return -1;
}
// 检查是否已存在同名函数
for(int i = 0; i < this->func_cont; i++)
{
if(strcmp(this->funcs[i].name, name) == 0)
{
errno = NAPI_FUNCTION_NOT_FOUND;
return -1;
}
}
// 1. 加载文件到栈顶(编译成 chunk
if(luaL_loadfile(this->L, path) != LUA_OK)
{
const char *ret = lua_tostring(this->L, -1);
this->reslt = ret;
errno = NAPI_LOAD_FILE_ERROR;
lua_pop(this->L, 1);
return -1;
}
// 2. 执行 chunk顶层代码期望返回一个函数
if(lua_pcall(this->L, 0, 1, 0) != LUA_OK)
{
errno = NAPI_SCRIPT_ERROR;
this->reslt = lua_tostring(this->L, -1);
lua_pop(this->L, 1);
return -1;
}
// 3. 检查栈顶是否为函数
if(!lua_isfunction(this->L, -1))
{
errno = NAPI_FUNCTION_NOT_VALID;
lua_pop(this->L, 1);
return -1;
}
// 4. 存入 Lua Registry绝对引用不受栈变化影响
int ref = luaL_ref(this->L, LUA_REGISTRYINDEX);
// 5. 记录到 funcs 表
snprintf(this->funcs[this->func_cont].name, 128, "%s", name);
this->funcs[this->func_cont].ref = ref;
this->func_cont++;
return 0;
}
int Lua_runner::unloadfunction(const char *name)
{
int sig = -1;
for(int i = 0; i < this->func_cont; i++)
{
if(strcmp(this->funcs[i].name, name) == 0)
{
sig = i;
break;
}
}
if(sig == -1)
{
errno = NAPI_FUNCTION_NOT_FOUND;
return -1;
}
if(!this->check_luastatue())
return -1;
// 释放 Registry 引用
luaL_unref(this->L, LUA_REGISTRYINDEX, this->funcs[sig].ref);
// 从 funcs 表中移除
for(int j = sig + 1; j < this->func_cont; j++)
{
this->funcs[j - 1] = this->funcs[j];
}
this->func_cont--;
return 0;
}
int Lua_runner::callfunction(const char *name,const char *arg)
{
if(!this->check_luastatue())
return -1;
// 在 funcs 表中查找(预加载函数表,独立于管道栈 pkgs[]
int sig = -1;
for(int i = 0; i < this->func_cont; i++)
{
if(strcmp(this->funcs[i].name, name) == 0)
{
sig = i;
break;
}
}
if(sig == -1)
{
errno = NAPI_FUNCTION_NOT_FOUND;
return -1;
}
// 从 Lua Registry 中取出函数(绝对引用,不受栈变化影响)
lua_rawgeti(this->L, LUA_REGISTRYINDEX, this->funcs[sig].ref);
// 如果有参数,压入参数
if(arg != NULL)
{
lua_pushlstring(this->L, arg, strlen(arg));
}
int argcount = (arg != NULL) ? 1 : 0;
if(lua_pcall(this->L, argcount, LUA_MULTRET, 0) == LUA_OK)
{
if(lua_isstring(this->L, -1))
this->reslt = lua_tostring(this->L, -1);
lua_pop(this->L, 1);
return 0;
}
else
{
errno = NAPI_SCRIPT_ERROR;
this->reslt = lua_tostring(this->L, -1);
lua_pop(this->L, 1);
return -1;
}
}
int Lua_runner::run(const char *path,const char *arg)
{
if(!this->check_luastatue())//检查lua状态机状态

View File

@ -15,17 +15,28 @@ typedef struct loaded_package
}ld_pkg;
// 预加载函数表:使用 luaL_ref 存入 Lua Registryref 为绝对索引,不受栈变化影响
#define MAX_LUA_FUNC 20
typedef struct loaded_function
{
char name[128];
int ref; // luaL_ref 返回的绝对索引
}ld_func;
#ifdef __cplusplus
class Lua_runner//lua运行器对象
{
public:
Lua_runner(const char *path = NULL);//创建lua状态机并持久化持有
~Lua_runner();//销毁状态机
int load_lib(const char *path,const char *name);//加载库
int load_lib(const char *path,const char *name);//加载库(块压栈)
int unload_lib(const char *name);//解除加载
int loadfunction(const char *path,const char *name);//预加载函数(顶层执行,结果存入独立函数表)
int unloadfunction(const char *name);//卸载预加载函数
int callfunction(const char *name,const char *arg);//按名称调用已预加载的函数
int run(const char *path = NULL,const char *arg = NULL);
int clean();//清除状态机缓存
//int callfunction();
int dostring(const char *target);
std::string reslt;
lua_State *get_lua_State();
@ -33,6 +44,8 @@ class Lua_runner//lua运行器对象
lua_State *L;
int pkg_cont;
ld_pkg pkgs[MAX_LUA_LIB];
int func_cont;
ld_func funcs[MAX_LUA_FUNC];
bool check_luastatue();
bool check_file_exists(const char *path);
};

View File

@ -15,6 +15,12 @@ foreign func run(selfd: CPointer<Unit>, path: CString, arg: CString): Int32
@C
foreign func dostring(selfd: CPointer<Unit>,target: CString): Int32
@C
foreign func loadfunction(selfd: CPointer<Unit>,path: CString, name: CString): Int32
@C
foreign func unloadfunction(selfd: CPointer<Unit>,name: CString): Int32
@C
foreign func callfunction(selfd: CPointer<Unit>,name: CString, arg: CString): Int32
@C
foreign func cleanup(selfd: CPointer<Unit>): Int32
@C
foreign func get_errno(): Int32
@ -94,6 +100,9 @@ public class LuaError <: Exception {
case 5020 => "Error in callback functions"
case 5021 => "change workdir error"
case 5022 => "No callable chunk found"
case 5023 => "Function not found"
case 5024 => "Too many preloaded functions"
case 5025 => "Preloaded file must return a function"
case _ => "Unknown error"
}
}
@ -230,6 +239,82 @@ public class LuaRunner {
return this.cachedResult
}
/// 加载函数到内存(预加载)
///
/// 加载 Lua 文件 → 编译成 chunk → 执行顶层代码(顶层代码只执行一次)→ 捕获返回的 Lua 函数 → 存入独立的 Registry 引用表。
/// 与 load() 的块压栈不同:
/// - load() 把 chunk 留在虚拟栈上(管道模式用),每次调用会重新执行
/// - loadFunction() 执行一次顶层代码,保留返回的函数对象,后续 callFunction 多次调用不重复执行
/// 注意:被加载的 Lua 文件必须在其顶层 return 一个函数。
///
/// @param path 函数所在文件路径
/// @param name 函数名称callFunction/unloadFunction 时使用)
/// @return 当前实例
/// @throws LuaError 当文件不存在/顶层未返回函数/已存在同名函数时抛出
public func loadFunction(path: String, name: String): This {
let pathPtr = toCStr(path)
let namePtr = toCStr(name)
let result = unsafe { loadfunction(this.handle, pathPtr, namePtr) }
unsafe {
if (!pathPtr.isNull()) { LibC.free(pathPtr) }
if (!namePtr.isNull()) { LibC.free(namePtr) }
}
if (result != 0) {
throw LuaError(unsafe { get_errno() })
}
return this
}
/// 卸载预加载的函数(释放 Registry 引用)
///
/// @param name loadFunction 时指定的函数名称
/// @return 当前实例
/// @throws LuaError 当函数未找到时抛出
public func unloadFunction(name: String): This {
let namePtr = toCStr(name)
let result = unsafe { unloadfunction(this.handle, namePtr) }
unsafe {
if (!namePtr.isNull()) { LibC.free(namePtr) }
}
if (result != 0) {
throw LuaError(unsafe { get_errno() })
}
return this
}
/// 调用已预加载的函数
///
/// 通过 name 在预加载函数表中查找,从 Lua Registry 取出函数对象后 pcall 调用。
/// 不消耗该函数,因此可对同一函数重复调用(顶层代码只在 loadFunction 时执行一次)。
/// 注意:与 runScript 管道模式完全独立,互不干扰。
///
/// @param name loadFunction 时指定的函数名称
/// @param arg 传递给函数的单参数;传空字符串表示无参数(底层会转为 NULL
/// @return 函数执行的字符串结果
/// @throws LuaError 当函数不存在或执行出错时抛出
public func callFunction(name: String, arg: String): String {
let namePtr = toCStr(name)
// 空字符串会被 toCStr 转为 NULLC 层据此识别“无参数”
let argPtr = toCStr(arg)
let code = unsafe { callfunction(this.handle, namePtr, argPtr) }
unsafe {
if (!namePtr.isNull()) { LibC.free(namePtr) }
if (!argPtr.isNull()) { LibC.free(argPtr) }
}
this.cachedResult = toString(unsafe { getresult(this.handle) })
if (code != 0) {
throw LuaError(unsafe { get_errno() }, this.cachedResult)
}
return this.cachedResult
}
/// 获取缓存的结果
public func result(): String { this.cachedResult }

View File

@ -323,3 +323,178 @@ func testDefaultIOCallback(): Unit {
let result = unsafe { defaultIO() }
@Expect(result, 0)
}
// ====================
// 8. loadFunction / callFunction 预加载测试
// ====================
@Test
func testLoadFunctionAndCall(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_add.lua"), "add")
let res = runner.callFunction("add", "hello")
// 闭包带计数器,第一次调用应返回 call #1
@Expect(res, "result: hello (call #1)")
}
@Test
func testCallFunctionMultipleTimes(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_multi_call.lua"), "multi")
let r1 = runner.callFunction("multi", "")
@Expect(r1, "multi:init-done")
let r2 = runner.callFunction("multi", "")
@Expect(r2, "multi:init-done")
let r3 = runner.callFunction("multi", "")
@Expect(r3, "multi:init-done")
let r4 = runner.callFunction("multi", "")
@Expect(r4, "multi:init-done")
}
@Test
func testCallFunctionCallCount(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_add.lua"), "add")
@Expect(runner.callFunction("add", "a"), "result: a (call #1)")
@Expect(runner.callFunction("add", "b"), "result: b (call #2)")
@Expect(runner.callFunction("add", "c"), "result: c (call #3)")
}
@Test
func testCallFunctionNotFound(): Unit {
let runner = LuaRunner()
try {
runner.callFunction("nonexistent", "")
fail("Should throw function not found error")
} catch (e: LuaError) {
@Expect(e.code, 5023)
}
}
@Test
func testLoadFunctionFileNotFound(): Unit {
let runner = LuaRunner()
try {
runner.loadFunction("/no/such/file.lua", "bad")
fail("Should throw file not found error")
} catch (e: LuaError) {
@Expect(e.code, 5003)
}
}
@Test
func testLoadFunctionNotAFunction(): Unit {
let runner = LuaRunner()
try {
runner.loadFunction(scriptPath("func_not_a_function.lua"), "bad")
fail("Should throw function not valid error")
} catch (e: LuaError) {
@Expect(e.code, 5025)
}
}
@Test
func testLoadFunctionDuplicate(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_add.lua"), "dup")
try {
runner.loadFunction(scriptPath("func_add.lua"), "dup")
fail("Should throw on duplicate name")
} catch (e: LuaError) {
@Expect(e.code, 5023)
}
}
@Test
func testCallFunctionRuntimeError(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_runtime_error.lua"), "bad")
try {
runner.callFunction("bad", "")
fail("Should throw runtime error")
} catch (e: LuaError) {
@Expect(e.code, 5010)
}
}
@Test
func testCallFunctionWithUnloadFunction(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_add.lua"), "add")
@Expect(runner.callFunction("add", "before"), "result: before (call #1)")
runner.unloadFunction("add")
try {
runner.callFunction("add", "")
fail("Should throw function not found after unloadFunction")
} catch (e: LuaError) {
@Expect(e.code, 5023)
}
// 卸载后可重新加载同名函数
runner.loadFunction(scriptPath("func_add.lua"), "add")
@Expect(runner.callFunction("add", "after"), "result: after (call #1)")
}
@Test
func testLoadFunctionAndPipelineNoInterference(): Unit {
let runner = LuaRunner()
// 预加载函数(用独立 func 脚本)
runner.loadFunction(scriptPath("func_add.lua"), "add")
@Expect(runner.callFunction("add", "x"), "result: x (call #1)")
// 管道模式(用 pipeline 脚本,走 pkgs 栈)
runner.load(scriptPath("pipeline_add_suffix.lua"), "add_suffix")
.load(scriptPath("pipeline_to_upper.lua"), "to_upper")
.load(scriptPath("pipeline_add_prefix.lua"), "add_prefix")
let initialData = runner.runScript(scriptPath("pipeline_generate_data.lua"), "hello")
@Expect(initialData, "hello")
@Expect(runner.runScript("", ""), "[PREFIX] hello")
@Expect(runner.runScript("", ""), "[PREFIX] HELLO")
@Expect(runner.runScript("", ""), "[PREFIX] HELLO [SUFFIX]")
// 管道结束后,预加载函数仍可调用,且计数器继续
@Expect(runner.callFunction("add", "y"), "result: y (call #2)")
}
@Test
func testLoadFunctionAndCleanCoexists(): Unit {
let runner = LuaRunner()
runner.loadFunction(scriptPath("func_add.lua"), "add")
@Expect(runner.callFunction("add", "x"), "result: x (call #1)")
runner.clear()
try {
runner.callFunction("add", "")
fail("Should throw after clean")
} catch (e: LuaError) {
@Expect(e.code, 5023)
}
}
@Test
func testLoadFunctionOverflow(): Unit {
ensureGeneratedScriptsDir()
let runner = LuaRunner()
for (i in 0..20) {
let name = "overflow_func_${i}"
let path = generatedScriptPath(name + ".lua")
File.writeTo(path, ("return function() return \"" + name + "\" end").toArray())
try {
runner.loadFunction(path, name)
} catch (e: LuaError) {
@Expect(e.code, 5024)
break
}
}
}

View File

@ -0,0 +1,6 @@
-- func_add.lua: 顶层执行一次(计数),返回闭包函数供 callFunction 多次调用
local callCount = 0
return function(input)
callCount = callCount + 1
return "result: " .. input .. " (call #" .. callCount .. ")"
end

View File

@ -0,0 +1,5 @@
-- func_multi_call.lua: 顶层执行一次(初始化),返回无参闭包
local init = "init-done"
return function()
return "multi:" .. init
end

View File

@ -0,0 +1,2 @@
-- func_not_a_function.lua: 顶层返回非函数loadFunction 应报 5025
return "not a function"

View File

@ -0,0 +1,5 @@
-- func_runtime_error.lua: 返回会在运行时抛错的函数
return function()
local x = nil
return x.nonexistent
end

View File

@ -326,6 +326,166 @@ TEST_F(LuaCjApiTest, DoStringNilReturn) {
EXPECT_EQ(get_errno(), NAPI_SCRIPT_BAD_RET);
}
// ==================== loadfunction / callfunction 预加载测试 ====================
// 基础预加载 + 调用
TEST_F(LuaCjApiTest, LoadFunctionAndCall) {
int ret = loadfunction(runner, get_script_path("func_add.lua").c_str(), "add");
EXPECT_EQ(ret, 0);
ret = callfunction(runner, "add", "hello");
EXPECT_EQ(ret, 0);
EXPECT_STREQ(getresult(runner), "result: hello (call #1)");
}
// 同一函数多次调用:顶层只执行一次,计数器持续累加
TEST_F(LuaCjApiTest, CallFunctionMultipleTimes) {
ASSERT_EQ(loadfunction(runner, get_script_path("func_add.lua").c_str(), "add"), 0);
EXPECT_EQ(callfunction(runner, "add", "a"), 0);
EXPECT_STREQ(getresult(runner), "result: a (call #1)");
EXPECT_EQ(callfunction(runner, "add", "b"), 0);
EXPECT_STREQ(getresult(runner), "result: b (call #2)");
EXPECT_EQ(callfunction(runner, "add", "c"), 0);
EXPECT_STREQ(getresult(runner), "result: c (call #3)");
}
// 无参函数多次调用
TEST_F(LuaCjApiTest, CallFunctionNoArgMultipleTimes) {
ASSERT_EQ(loadfunction(runner, get_script_path("func_multi_call.lua").c_str(), "multi"), 0);
for (int i = 0; i < 5; i++) {
EXPECT_EQ(callfunction(runner, "multi", NULL), 0);
EXPECT_STREQ(getresult(runner), "multi:init-done");
}
}
// 调用未预加载的函数
TEST_F(LuaCjApiTest, CallFunctionNotFound) {
int ret = callfunction(runner, "nonexistent", NULL);
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_FUNCTION_NOT_FOUND);
}
// 预加载不存在的文件
TEST_F(LuaCjApiTest, LoadFunctionFileNotFound) {
int ret = loadfunction(runner, "/no/such/file.lua", "bad");
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_LOAD_FILE_ERROR);
}
// 预加载顶层未返回函数的文件
TEST_F(LuaCjApiTest, LoadFunctionNotAFunction) {
int ret = loadfunction(runner, get_script_path("func_not_a_function.lua").c_str(), "bad");
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_FUNCTION_NOT_VALID);
}
// 预加载同名函数
TEST_F(LuaCjApiTest, LoadFunctionDuplicate) {
ASSERT_EQ(loadfunction(runner, get_script_path("func_add.lua").c_str(), "dup"), 0);
int ret = loadfunction(runner, get_script_path("func_add.lua").c_str(), "dup");
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_FUNCTION_NOT_FOUND);
}
// 运行时错误
TEST_F(LuaCjApiTest, CallFunctionRuntimeError) {
ASSERT_EQ(loadfunction(runner, get_script_path("func_runtime_error.lua").c_str(), "bad"), 0);
int ret = callfunction(runner, "bad", NULL);
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_SCRIPT_ERROR);
}
// unloadfunction 后再调用应报 5023且可重新加载
TEST_F(LuaCjApiTest, CallFunctionWithUnloadFunction) {
ASSERT_EQ(loadfunction(runner, get_script_path("func_add.lua").c_str(), "add"), 0);
EXPECT_EQ(callfunction(runner, "add", "before"), 0);
EXPECT_STREQ(getresult(runner), "result: before (call #1)");
EXPECT_EQ(unloadfunction(runner, "add"), 0);
int ret = callfunction(runner, "add", NULL);
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_FUNCTION_NOT_FOUND);
// 卸载后可重新预加载,计数器重置
EXPECT_EQ(loadfunction(runner, get_script_path("func_add.lua").c_str(), "add"), 0);
EXPECT_EQ(callfunction(runner, "add", "after"), 0);
EXPECT_STREQ(getresult(runner), "result: after (call #1)");
}
// 预加载与管道模式共存,互不干扰:
// 预加载函数走独立的 funcs[]+Registry管道走 pkgs[] 栈
TEST_F(LuaCjApiTest, LoadFunctionAndPipelineNoInterference) {
// 预加载一个函数(独立于管道栈)
ASSERT_EQ(loadfunction(runner, get_script_path("func_add.lua").c_str(), "add"), 0);
EXPECT_EQ(callfunction(runner, "add", "x"), 0);
EXPECT_STREQ(getresult(runner), "result: x (call #1)");
// 管道模式照常pkgs 栈)
ASSERT_EQ(load_lib(runner, get_script_path("pipeline_add_suffix.lua").c_str(), "add_suffix"), 0);
ASSERT_EQ(load_lib(runner, get_script_path("pipeline_to_upper.lua").c_str(), "to_upper"), 0);
ASSERT_EQ(load_lib(runner, get_script_path("pipeline_add_prefix.lua").c_str(), "add_prefix"), 0);
int ret = run(runner, get_script_path("pipeline_generate_data.lua").c_str(), "hello");
EXPECT_EQ(ret, 0);
EXPECT_STREQ(getresult(runner), "hello");
ret = run(runner, nullptr, nullptr);
EXPECT_EQ(ret, 0);
EXPECT_STREQ(getresult(runner), "[PREFIX] hello");
ret = run(runner, nullptr, nullptr);
EXPECT_EQ(ret, 0);
EXPECT_STREQ(getresult(runner), "[PREFIX] HELLO");
ret = run(runner, nullptr, nullptr);
EXPECT_EQ(ret, 0);
EXPECT_STREQ(getresult(runner), "[PREFIX] HELLO [SUFFIX]");
// 管道跑完后,预加载函数仍可调用,计数器继续
EXPECT_EQ(callfunction(runner, "add", "y"), 0);
EXPECT_STREQ(getresult(runner), "result: y (call #2)");
}
// 预加载数量上限:默认 MAX_LUA_FUNC=20
TEST_F(LuaCjApiTest, LoadFunctionOverflow) {
for (int i = 0; i < MAX_LUA_FUNC; i++) {
char name[20];
snprintf(name, sizeof(name), "func%d", i);
std::string content = "return function() return '" + std::to_string(i) + "' end";
std::string path = temp_dir + "/func" + std::to_string(i) + ".lua";
FILE* f = fopen(path.c_str(), "w");
ASSERT_NE(f, nullptr);
fprintf(f, "%s", content.c_str());
fclose(f);
int ret = loadfunction(runner, path.c_str(), name);
EXPECT_EQ(ret, 0);
}
// 第 21 个应报 5024
std::string path = temp_dir + "/overflow.lua";
FILE* f = fopen(path.c_str(), "w");
ASSERT_NE(f, nullptr);
fprintf(f, "return function() return 'overflow' end");
fclose(f);
int ret = loadfunction(runner, path.c_str(), "overflow");
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_LOAD_FUNCTION_OVER);
}
// cleanup 后预加载函数应失效
TEST_F(LuaCjApiTest, CallFunctionAfterCleanup) {
ASSERT_EQ(loadfunction(runner, get_script_path("func_add.lua").c_str(), "add"), 0);
EXPECT_EQ(callfunction(runner, "add", "x"), 0);
EXPECT_EQ(cleanup(runner), 0);
int ret = callfunction(runner, "add", NULL);
EXPECT_EQ(ret, -1);
EXPECT_EQ(get_errno(), NAPI_FUNCTION_NOT_FOUND);
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();