Lua 5.2 引入的「可 yield 的 pcall 與元方法」是里程碑式改進。核心挑戰在於:**當 C 函數呼叫 Lua 函數,而 Lua 函數中途 yield 時,C 堆疊幀已銷毀,如何在後續 resume 時繼續執行 C 函數剩餘邏輯?** 本文深入剖析 Lua 5.2 引入的 lua_callk / lua_pcallk 機制與 延續傳遞風格(CPS) 解決方案。
(C) lua_resume → Lua 函數 → coroutine.yield
→ (C) lua_yield → (C) return
"attempt to yield across metamethod/C-call boundary"。
(C) lua_resume → Lua 函數 → C 函數
→ (C) lua_call → Lua 函數
→ coroutine.yield → (C) lua_yield
此時 C 函數位於 lua_call 之後,若 yield,lua_call 無法正常返回,C 堆疊幀隨長跳躍銷毀,後續 resume 無法回到原 C 程式碼繼續執行。
void lua_callk(lua_State *L, int nargs, int nresults, lua_KContext ctx, lua_KFunction k);
k:延續函數指標,簽名 int (*k)(lua_State *L, int status, lua_KContext ctx)。ctx:使用者自定義上下文(輕量整數/指標),用於攜帶 C 局部狀態。k 不被調用。resume 時自動調用 k 繼續執行。
// C 函數尾端調用 Lua 回調
int my_c_function(lua_State *L) {
// ... 準備參數 ...
lua_callk(L, 0, LUA_MULTRET, 0, continuation);
return continuation(L); // 正常路徑也執行 continuation 以統一邏輯
}
// 延續函數:處理 yield 後的恢復邏輯
int continuation(lua_State *L) {
// 此處可取得 Lua 回傳值,繼續 C 邏輯
// 透過 lua_getctx(L, &ctx) 判斷是首次進入還是 resume 後進入
return 0; // 回傳值數量
}
lua_KContext ctx;
int status = lua_getctx(L, &ctx);
// status == 0 : 正常路徑(未被 yield 打斷)
// status != 0 : 延續路徑(resume 後進入),status 為 yield 狀態碼
類似 setjmp/fork 的雙重返回語意,讓同一函數處理兩種流程。
lua_State)完整保存 Lua 棧、執行指標、上值等。ctx(通常傳遞結構體指標)或 Lua 登錄表手動保存。Lua 5.2 將 lua_call、lua_pcall、lua_resume、lua_yield 全部提供 k 版本:
lua_callk / lua_pcallk:C → Lua 呼叫邊界。lua_yieldk:Lua → C yield 邊界(C 函數主動 yield)。lua_resume 內部隱式處理延續恢復。Lua 5.1 時代的 Coco 使用作業系統協程(Windows Fiber、POSIX ucontext)為每個 Lua 協程分配獨立 C 堆疊。優點:透明支援任意 C yield;缺點:平台相依、記憶體開銷大(每協程 ~64KB+ 堆疊)、移植困難。Lua 5.2 方案**零平台相依、零額外堆疊**,純用戶態實現。
lua_callk/lua_pcallk。ctx 傳遞輕量狀態(索引、旗標、小結構指標),大型狀態存 Lua 登錄表。lua_getctx 區分首次/恢復兩條路徑,避免重複初始化。Lua 5.2 透過 延續傳遞風格(CPS) 將「C 堆疊無法保存」的硬性限制,轉化為「由程式設計師顯式編寫延續函數」的工程任務。雖增加少許編碼負擔,但換來的是**純標準 C 實現、零平台相依、極低記憶體開銷**的協程完整支援,是系統級嵌入式腳本語言的典範設計。