1. 引言
在游戏开发与嵌入式脚本领域,Lua 凭借轻量、高效、可嵌入的特性成为最受欢迎的脚本语言之一。而 C++ 作为高性能底层语言,与 Lua 的结合早已是业界标配。当业务逻辑越来越复杂,异步任务、状态机、迭代器生成等场景不断涌现时,协程(Coroutine)便成为连接 C++ 与 Lua 的一座重要桥梁。
本文将深入探讨 C++ 与 Lua 的协程交互机制,从 Lua 协程的基本原理讲起,逐步过渡到 C++ 侧如何创建、恢复、挂起协程,并给出完整的可运行示例,帮助读者在实际项目中游刃有余地运用这一技术。
2. Lua 协程基础
2.1 什么是协程
协程是一种用户态轻量级线程,它由程序员显式控制挂起(yield)与恢复(resume),不依赖操作系统内核调度。与线程不同,协程在同一时刻只有一个在执行,因此无需考虑数据竞争与锁问题。
Lua 从 5.1 版本开始原生支持协程,核心 API 包括:
coroutine.create(f):创建一个协程,参数为协程主体函数。coroutine.resume(co, ...):恢复(或首次启动)协程,可传入参数。coroutine.yield(...):挂起当前协程,可向外传递返回值。coroutine.status(co):查询协程状态(suspended / running / normal / dead)。coroutine.wrap(f):以函数形式封装协程,更简洁。
2.2 一个简单的 Lua 协程示例
lua
local co = coroutine.create(function(a, b)
print("协程启动,参数:", a, b)
local x = coroutine.yield(a + b)
print("协程恢复,收到:", x)
return "done"
end)
print(coroutine.resume(co, 10, 20)) -- 输出:协程启动,参数:10 20 / true 30
print(coroutine.resume(co, 99)) -- 输出:协程恢复,收到:99 / true done
从这个例子可以看到,resume 首次调用时传入的参数会作为协程函数的入参;协程内部 yield 挂起时,其参数作为 resume 的返回值返回给调用者;再次 resume 时传入的参数则作为 yield 的返回值回到协程内部。
3. C++ 侧操作 Lua 协程
3.1 环境准备
本文示例基于 Lua 5.4,使用官方 C API。请确保开发环境中已正确安装 Lua 开发库(如 Linux 下 liblua5.4-dev,Windows 下可从官网下载源码编译)。
3.2 创建与恢复协程
在 C++ 中,我们通过 lua_newthread 创建协程,它返回一个新的线程(即协程)引用,并压入栈顶。随后用 lua_resume 恢复执行。
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)
local b = coroutine.yield(a * 2)
return a + b
end)
return co
)";
luaL_dostring(L, script);
// 栈顶即为协程对象
lua_State* co = lua_tothread(L, -1);
// 首次恢复,传入参数 10
lua_pushinteger(L, 10);
int status = lua_resume(co, L, 1);
std::cout << "resume status: " << status << std::endl; // LUA_OK(0)
// 读取 yield 返回值(20)
int first = lua_tointeger(co, -1);
std::cout << "yield value: " << first << std::endl; // 20
// 再次恢复,传入参数 5
lua_pushinteger(L, 5);
status = lua_resume(co, L, 1);
std::cout << "resume status: " << status << std::endl; // LUA_OK(0)
// 读取最终返回值(15)
int result = lua_tointeger(co, -1);
std::cout << "final result: " << result << std::endl; // 15
lua_close(L);
return 0;
}
3.3 错误处理
当协程执行出错时,lua_resume 返回 LUA_ERRRUN,错误信息保存在协程栈顶。务必在每次 resume 后检查返回值:
cpp
if (status != LUA_OK) {
const char* err = lua_tostring(co, -1);
std::cerr << "协程错误: " << err << std::endl;
lua_pop(co, 1);
}
4. C++ 与 Lua 协程的双向数据传递
4.1 从 C++ 向协程传参
lua_resume 的第三个参数表示传入参数的个数,参数依次压入调用者的栈(即主线程 L)中。协程内部通过 ... 或具名参数接收。
4.2 从协程向 C++ 返回值
协程 yield 或 return 的值会留在协程自己的栈顶,C++ 侧通过 lua_tointeger、lua_tostring 等函数读取。
4.3 完整示例:C++ 驱动的生成器
下面实现一个经典的斐波那契生成器,C++ 每次调用 resume 获取下一个值:
cpp
#include <lua.hpp>
#include <iostream>
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
const char* script = R"(
return coroutine.wrap(function()
local a, b = 0, 1
while true do
coroutine.yield(a)
a, b = b, a + b
end
end)
)";
luaL_dostring(L, script);
// 获取 wrap 返回的函数
lua_State* co = lua_tothread(L, -1);
for (int i = 0; i < 10; ++i) {
lua_pushinteger(L, 0); // wrap 函数需要至少一个参数
if (lua_resume(co, L, 1) != LUA_OK) {
std::cerr << "协程错误: " << lua_tostring(co, -1) << std::endl;
break;
}
std::cout << lua_tointeger(co, -1) << " ";
}
std::cout << std::endl;
lua_close(L);
return 0;
}
5. 进阶:C++ 函数中挂起协程
5.1 问题场景
有时我们希望 C++ 注册给 Lua 的函数内部也能触发协程挂起,例如等待一个异步 IO 完成后再恢复。Lua 5.4 提供了 lua_yieldk,允许 C 函数主动挂起当前协程。
5.2 使用 lua_yieldk
cpp
// 挂起后再次恢复时的回调
int after_yield(lua_State* L, int status, lua_KContext ctx) {
lua_pushinteger(L, 42); // 恢复后返回给 Lua 的值
return 1;
}
// 被 Lua 调用的 C 函数
int c_wait(lua_State* L) {
// 挂起当前协程,恢复时执行 after_yield
return lua_yieldk(L, 0, 0, after_yield);
}
注册该函数后,Lua 侧调用 c_wait() 时协程会挂起,C++ 主程序在合适的时机(如异步回调完成)再次 resume,协程将从 after_yield 继续执行。
6. 常见陷阱与最佳实践
6.1 栈空间管理
每个协程默认拥有独立的栈空间(Lua 5.4 默认 20KB 左右)。创建大量协程时需注意内存占用,可通过 lua_gc 或调整 LUAI_MAXCSTACK 控制。
6.2 不要在协程间共享非线程安全资源
虽然协程是单线程模型,但多个协程共享同一个全局状态时仍需注意逻辑上的竞态,尤其是涉及 C 侧静态变量时。
6.3 区分主线程与协程
lua_resume 的第一个参数是协程自身,第二个参数是调用者上下文。在协程内部调用 lua_push* 时,操作的是协程自己的栈,而非主线程栈,切勿混淆。
6.4 错误传播
协程内的错误不会自动传播到主线程,必须显式检查 lua_resume 返回值并读取错误信息。建议封装一个 RAII 风格的协程管理类,统一处理创建、恢复与错误清理。
7. 总结
C++ 与 Lua 的协程交互,本质上是围绕 lua_newthread、lua_resume、lua_yieldk 三个核心 API 展开的。理解协程的挂起与恢复模型,掌握数据在 C++ 与 Lua 栈之间的传递方式,就能在游戏逻辑、异步任务、数据流处理等场景中充分发挥两者的优势。
建议读者动手运行本文示例,并尝试将协程封装为 C++ 类,逐步构建属于自己的脚本协程管理框架。