Handling Yields in C 是 Lua 5.2 的重大改进之一,最早从 blog《Lua 5.2 如何实现 C 调用中的 Continuation》了解到。这些资料围绕新 API lua_yieldk
,lua_callk
,和 lua_pcallk
来介绍这个新特性,自然有很多关于新增加的 continuation 参数的讨论。其实以 continuation 参数作为切入点介绍 yields-in-C 容易混淆问题的实质。首先回顾一下《Programming in Lua, 2ed》(中文版) 中的一段话 (第 30.1 章):
The only way for a C function to yield is when returning, so that it actually does not suspend itself, but its caller — which should be a Lua function.
这段话针对 Lua 5.1 而写,当时尚无 continuation 参数。严格地说这会误导读者。根据描述本身,可以理解为 Lua 无法在 C 代码中 yield (包括被 C 代码再次调用的第二层 Lua 代码以及之后的 stack 上更深的执行层次) 是因为无法纪录这种情况下 resume 所需的信息 —— C 代码的 stack 和 program counter。这种解释的推论是,在 C 代码即将返回 Lua 前,由于 C stack 已经恢复为调用前的状态 (可以称为「空 stack」),program counter 也处于即将进入 Lua 代码的状态,所以可以调用 lua_yield
。原理上这个结论可以推广到 lua_call
/lua_pcall
。如果程序在 Lua 和 C 代码之间调用切换多次,整个 virtual stack 上的 Lua 部分的 stack 会被 C 代码分割成若干段。不过只要这三个 API 总是在 C 代码即将返回 Lua 前被调用,那么这些 C stack 都是空 stack,Lua VM 只需知道 C 代码在 Lua stack 段间的位置,不需要实际纪录 C stack/program counter 本身的内容。「在多于一层 C/Lua 切换的情形下 yield」应该正常工作。
问题是 Lua 5.1 不支持「在多于一层 C/Lua 切换的情形下 yield」!
根据上面的分析,这个限制并非 Lua 语言或 C API 本身的设计所固有,它是一个纯粹的 VM 实现问题。也就是说,即便 Lua 在 5.1 之后不引入 continuation 参数,保留「lua_yield
(以及 lua_call
/lua_pcall
) 只能在即将返回到 Lua 之前调用」这个限制,也还是可以支持从 C 或者从第二层及以上的 Lua 代码中 yield。
Lua 5.2 实现了「在多于一层 C/Lua 切换的情形下 yield」,这是一个 VM 内部改进,仅仅为此并无必要引入 continuation 参数。 Continuation 参数解决的是另一个问题 ——「Lua 无法跟踪程序在 C 代码中的 stack 和 program counter」,但仍保留诸多限制:首先,它无法解决纪录 C stack 的问题,所以,仍然不允许在 C stack 上有多于一层 C 函数时调用新 API;其次,它也无法纪录 program counter,编写 C 代码时必须手工把可能发生 yield 之后的 C 代码 factor 到一个单独的 C 函数中,通过函数分割这种变通方式部分的模拟 yield 时的 program counter。由于没有真正的管理 C stack,充当 continuation 参数的 C 函数在运行中不能依赖 caller 的 C stack (实际上这个问题不大,因为它只能接受一个 lua_State
结构)。最后,仿照某些评测给 Lua 5.2 的新特性做一个「优雅 / 有待改进 / 丑陋」的总结:
优雅
实现了「在多于一层 C/Lua 切换的情形下 yield」。对于「Lua 无法跟踪程序在 C 代码中的 stack 和 program counter」这个问题的剪裁得当,既扩大了支持的应用场景,放松了对 C 代码的限制。同时避免了编程接口过分复杂化,和使用底层 C runtime 机制破坏 VM 的跨平台性。
有待改进
文档没有分别说明两个问题,混淆了 VM 内部实现的改进和 API 改变的原因。
丑陋
新 API 在 continuation 参数为 NULL
时沿袭旧 API 的限制 —— 禁止在多于一层 C/Lua 切换的情形下 yield。这是不必要的,也是混淆两个独立问题的误解最大的来源。现在,对于那些已经在「即将返回 Lua 之前」被调用的 lua_yieldk
/lua_callk
/lua_pcalk
,也必须传入一个 no-op 的 continuation 函数。不过,Lua 5.2 的发布已经有段时日,估计这个 API 上的小问题也不会再未来更改了。
发表评论