把 Lua 嵌入 C 或 C++ 程序,真正的接口不是脚本文本,而是 lua_State、虚拟栈和宿主定义的权限。示例使用 Lua 5.4.8 并保持最小。稳定嵌入层需要三个不变量:每个函数明确栈输入输出,所有不可信脚本都通过受保护调用进入,状态和宿主资源只有一个可追踪的所有者。

从一个明确所有者开始

luaL_newstate 创建状态,失败时返回空指针。成功后由宿主负责最终 lua_close。不要让多个对象都以为自己拥有同一状态,也不要在调用栈上仍有 Lua C 函数运行时关闭它。

#include <stdio.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int run_script(const char *source) {
    lua_State *L = luaL_newstate();
    if (L == NULL) return 1;

    luaL_openlibs(L);
    int status = luaL_loadstring(L, source);
    if (status == LUA_OK) {
        status = lua_pcall(L, 0, 0, 0);
    }

    if (status != LUA_OK) {
        const char *message = lua_tostring(L, -1);
        fprintf(stderr, "lua: %s\n", message ? message : "unknown error");
        lua_pop(L, 1);
    }

    lua_close(L);
    return status == LUA_OK ? 0 : 1;
}

这个版本适合可信本地脚本。对插件或配置,直接 luaL_openlibs 权限过大;应只打开需要的库,并移除文件、进程、动态加载和调试能力。沙箱是完整威胁模型,不是删除一个全局变量。

把栈效果写进函数合同

C API 用正索引从栈底定位,负索引从栈顶定位。每个辅助函数都应记录进入时需要几个值、成功推出几个结果、失败是否留下错误。保存 int top = lua_gettop(L),在返回前检查或恢复到预期高度,是发现泄漏的简单方法。

读取参数时先用 lua_typeluaL_check* 验证;从 Lua 调用的 C 函数通过返回值告诉 Lua 推出了多少个结果。宿主调用 Lua 函数时,先压函数再压参数,lua_pcall 的参数数量和期望结果必须与之匹配。不要把栈索引长期保存到可能 push、pop 或调用 Lua 的代码之外。

所有脚本错误都停在保护边界

lua_call 遇到错误会长跳转;宿主入口通常应使用 lua_pcall,把错误对象留在栈上并返回状态码。加载和执行是两个阶段:luaL_loadfileluaL_loadbuffer 只编译 chunk,成功后仍需调用。分别记录语法错误、运行错误、内存错误和消息处理错误。

错误对象不保证是字符串,日志要安全转换并限制长度。若需要 traceback,可提供 message handler,但它本身也必须遵守栈合同。不要把敏感宿主路径、密钥或整份用户脚本直接写进日志。

宿主指针需要生命周期协议

通过 userdata 暴露宿主对象时,Lua 保存的不是自动安全引用。定义对象由 Lua 拥有还是宿主借出;用 metatable 检查类型;在 __gc 或显式 close 中保证释放至多一次。宿主先销毁借出对象时,要让 userdata 进入不可用状态,后续方法返回错误,而不是解引用悬空指针。

Lua 5.4 的 to-be-closed 变量可以帮助脚本表达作用域清理,但宿主仍应处理脚本错误、取消和整个状态关闭。C++ 封装还要注意 Lua 的错误机制与 C++ 析构边界,不能假设长跳转会按普通异常展开所有对象。

限制时间和内存

不可信脚本可以无限循环或分配内存。自定义 allocator 能计量和拒绝超额分配;debug hook 可实现指令预算或取消检查。两者要在压力测试中验证,因为回调也处于资源不足路径。文件系统、网络和系统命令只通过白名单宿主函数暴露,并验证每个参数。

测试最坏路径

测试语法错误、运行错误、非字符串错误、栈溢出趋势、内存拒绝、超时、重复关闭和宿主对象先销毁。使用地址与未定义行为检测器构建 C 层,并在每个调用后断言栈高度。模糊测试适合覆盖 Lua 到 C 的参数边界,但测试状态必须每次独立,避免一次脚本污染后续结果。

结论

Lua 的 C API 很小,却要求严格纪律。让 lua_State 有单一所有者,为每个函数写栈合同,以 lua_pcall 隔离脚本错误,只开放必要能力,并给 userdata、时间和内存明确上限。嵌入层越窄,Lua 就越像可控扩展语言,而不是进入宿主进程的任意代码通道。