【C++/Lua联合开发】 (三) C++调用Lua
上篇讨论了 C 函数如何被 Lua 调用,反过来呢?C++ 程序如何读取 Lua 脚本里的变量、调用 Lua 函数、处理返回值?
答案仍然是虚拟栈——只是方向反了:C++ 主动向栈上压参数、主动从栈上取结果。本文用循序渐进的方式,把这套操作讲清楚。
文章目录
C++ 调用 Lua:从入门到实践
1. 核心认知:C++ 是"调用方",Lua 是"被调用方"
跟前一篇(C 函数被 Lua 调用)的区别在于角色互换:
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_setfield 和 lua_setglobal 都弹出栈顶值(消费型);lua_getfield 和 lua_getglobal 都压入新值(生产型)。这一推一弹,构成了 C API 最基本的操作节奏。
6. 调用 Lua 函数
这是实际项目中最常用的场景。核心 API 是 lua_pcall:
int lua_pcall(lua_State *L, int nargs, int nresults, int errfunc);
调用前栈布局必须是:[function, arg1, arg2, ..., argN](栈顶)。
两个容易踩的坑:
lua_State不是线程安全的,多线程场景须通过消息队列串行化。- 每次
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 |
五个核心原则:
- 栈是唯一的数据通道。C 与 Lua 之间一切数据交换都经过虚拟栈。掌握"谁压入、谁弹出"是基本功。
- 每次压栈都要有对应的清理。成功和失败分支都要做
lua_pop。残留值会导致后续调用拿到错误数据——这是最难排查的 bug 类型。 - 先检查类型,再读取值。用
lua_isfunction、lua_isstring做防御,避免 Lua 侧数据类型不匹配导致难以追踪的运行时错误。 lua_pcall是函数调用的唯一正道。它提供保护模式,出错不崩宿主程序;配合errfunc可构建健壮的错误处理链路。- 多线程串行化。每个
lua_State只能在一个线程中使用。跨线程请通过消息队列 dispatch。
更多推荐
所有评论(0)