歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> Lua 5.2 如何實現 C 調用中的Continuation

Lua 5.2 如何實現 C 調用中的Continuation

日期:2017/2/27 15:48:53   编辑:Linux教程

Lua 5.2 最重大的改進,莫過於 "yieldable pcall and metamethods" 。這需要克服一個難題:如何在 C 函數調用中,正確的 yield 回 resume 調用的位置。

resume 的發起總是通過一次 lua_resume 的調用,在 Lua 5.1 以前,yield 的調用必定結束於一次 lua_yield 調用,而調用它的 C 函數必須立刻返回。中間不能有任何 C 函數執行到中途的狀態。這樣,Lua VM 才能正常工作。

(C)lua_resume -> Lua functions -> coroutine.yield
   -> (C)lua_yield -> (C) return

在這個流程中,無論 Lua functions 有多少層,都被 lua state 中的 lua stack 管理。所以當最後 C return 返回到最初 resume 點 ,都不存在什麼問題,可以讓下一次 resume 正確繼續。也就是說,在 yield 時,lua stack 上可以有沒有執行完的 lua 函數,但不可以有沒有執行完的 C 函數。

如果我們寫了這麼一個 C 擴展,在 C function 裡回調了傳入的一個 Lua 函數。情況就變得不一樣了。

(C)lua_resume -> Lua function -> C function 
  -> (C) lua_call  -> Lua function 
  -> coroutine.yield -> (C)lua_yield 

C 通過 lua_call 調用的 Lua 函數中再調用 coroutine.yield 會導致在 yield 之後,再次 resume 時,不再可能從 lua_call 的下一行繼續運行。lua 在遇到這種情況時,會拋出一個異常 "attempt to yield across metamethod/C-call boundary" 。

在 5.2 之前,有人試圖解決這個問題,去掉 coroutine 的這些限制。比如 Coco 這個項目。它用操作系統的協程來解決這個問題 (例如,在 Windows 上使用 Fiber )。即給每個 lua coroutine 真的附在一個 C 協程上,獨立一個 C 堆棧。

這樣的方案開銷較大,且依賴平台特性。到了 Lua 5.2 中,則換了一個更徹底的方案解決這個問題。


其實,需要解決的問題是在 C 和 Lua 的邊界時,如果在 yield 之後,resume 如何繼續運行 C 邊界之後的 C 代碼。

當只有一個 C 堆棧時,只能從調用深處跳出來(使用 longjmp),卻無法回到那個位置(因為一旦跳出,堆棧就被破壞)。Lua 5.2 想了一個巧妙的方法來解決這個問題。

C 進入 Lua 的邊界一共有四個 API :lua_call , lua_pcall , lua_resumelua_yield 。其中要解決的關鍵問題在於 call 一個 lua function 有兩條返回路徑。

lua function 的正常返回應該執行 lua_call 調用後面的 C 代碼,而中途如果 yield 發生,回導致執行序回到前面 lua_resume 調用處的下一行 C 代碼執行。對於後一種,在後續的某次 lua_resume 發生後,lua coroutine 結束,還需要回到 lua_call 之後完成後續的 C 執行邏輯。C 語言是不允許這樣做的,因為當初的 C 堆棧已經不存在了。

Lua 5.2 提供了新的 API :lua_callk 來解決這個問題。既然無法在 yield 之後,C 的執行序無法回到 lua_callk 的下一行代碼,那麼就讓 C 語言使用者自己提供一個 Continuation 函數 k 來繼續。

我們可以這樣理解 k 這個參數:當 lua_callk 調用的 lua 函數中沒有發生 yield 時,它會正常返回。一旦發生 yield ,調用者要明白,C 代碼無法正常延續,而 lua vm 會在需要延續時調用 k 來完成後續工作。

k 會得到正確的 L 保持正確的 lua state 狀態,看起來就好像用一個新的 C 執行序替代掉原來的 C 執行序一樣。

典型的用法就是在一個 C 函數調用的最後使用 callk :

lua_callk(L, 0, LUA_MULTRET, 0, k);
return k(L);

也就是把 callk 後面的執行邏輯放在一個獨立 C 函數 k 中,分別在 callk 後調用它,或是傳遞給框架,讓框架在 resume 後調用。

這裡,lua 狀態機的狀態被正確保存在 L 中,而 C 函數堆棧會在 yield 後被破壞掉。如果我們需要在 k 中得到延續點前的 C 函數狀態怎麼辦呢?lua 提供了 ctx 用於輔助記錄 C 中的狀態。

在 k 中,可以通過 lua_getctx 獲得最近一次邊界調用時傳入的 k 。lua_getctx 返回兩個參數,分別是 k 和當前所處的執行位置。是原始函數(沒有被 yield 打斷的),還是在被 yield 打斷後的延續點函數中。這有一點點像 setjmp 或 fork 的接口設計。


其實在 Lua 5.2 的官方文檔中,對以上已經做了詳盡的說明。可以看 4.7 節 Handling Yields in C 。

或許,我們還可以參考這個接口設計,實現一個不需要獨立堆棧的 C 版 Coroutine 庫。只是用起來會很麻煩吧。

Copyright © Linux教程網 All Rights Reserved