1. 引言
在游戏开发和脚本化系统中,C++ 与 Lua 的组合非常常见。C++ 负责高性能的核心逻辑,Lua 负责灵活的业务脚本。而协程(Coroutine)作为 Lua 的重要特性,常被用来实现异步流程、状态机、剧情脚本等。本文将从原理出发,结合代码示例,讲解如何在 C++ 中创建、驱动和销毁 Lua 协程,并实现双向交互。
2. Lua 协程基础
Lua 的协程由 coroutine 库提供,核心函数包括 coroutine.create、coroutine.resume、coroutine.yield 和 coroutine.status。协程的本质是一个独立的执行栈,可以在挂起点暂停,并在之后从挂起点继续执行。
一个典型的 Lua 协程示例:
lua
local co = coroutine.create(function(a, b)
local sum = a + b
local c = coroutine.yield(sum) -- 第一次挂起,返回 sum
return c * 2
end)
local ok, result = coroutine.resume(co, 3, 4)
print(result) -- 输出 7
ok, result = coroutine.resume(co, 10)
print(result) -- 输出 20
第一次 resume 传入参数 3 和 4,协程执行到 yield 挂起,并把 sum 的值 7 返回给调用方。第二次 resume 传入 10,作为 yield 的返回值赋给变量 c,协程继续执行并返回最终结果 20。
3. C++ 侧创建与驱动协程
在 C++ 中操作 Lua 协程,核心是使用 Lua C API 中的 lua_newthread 创建协程线程,然后通过 lua_resume 和 lua_yield 驱动执行。下面给出一个完整的 C++ 示例,演示如何创建协程、传入参数并获取返回值。
cpp
#include <lua.hpp>
#include <iostream>
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
// 加载协程脚本
const char* script = R"(
local co = coroutine.create(function(a, b)
local sum = a + b
local c = coroutine.yield(sum)
return c * 2
end)
return co
)";
luaL_loadstring(L, script);
lua_pcall(L, 0, 1, 0);
// 栈顶是协程对象
lua_State* co = lua_tothread(L, -1);
// 第一次 resume:传入参数 3 和 4
lua_pushinteger(co, 3);
lua_pushinteger(co, 4);
int status = lua_resume(co, L, 2);
if (status == LUA_OK) {
// 协程挂起,栈顶是 yield 返回的值
int sum = lua_tointeger(co, -1);
std::cout << "第一次 resume 结果: " << sum << std::endl;
}
// 第二次 resume:传入参数 10
lua_pushinteger(co, 10);
status = lua_resume(co, L, 1);
if (status == LUA_OK) {
int result = lua_tointeger(co, -1);
std::cout << "第二次 resume 结果: " << result << std::endl;
}
lua_close(L);
return 0;
}
这里的关键点在于:lua_newthread 创建的协程拥有独立的栈,但共享全局状态。每次 lua_resume 前,需要把参数压入协程自己的栈;lua_resume 返回后,结果同样位于协程栈顶。
4. 从 C++ 向协程传递数据
除了在 resume 时传参,C++ 还可以在协程挂起期间,通过 lua_yield 的返回值向协程内部传递数据。这需要 C++ 侧维护协程的挂起状态,并在合适的时机再次 resume。
下面演示一个更完整的交互模式:C++ 驱动一个 Lua 协程,协程每次挂起时向 C++ 请求一个数值,C++ 计算后回传。
cpp
#include <lua.hpp>
#include <iostream>
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
const char* script = R"(
local co = coroutine.create(function()
local a = coroutine.yield("request_a")
local b = coroutine.yield("request_b")
return a + b
end)
return co
)";
luaL_loadstring(L, script);
lua_pcall(L, 0, 1, 0);
lua_State* co = lua_tothread(L, -1);
// 第一次 resume:启动协程
int status = lua_resume(co, L, 0);
if (status == LUA_OK) {
const char* req = lua_tostring(co, -1);
std::cout << "协程请求: " << req << std::endl;
// 回传 a = 10
lua_pushinteger(co, 10);
status = lua_resume(co, L, 1);
}
if (status == LUA_OK) {
const char* req = lua_tostring(co, -1);
std::cout << "协程请求: " << req << std::endl;
// 回传 b = 20
lua_pushinteger(co, 20);
status = lua_resume(co, L, 1);
}
if (status == LUA_OK) {
int result = lua_tointeger(co, -1);
std::cout << "最终结果: " << result << std::endl;
}
lua_close(L);
return 0;
}
这种模式非常适合实现「C++ 主导、Lua 协作」的异步流程,例如剧情对话、技能释放、AI 决策等场景。
5. 协程状态管理与错误处理
在 C++ 中管理 Lua 协程,需要关注协程的状态和错误处理。Lua 提供 lua_status 函数查询协程状态,LUA_YIELD 表示协程挂起,LUA_OK 表示正常结束。
当协程内部发生错误时,lua_resume 会返回错误码,错误信息位于协程栈顶。C++ 侧需要及时捕获并处理,避免错误扩散到整个 Lua 状态。
cpp
int status = lua_resume(co, L, 0);
if (status != LUA_OK && status != LUA_YIELD) {
const char* err = lua_tostring(co, -1);
std::cerr << "协程错误: " << err << std::endl;
// 清理协程或回滚状态
}
另外,协程结束后需要及时释放资源。虽然 Lua 的垃圾回收会自动回收协程对象,但在长时间运行的服务中,建议显式管理协程的生命周期,避免悬挂引用。
6. 实战:用协程实现剧情脚本
下面结合一个游戏剧情脚本的简化示例,展示 C++ 与 Lua 协程的完整交互流程。Lua 脚本定义剧情步骤,C++ 负责按顺序驱动。
lua
-- dialogue.lua
local dialogue = {}
function dialogue.start()
local co = coroutine.create(function()
say("你好,冒险者!")
wait(1.0)
say("前方有危险,请小心。")
wait(0.5)
say("准备好了吗?")
return "ready"
end)
return co
end
return dialogue
cpp
#include <lua.hpp>
#include <iostream>
void say(const char* text) {
std::cout << "[对话] " << text << std::endl;
}
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
// 注册 C++ 函数供 Lua 调用
lua_register(L, "say", [](lua_State* L) -> int {
const char* text = lua_tostring(L, 1);
say(text);
return 0;
});
// 注册 wait 函数(简化:直接返回)
lua_register(L, "wait", [](lua_State* L) -> int {
double seconds = lua_tonumber(L, 1);
std::cout << "[等待] " << seconds << " 秒" << std::endl;
return 0;
});
// 加载并执行 dialogue.lua
luaL_dofile(L, "dialogue.lua");
lua_getglobal(L, "dialogue");
lua_getfield(L, -1, "start");
lua_pcall(L, 0, 1, 0);
// 获取协程
lua_State* co = lua_tothread(L, -1);
// 驱动协程直到结束
int status = lua_resume(co, L, 0);
while (status == LUA_YIELD) {
status = lua_resume(co, L, 0);
}
if (status == LUA_OK) {
const char* result = lua_tostring(co, -1);
std::cout << "剧情结束,返回: " << result << std::endl;
}
lua_close(L);
return 0;
}
在这个示例中,Lua 协程通过 yield 挂起,C++ 侧循环 resume 驱动剧情推进。C++ 注册的 say 和 wait 函数在协程内部被调用,实现了双向交互。
7. 常见问题与注意事项
- 栈空间:每个协程拥有独立的栈,默认大小有限。递归或深层嵌套的协程可能耗尽栈空间,需要合理设计或调整栈大小。
- 错误隔离 :协程内的错误不会自动传播到主线程,但
lua_resume返回错误码后,协程已不可恢复,需要重新创建。 - 生命周期:长时间运行的协程要注意内存占用,及时结束并清理,避免协程泄漏。
- 线程安全 :Lua 状态默认不是线程安全的。多线程环境下,每个线程应使用独立的
lua_State,或通过锁保护共享状态。 - 版本差异 :Lua 5.1 与 5.3 的协程 API 略有差异,例如
lua_resume的参数个数和错误处理方式,需要根据实际版本调整。
8. 总结
C++ 与 Lua 的协程交互,核心在于理解 lua_newthread、lua_resume 和 lua_yield 的配合方式。通过协程,C++ 可以把复杂的异步流程交给 Lua 脚本编排,同时保持高性能的核心逻辑在 C++ 侧执行。这种模式在游戏开发、工具链和嵌入式脚本系统中都有广泛的应用价值。