第 17 章 C API 与嵌入式 Lua
第 17 章 C API 与嵌入式 Lua
Lua 最重要的用途之一是嵌入宿主程序。C 程序可以创建 Lua 状态机、执行脚本、注册函数并读写栈上的值。Lua 负责灵活的业务逻辑,C 负责高性能或需要系统权限的部分。
嵌入场景
常见场景包括:
-
游戏中的关卡脚本、AI 和配置规则。
-
网络设备中的策略与运维脚本。
-
编辑器插件和扩展系统。
-
服务端动态规则与工作流。
-
需要热更新而不重新编译宿主程序的模块。
嵌入前应明确边界:哪些 API 暴露给 Lua、哪些操作需要授权、脚本出错时宿主如何恢复。
状态机与栈
每个独立的 Lua 执行环境由 lua_State 表示:
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
int main(void) {
lua_State *L = luaL_newstate();
if (L == NULL) {
return 1;
}
luaL_openlibs(L);
lua_pushstring(L, "hello from C");
const char *message = lua_tostring(L, -1);
printf("%s\n", message);
lua_pop(L, 1);
lua_close(L);
return 0;
}
Lua 的 C API 使用虚拟栈交换值。向 Lua 传值时压栈,从 Lua 取值时通过索引读取。正索引从栈底计数,负索引从栈顶计数;-1 始终表示栈顶。
栈规则
-
调用 C 函数前,参数位于栈上。
-
C 函数返回时,返回值必须放在栈顶。
-
C 函数返回的整数表示返回值数量。
-
栈空间有限,循环压栈后应恢复平衡。
static int add(lua_State *L) {
lua_Number left = luaL_checknumber(L, 1);
lua_Number right = luaL_checknumber(L, 2);
lua_pushnumber(L, left + right);
return 1;
}
示例没有在前两步平衡栈,因为宿主会清理本次 C 调用参数。若函数内部临时压入很多值,应使用 lua_settop 恢复到预期位置。
调用 Lua 与注册 C 函数
注册 C 函数
lua_pushcfunction(L, add);
lua_setglobal(L, "c_add");
对应 Lua:
print(c_add(2, 3))
多个函数可以放在表中:
static const luaL_Reg math_extensions[] = {
{"add", add},
{NULL, NULL}
};
luaL_newlib(L, math_extensions);
lua_setglobal(L, "math_ext");
执行 Lua 代码
int run_script(lua_State *L, const char *source, const char *name) {
int status = luaL_loadbuffer(L, source, strlen(source), name);
if (status == LUA_OK) {
status = lua_pcall(L, 0, LUA_MULTRET, 0);
}
if (status != LUA_OK) {
const char *message = lua_tostring(L, -1);
fprintf(stderr, "Lua error: %s\n", message);
lua_pop(L, 1);
return 0;
}
return 1;
}
luaL_loadbuffer 只编译代码,不执行;lua_pcall 执行受保护的调用。错误信息可能不是字符串,读取前应处理 lua_type。
宿主函数返回错误
static int fail(lua_State *L) {
return luaL_error(L, "operation failed");
}
luaL_error 不会返回;它会通过 Lua 的错误机制向调用者传播信息。不要在被 Lua 调用的 C 函数中直接 longjmp,除非完全理解 Lua 的错误传播机制。
用户数据与资源生命周期
userdata 可以保存无法直接映射为 Lua 基础类型的数据:
typedef struct {
FILE *file;
} file_handle;
static int close_file(lua_State *L) {
file_handle *handle = (file_handle *)luaL_checkudata(
L, 1, "file_handle"
);
if (handle->file != NULL) {
fclose(handle->file);
handle->file = NULL;
}
return 0;
}
元方法 __gc 可以提供基本清理,但不能依赖它及时释放资源。显式关闭函数或 Lua 5.4 的 __close 更适合处理生命周期。
__close
Lua 5.4 的 <close> 变量会调用元方法 __close:
static int close_handle(lua_State *L) {
return close_file(L);
}
/* 在设置 userdata 元表时注册 __close */
Lua 侧:
do
local handle <close> = open_handle("data.bin")
use_handle(handle)
end
清理函数应能安全处理已经关闭的资源。关闭失败时,应该保留原始错误并添加清理上下文。
安全边界
脚本沙箱不是简单调用一个“禁用函数”的开关。需要同时限制:
-
文件、网络、进程和动态库访问。
-
CPU 时间、内存和栈空间。
-
可加载模块和 C 扩展。
-
宿主 API 的权限与调用者身份。
-
调试库和错误消息中的敏感信息。
资源限制
宿主可以设置钩子检测长时间运行:
lua_sethook(L, instruction_hook, LUA_MASKCOUNT, 100000);
钩子只能检测,不能自动恢复任意 C 扩展中发生的阻塞。资源隔离通常需要操作系统进程、容器或专用运行时配合。
栈空间与类型边界
-
用
luaL_check*验证参数,并让它产生可读错误。 -
不要把未验证的指针写入 Lua 可达对象。
-
固定大小的缓冲区使用
luaL_Buffer或大小检查。 -
跨 C 调用保存 Lua 引用时使用注册表或引用 API。
-
不要在错误的
lua_State上操作值。