返回首页

教程导航

返回首页

🏷️ 标签(1)

🗂️ 分类(1)

第 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 上操作值。

上一章:标准库精讲 | 下一章:任务管理 CLI 实战