改進的協程補丁 |
|
補丁如下:檔案:wiki_insecure/power_patches/5.0/ejcoro.patch
以下是自述檔案的簡略維基化版本...
-Eric Jacobs
此補丁使用與 Lua 在標準情況下(Lua 函數呼叫 Lua 函數)略有不同的策略來允許產生。在標準情況下,Lua 為使用普通函數呼叫呼叫的任意數量的 Lua 框架(CallInfo?)維護一組 C 堆疊框架。這些 C 堆疊框架中最底層的是直譯器的主要迴圈 luaV_execute()。從 Lua 到 Lua 的呼叫是在不重新進入 luaV_execute() 的情況下完成的;而是將新的 Lua 框架新增或移除堆疊,並執行「goto」以在適當的位置重新啟動 luaV_execute() C 框架。
/<+------------+ / | luaFunc3 | / | | / +------------+ / | luaFunc2 | / | CI_CALLING | / +------------+ +--------------+ 1:n | luaFunc1 | | luaV_execute | | CI_CALLING | +--------------+<======>+------------+ C stack Lua CallInfo's此圖說明了在 Lua 函數呼叫 Lua 函數的情況下,C 堆疊和 Lua CallInfo? 框架堆疊如何對齊。luaV_execute() 的單個 C 框架映射到任意數量的 Lua 框架。請注意,CI_CALLING 標誌設置在以此方式進行呼叫的函數中。
當元方法或 C 函數回調到標準 Lua 時,會重新進入 luaV_execute() 函數,並且禁止產生。
+------------+ | luaFunc2 | +--------------+ | | | luaV_execute |<======>| | ..........+--------------+........+------------+ | callTMxxx | | luaFunc1 | +--------------+ | | | luaV_execute |<======>| | +--------------+ +------------+ C stack Lua CallInfo's +------------+ | luaFunc2 | +--------------+ | | | luaV_execute |<======>| | ..........+--------------+........+------------+ | cFunc | | cFunc C| +--------------+ +------------+ | luaV_execute |<======>| luaFunc1 | +--------------+ +------------+ C stack Lua CallInfo's此圖顯示了在呼叫 Lua 程式碼的元方法或 C 函數的情況下,C 堆疊和 Lua CallInfo? 如何對齊。在這種情況下,未設置 CI_CALLING 標誌。虛線表示 C 堆疊如何分為兩組 C 框架,每個 Lua CallInfo? 一組。這條線正是標準 Lua 不允許您跨越的邊界。
消除元方法的此限制的一種策略是使元方法呼叫的行為類似於 Lua 的普通函數呼叫;也就是說,它們不會重新進入 luaV_execute(),而只是「goto」到已在執行的 luaV_execute() C 框架的開頭。雖然對於純 Lua 元方法來說可能是可行的,但由於以下兩個原因,這種方法不適用於 C 函數
C 函數通常將局部變數存儲在 C 堆疊上。為了維護 luaV_execute() 的單個實例,必須將這些變數從 C 堆疊移到其他地方(例如,Lua 堆疊),以便讓 C 堆疊展開。這對於 Lua 函數來說不是問題,因為 Lua 函數將其局部變數存儲在 Lua 堆疊上。但是,對於 C 函數,必須展開堆疊才能回調到 Lua。這是 C 函數的不良要求。儘管 C 函數必須能夠這樣做才能充分利用協程,但並非每次都必須這樣做,才能回調到 Lua。如果被呼叫的程式碼從未產生,那麼來回傳輸變數只是浪費 CPU 時間。
此補丁通過使用樂觀策略來解決這些問題。當從 C 函數呼叫 Lua 程式碼時,首先假設 Lua 程式碼不會產生,並且 C 堆疊會像在標準 Lua 中那樣遞迴地建立。如果被呼叫的函數在沒有產生的情況下完成,則情況與標準 Lua 完全相同。但是假設 Lua 程式碼反而產生了
+-----------------+ +------------+ /---| coroutine.yield | | luaFunc2 | | +-----------------+ | | -1 | | luaV_execute |<======>| | <- Yield | +-----------------+ +------------+ \ return| | cFunc | | cFunc C| <-- CI_CALLING flag becomes set value | +-----------------+ +------------+ / | | luaV_execute |<======>| luaFunc1 | <- v +-----------------+ +------------+ C stack Lua CallInfo'syield 函數產生返回值 -1。此時,直譯器知道樂觀策略不起作用,並開始展開 C 堆疊。隨著 yield 返回值向下傳播,每個 C 框架都必須將其狀態保存在相應的 Lua 框架中,設置 CI_CALLING 標誌並返回 -1,否則拋出錯誤,說明無法完成 yield。正是在這一點上,將發生關於非法產生的錯誤。
如果 yield 成功,則 C 堆疊返回其展開狀態,並且 Lua 堆疊包含所有狀態資訊。通過等到出現 -1 yield 返回值後再要求 C 函數保存狀態,我們優化了不發生 yield 的情況,同時讓 C 函數有機會在發生 yield 時處理它。
恢復協程時,會像在當前 Lua 中一樣檢查頂部 CallInfo?。由於堆疊已成功展開,因此設置了 CI_CALLING 標誌。這表明完成函數呼叫的程式碼(對 luaD_poscall 的呼叫)沒有在 C continuation 上等待,我們需要根據 CallInfo? 狀態找出如何完成函數呼叫並恢復函數。對於 Lua 函數,這涉及查看 (ci->savedpc - 1) 處指令的操作碼。在一個名為 luaV_return() 的新函數中有一個開關可以做到這一點。對於 C 函數,它涉及再次呼叫 C 函數以要求它恢復自身。在 CallInfo? 結構中有一個新的使用者定義的整數/指標聯合欄位,C 函數可以使用該欄位來做到這一點。這也由 luaV_return() 處理。
以下是補丁更改的摘要
錯誤訊息指示使用了哪個無法處理 yield 的 API 呼叫。
int lua_call_yp (lua_State *L, int nargs, int nresults, int tailcall);
此函數與 lua_call() 的含義相同,只是它不會阻止被呼叫的程式碼產生。如果發生 yield,則返回值為 -1。否則,返回值為實際結果數。
如果此呼叫是 C 函數要執行的最後一個操作,則 tailcall 參數應設置為 1。在這種情況下,如果程式碼產生,則當它恢復時,被呼叫的函數返回的結果將成為 C 函數的結果。在這種情況下,可以像這樣簡單地呼叫 API
如果 tailcall 參數設置為 0,並且程式碼產生,則當它恢復時,將重新呼叫 C 函數。在這種情況下,lua_call_yp() 返回 -1,並且 C 函數將需要保存其局部變數和其他狀態的狀態,以便在重新呼叫 C 函數時能夠重建它們。它可以使用 Lua 堆疊來做到這一點;但是,它_不能_將值壓入 Lua 堆疊,因為被呼叫的函數已直接佔用了堆疊上方的空間。
對於許多 C 函數來說,最簡單的方法可能是在堆疊中保留幾個插槽,這些插槽可以用數字或使用者資料填充(如果需要清理,則設置 __gc 元方法。)
當重新呼叫 C 函數時,Lua 堆疊就像原始 lua_call_yp() 成功時的樣子,並且可以再次將值壓入堆疊。
void *lua_get_frame_state (lua_State *L);
此函數返回指向 Lua 堆疊框架中變數的指標,C 函數可以使用該指標在 C 函數的多次呼叫之間保留狀態。返回值可以轉換為 int * 或 void **。當第一次呼叫 C 函數時,框架狀態的值始終為 0。在返回 -1 以防產生 API 呼叫之前,C 函數應將框架狀態設置為非零值,以便它在下次被呼叫時能夠識別它正在被恢復。
框架狀態值的可能用途是:簡單迴圈的數字計數器、存儲變數的堆疊索引或指向使用者資料的指標(當然,使用者資料必須存儲在堆疊中的某個位置,以防止它被收集。)
table.foreachi() table.foreach() tostring() print() dofile() string.gsub()因此,從這些函數呼叫的回調函數能夠產生。
這些操作碼是
OP_CALL OP_TAILCALL OP_GETTABLE OP_GETGLOBAL OP_SELF OP_ADD OP_SUB OP_MUL OP_DIV OP_POW OP_CONCAT OP_UNM OP_LT OP_LE OP_EQ OP_SETTABLE OP_SETGLOBAL OP_TFORLOOPluaV_return() 在執行 OP_RETURN 期間從 luaV_execute() 呼叫,並在恢復協程時從 resume() 呼叫。
0 = 此 CallInfo? 存在一個活動的 C 堆疊框架,
CI_CALLING 標誌設置在 -
lvm.c 中,在執行 OP_CALL 和 OP_TAILCALL 期間,當
lvm.c 中,在執行呼叫元方法的操作碼期間
nCcalls 計數器不再用於禁止 yield 的發生。相反,無效 yield 的錯誤是由無法處理 yield 的 API 呼叫產生的。nCcalls 計數器仍然會被維護;但是,它的值可能沒有那麼有用,因為它會計算已暫停(從 C 堆疊中移除)的 C 函式;因此它可能與 C 堆疊的大小沒有任何關係。
lua_gettable_yp() lua_settable_yp() ... etc
完整的 YP API 函式集