上篇讨论了 C 函数如何被 Lua 调用,反过来呢?C++ 程序如何读取 Lua 脚本里的变量、调用 Lua 函数、处理返回值?

答案仍然是虚拟栈——只是方向反了:C++ 主动向栈上压参数、主动从栈上取结果。本文用循序渐进的方式,把这套操作讲清楚。



C++ 调用 Lua:从入门到实践

1. 核心认知:C++ 是"调用方",Lua 是"被调用方"

跟前一篇(C 函数被 Lua 调用)的区别在于角色互换:

Lua 脚本 虚拟栈 C++ 程序 Lua 脚本 虚拟栈 C++ 程序 C++ 调用 Lua 函数 [func, "hello", 42] ← 函数和参数就位 ["result"] ← 返回值留在栈顶 lua_getglobal(L, "lua_func") lua_pushstring(L, "hello") lua_pushinteger(L, 42) lua_pcall(L, 2, 1, 0) → 执行 lua_func("hello", 42) return "result" lua_tostring(L, -1) 读取结果 lua_pop(L, 1) 清理

2. 读取 Lua 全局变量

最简单的场景:Lua 侧定义了全局变量,C++ 侧读出来。

-- Lua 侧
width = 1920
height = 1080
// C++ 侧 —— 三步走:压入 → 读取 → 弹出
lua_getglobal(L, "width");        // 将 width 的值压入栈顶
int width = lua_tointeger(L, -1); // 读栈顶值(不弹出栈)
lua_pop(L, 1);                    // 清理,恢复栈平衡

lua_getglobal(L, "height");
int height = lua_tointeger(L, -1);
lua_pop(L, 1);

std::cout << "Resolution: " << width << "x" << height << std::endl;

关键lua_getglobal 生产(压入)、lua_tointeger 读取(不改栈)、lua_pop 消费(弹出)。三者缺一不可——忘记 lua_pop 会让栈无限增长。


3. 向 Lua 传递全局变量

反过来,C++ 可以在执行脚本前"预埋"全局变量。

// 在 luaL_loadfile / lua_pcall 之前设置
lua_pushstring(L, "hello from C++");  // 值压栈
lua_setglobal(L, "cname");            // 弹出栈顶,赋给 _G["cname"]
// 等价于 Lua: cname = "hello from C++"
-- Lua 脚本中直接访问
print(cname)  --> hello from C++

时机很关键:lua_setglobal 必须在脚本执行前调用,否则 Lua 侧看不到这个变量。


4. 读取 Lua Table

Table 是 Lua 唯一的数据结构,C++ 通过 lua_getfield 逐字段读取。

-- Lua 侧
config = {
    title  = "My Window",
    width  = 800,
    height = 600
}
// C++ 侧 —— 栈变化以 [正/负索引:值] 标注
lua_getglobal(L, "config");        // 栈: [..., -1:config_table]
lua_getfield(L, -1, "title");      // 栈: [..., -2:config_table, -1:"My Window"]
const char *title = lua_tostring(L, -1);
lua_pop(L, 1);                     // 栈: [..., -1:config_table]

lua_getfield(L, -1, "width");      // 栈: [..., -2:config_table, -1:800]
int width = lua_tointeger(L, -1);
lua_pop(L, 1);

lua_getfield(L, -1, "height");     // 栈: [..., -2:config_table, -1:600]
int height = lua_tointeger(L, -1);
lua_pop(L, 1);

lua_pop(L, 1);                     // ★ 别忘了弹出 config_table 本身
// 栈恢复: [...]

lua_getfield(L, idx, key) 等价于把 table[key] 的结果压栈。操作完后逐层 lua_pop


5. 向 Lua 传递 Table

三步:newtable → setfield → setglobal

lua_newtable(L);                    // 栈: [..., -1:table]
lua_pushstring(L, "skan");          // 栈: [..., -2:table, -1:"skan"]
lua_setfield(L, -2, "name");        // table["name"] = "skan"(弹出了 "skan")
                                    // 栈: [..., -1:table]

lua_pushinteger(L, 42);
lua_setfield(L, -2, "age");         // table["age"] = 42
                                    // 栈: [..., -1:table]

lua_setglobal(L, "person");         // _G["person"] = table(弹出了 table)
                                    // 栈: [...]
-- Lua 侧直接使用
print(person.name)  --> skan
print(person.age)   --> 42

记法lua_setfieldlua_setglobal弹出栈顶值(消费型);lua_getfieldlua_getglobal压入新值(生产型)。这一推一弹,构成了 C API 最基本的操作节奏。


6. 调用 Lua 函数

这是实际项目中最常用的场景。核心 API 是 lua_pcall

int lua_pcall(lua_State *L, int nargs, int nresults, int errfunc);

调用前栈布局必须是:[function, arg1, arg2, ..., argN](栈顶)。

两个容易踩的坑:

  1. lua_State 不是线程安全的,多线程场景须通过消息队列串行化。
  2. 每次 lua_pcall 都会弹出函数和参数,调用后务必清理返回值或错误信息。

6.1 无参无返回值

-- Lua 侧
function on_event()
    print("Event triggered")
end
lua_getglobal(L, "on_event");
if (!lua_isfunction(L, -1)) {      // 防御:检查是否为函数
    lua_pop(L, 1);
    return;
}

int ret = lua_pcall(L, 0, 0, 0);   // 0 参数,0 返回值
if (ret != LUA_OK) {
    const char *err = lua_tostring(L, -1);
    printf("Error: %s\n", err);
    lua_pop(L, 1);                 // 弹出错误信息
}
// 成功时栈已空,不需要额外清理

lua_pcall 行为速查

场景 调用前栈 调用后栈 需要手动 pop?
成功,nresults=0 [-1:func] []
成功,nresults=1 [-1:func] [-1:ret_val] 是,弹出返回值
失败 [-1:func] [-1:error_msg] 是,弹出错误信息

无论成功与否,lua_pcall弹出函数和所有参数。失败时额外压入一条错误信息。

6.2 传参与接收返回值

-- Lua 侧
function process(e1, tb2)
    print("Got:", e1)
    print("Name:", tb2.name)
    return {result = "ok"}
end
lua_getglobal(L, "process");
if (!lua_isfunction(L, -1)) { lua_pop(L, 1); return; }

// 构造参数
lua_pushstring(L, "data_from_cpp");  // 参数1: 字符串
lua_newtable(L);                     // 参数2: table
lua_pushstring(L, "alice");
lua_setfield(L, -2, "name");
// 栈: [..., -3:process, -2:"data_from_cpp", -1:{name="alice"}]

int ret = lua_pcall(L, 2, 1, 0);     // 2 个参数, 期望 1 个返回值
if (ret != LUA_OK) {
    const char *err = lua_tostring(L, -1);
    printf("Error: %s\n", err);
    lua_pop(L, 1);
    return;
}

// 解析返回的 table
lua_getfield(L, -1, "result");       // 栈: [..., -2:{result="ok"}, -1:"ok"]
if (lua_isstring(L, -1))
    printf("Result: %s\n", lua_tostring(L, -1));

lua_pop(L, 2);                       // 清理返回值 table 和 result 字段值

6.3 自定义错误处理函数

当 Lua 函数出错时,可以通过 errfunc 参数指定一个 Lua 函数来格式化错误信息。

-- Lua 侧
function error_handler(err)
    return "[C++ Error] " .. tostring(err)
end
// errfunc 的栈索引必须在压入函数和参数之前计算
int errfunc_idx = lua_gettop(L) + 1;    // 当前栈顶+1,即 error_handler 将占据的位置
lua_getglobal(L, "error_handler");       // 栈: [..., -1:error_handler]

lua_getglobal(L, "process");
lua_pushstring(L, "hello");
// 栈: [..., -3:error_handler, -2:process, -1:"hello"]

int ret = lua_pcall(L, 1, 1, errfunc_idx);
if (ret != LUA_OK) {
    // 栈顶是 error_handler 格式化后的错误字符串
    const char *err = lua_tostring(L, -1);
    printf("Handled error: %s\n", err);
    lua_pop(L, 1);                       // 弹出格式化后的错误信息
} else {
    printf("Success: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);                       // 弹出返回值
}
lua_pop(L, 1);                           // 弹出 error_handler

errfunc 的工作原理:当被调用函数出错时,lua_pcall 自动以错误信息为参数调用 errfunc,然后将 errfunc 的返回值压栈。C++ 侧拿到的永远是格式化后的错误字符串——适合做统一日志上报。


7. 总结

操作方向 生产型 API(压栈) 消费型 API(弹栈并赋值)
全局变量 lua_getglobal lua_setglobal
表字段 lua_getfield lua_setfield
通用 lua_push* 系列 lua_pop / lua_settop

五个核心原则:

  1. 栈是唯一的数据通道。C 与 Lua 之间一切数据交换都经过虚拟栈。掌握"谁压入、谁弹出"是基本功。
  2. 每次压栈都要有对应的清理。成功和失败分支都要做 lua_pop。残留值会导致后续调用拿到错误数据——这是最难排查的 bug 类型。
  3. 先检查类型,再读取值。用 lua_isfunctionlua_isstring 做防御,避免 Lua 侧数据类型不匹配导致难以追踪的运行时错误。
  4. lua_pcall 是函数调用的唯一正道。它提供保护模式,出错不崩宿主程序;配合 errfunc 可构建健壮的错误处理链路。
  5. 多线程串行化。每个 lua_State 只能在一个线程中使用。跨线程请通过消息队列 dispatch。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐