From 42e8b7676d94b7eeb8537cae40fe8f70ae431215 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 23 Aug 2026 12:03:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0loadFunction=E9=A2=84?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=87=BD=E6=95=B0=E8=83=BD=E5=8A=9B=20+=20AI?= =?UTF-8?q?=E8=BE=85=E5=8A=A9=E7=BC=96=E7=A8=8B=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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 辅助编程标识及预加载函数模式说明 --- README.md | 19 ++- doc/api.md | 57 ++++++++- lib/errors.h | 3 + lib/lua_cj_api.cpp | 59 +++++++++ lib/lua_cj_api.h | 20 +++ lib/lua_runner.cpp | 152 ++++++++++++++++++++++- lib/lua_runner.hpp | 17 ++- src/bridge.cj | 85 +++++++++++++ src/lua_runner_test.cj | 175 +++++++++++++++++++++++++++ test/scripts/func_add.lua | 6 + test/scripts/func_multi_call.lua | 5 + test/scripts/func_not_a_function.lua | 2 + test/scripts/func_runtime_error.lua | 5 + test/test_lua_cj_api.cpp | 160 ++++++++++++++++++++++++ 14 files changed, 759 insertions(+), 6 deletions(-) create mode 100644 test/scripts/func_add.lua create mode 100644 test/scripts/func_multi_call.lua create mode 100644 test/scripts/func_not_a_function.lua create mode 100644 test/scripts/func_runtime_error.lua diff --git a/README.md b/README.md index 98d6842..cfc49bf 100644 --- a/README.md +++ b/README.md @@ -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 脚本模块,避免全局污染。 diff --git a/doc/api.md b/doc/api.md index 3c3b3d0..d991692 100644 --- a/doc/api.md +++ b/doc/api.md @@ -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) diff --git a/lib/errors.h b/lib/errors.h index 09c2eb1..a39d65c 100755 --- a/lib/errors.h +++ b/lib/errors.h @@ -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 \ No newline at end of file diff --git a/lib/lua_cj_api.cpp b/lib/lua_cj_api.cpp index 4d7f3cb..6dc05e3 100755 --- a/lib/lua_cj_api.cpp +++ b/lib/lua_cj_api.cpp @@ -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 执行语句(字符串类型) diff --git a/lib/lua_cj_api.h b/lib/lua_cj_api.h index 449f85a..77c9dc7 100755 --- a/lib/lua_cj_api.h +++ b/lib/lua_cj_api.h @@ -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 脚本路径 diff --git a/lib/lua_runner.cpp b/lib/lua_runner.cpp index 090178f..fe73bb0 100755 --- a/lib/lua_runner.cpp +++ b/lib/lua_runner.cpp @@ -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状态机状态 diff --git a/lib/lua_runner.hpp b/lib/lua_runner.hpp index 0376e2e..17b6fd4 100644 --- a/lib/lua_runner.hpp +++ b/lib/lua_runner.hpp @@ -15,17 +15,28 @@ typedef struct loaded_package }ld_pkg; +// 预加载函数表:使用 luaL_ref 存入 Lua Registry,ref 为绝对索引,不受栈变化影响 +#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); }; diff --git a/src/bridge.cj b/src/bridge.cj index acb116f..ce0e6fb 100644 --- a/src/bridge.cj +++ b/src/bridge.cj @@ -15,6 +15,12 @@ foreign func run(selfd: CPointer, path: CString, arg: CString): Int32 @C foreign func dostring(selfd: CPointer,target: CString): Int32 @C +foreign func loadfunction(selfd: CPointer,path: CString, name: CString): Int32 +@C +foreign func unloadfunction(selfd: CPointer,name: CString): Int32 +@C +foreign func callfunction(selfd: CPointer,name: CString, arg: CString): Int32 +@C foreign func cleanup(selfd: CPointer): 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 转为 NULL,C 层据此识别“无参数” + 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 } diff --git a/src/lua_runner_test.cj b/src/lua_runner_test.cj index 2fbb973..e46b0de 100644 --- a/src/lua_runner_test.cj +++ b/src/lua_runner_test.cj @@ -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 + } + } +} diff --git a/test/scripts/func_add.lua b/test/scripts/func_add.lua new file mode 100644 index 0000000..c8702a5 --- /dev/null +++ b/test/scripts/func_add.lua @@ -0,0 +1,6 @@ +-- func_add.lua: 顶层执行一次(计数),返回闭包函数供 callFunction 多次调用 +local callCount = 0 +return function(input) + callCount = callCount + 1 + return "result: " .. input .. " (call #" .. callCount .. ")" +end diff --git a/test/scripts/func_multi_call.lua b/test/scripts/func_multi_call.lua new file mode 100644 index 0000000..9e4f50d --- /dev/null +++ b/test/scripts/func_multi_call.lua @@ -0,0 +1,5 @@ +-- func_multi_call.lua: 顶层执行一次(初始化),返回无参闭包 +local init = "init-done" +return function() + return "multi:" .. init +end diff --git a/test/scripts/func_not_a_function.lua b/test/scripts/func_not_a_function.lua new file mode 100644 index 0000000..55c0310 --- /dev/null +++ b/test/scripts/func_not_a_function.lua @@ -0,0 +1,2 @@ +-- func_not_a_function.lua: 顶层返回非函数,loadFunction 应报 5025 +return "not a function" diff --git a/test/scripts/func_runtime_error.lua b/test/scripts/func_runtime_error.lua new file mode 100644 index 0000000..57857f6 --- /dev/null +++ b/test/scripts/func_runtime_error.lua @@ -0,0 +1,5 @@ +-- func_runtime_error.lua: 返回会在运行时抛错的函数 +return function() + local x = nil + return x.nonexistent +end diff --git a/test/test_lua_cj_api.cpp b/test/test_lua_cj_api.cpp index 9ed6f9e..e997efb 100644 --- a/test/test_lua_cj_api.cpp +++ b/test/test_lua_cj_api.cpp @@ -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();