可恢復 Vm Patch |
|
具體來說,現在您可以:
__gc
)。for x in func
)。table.foreach()
、dofile()
,...)。pcall()
、xpcall()
,... )。這是一個針對此問題的新型且完全可移植的方法。它結合了來自 Eric Jacobs「進階程序」修補程式和來自我的「真實 C 程序」修補程式工作經驗的想法。這些對 Lua 核心所做的變更儘可能少侵入,因為它們具有彈性的讓步機制。副作用之一是速度更快的 pcall()
和一般的較快速 Lua VM。
授權通知:我特此宣告我對 Lua 核心所貢獻的所有工作都受到與 Lua 核心相同的授權規範。- MikePall
Greg Falcon 也稱 VeLoSo 已提出要成為該修補程式的維護者。請將所有關於修補程式相關問題提交給他。
我已決定繼續我對於跨 C 讓步問題的不同方法的研究。請也看看 [Coco]。這兩種方法各有其優缺點。欲瞭解更多資訊,請在 [Lua 寄件清單檔案] 中搜尋「Coco」和/或「RVM」。- MikePall
我附議。 [Coco] 有許多比此修補程式更好的優點。它是一種更漂亮的處理方法,測試更完善,不需要修改您的 Lua C 函數以讓它們可回復,它可以在相當多的平台上運行,這是 [LuaJIT 1.x] 所必需的。如果您需要程式改善程式協同程序,那麼 Coco 可能對您的專案來說是正確的選擇。
話說回來,我覺得這個修補程式是有價值的,因為它具有 ANSI C 方法,這可以讓它在 Lua 建置平台上運行,並且可能有助於未來 Lua 核心具有更好的程式協同程序運作方式。- VeLoSo
LuaFiveTwo 實作一個類似的方法 [1]。
按一下下載修補程式 [相較於 5.1 final]。
更新 2006-04-16 VeLoSo:針對 Lua 5.1 final 修補。沒有重大的變更。我確實移除一些未進一步達成可恢復 VM 目標的效能修補程式 (為了清楚起見)。我使用相當大的心力更新了這支 5.1 修補程式,但可能仍會遺漏一些細微之處。買家自負。
更新 2005-05-24 MikePall:對於 Lua 5.1-work6 的修補程式。沒有太大的功能變更,但有一些內部重組。現在可以從死掉的協程中取得追蹤 (如基準線)。work6 中的新運算子 (*s 和 a%b) 自然也是可恢復的。
多個針對 Lua 5.1-work6 基準線的修正已包含在修補程式中:MSVC number2int 修復、*s 效能改善、移除未定義的 lua_checkpc 宣告。
Lua API **沒有** 被更動,而且 **不需要** 變更任何現有 Lua 程式碼。特別是,你現在可以在協程中放入使用 pcall()
的程式碼,而且依然可以從受保護函數中讓出,**不需要** 更改任何程式碼。
當然 **不需要** 變更現有的 C 程式碼。但現有函數肯定無法完全恢復,因為舊的 C API 沒有提供這項功能。只有當你的 C 函數需要呼叫回函式 (lua_call
) 或受保護呼叫 (lua_pcall
),**而且** 你要從被呼叫的函數中讓出時,這 **才** 是令人擔心的問題。
有少數幾個新的 C API 函數可讓你建立可恢復的 C 函數。轉換相當容易。例如,我只需要變更每一行中的兩行就可以讓標準 Lua 函式庫函數可恢復。有關教學課程,請見下方。
由於新讓出機制的特別技巧,因此對 Lua 核心所做的變更並未造成影響:只要有介入的 (但可恢復的) C 呼叫界限,讓出機制便只會擲出一個「讓出錯誤」,以快速展開 C 堆疊。沒有必要在每個函數呼叫傳回時檢查讓出條件,直到 C 堆疊最底部。
這有助於將變更減到最小限度,並讓轉換現有的 C 函數變簡單。對標準控制流程 (也就是說,未讓出) 沒有效能影響。
順帶一提:標準情況下的 coroutine.yield()
並未採用讓出擲出機制,以避免 longjmp 的開銷。
受保護呼叫機制已變更,以避免只要 C 堆疊中已經有這種包裝器,就不會使用 setjmp 封裝器。現在,展開 C 堆疊和 Lua 堆疊已變成兩件不相干的事。這讓 pcall()
的開銷變成和函數呼叫一樣:x86 上快了大約 10-15%,而對於具有許多暫存器的 CPU 來說甚至更快,例如 IA64 (Itanium) 上快了大約 30%。
我已做了相當多次的指令剖析和快取遺失剖析,並從 Lua 核心衍生出許多微小的最佳化。例如,協程需要大約少 10% 的記憶體。
我也修正了幾個 bug/錯誤特性
coroutine.yield()
回傳到 Lua 函數時,呼叫已移除的「呼叫」hook。coroutine.resume()
摧毀。lua_resume
回傳 ok 之後)。但目前僅從 C API 執行 (只要在堆疊上推入新的函數和參數,並透過 lua_resume
復活它)。請注意,因為許多單行變更和一些在 ldo.c 中必須移動的程式碼,所以此修補程式看來有點長。事實上,除了新的堆疊解開機制和 VM 指令碼的 resume 處理程式之外,沒那麼多新的程式碼。
僅有一個新的函數
epcall(err, f, arg1, arg2, ...)它旨在替換奇怪的
xpcall()
API,後者不允許你在想要設定錯誤處理程式時,傳遞參數給受保護的函數。這表示你必須人工建立封閉函式才能搭配 xpcall()
使用。epcall()
沒有這種限制。
epcall()
在各方面都像 pcall()
一樣,但它會設定一個錯誤處理器函數,在 Lua 堆疊解開前呼叫 (就像 xpcall()
所做的那樣)。
xpcall()
僅保留相容性。
另一個小變更是,當讓出失敗時,你會得到不同的錯誤訊息
這在錯誤發生時 (如果發生了) 更能反映情況。
一組新的 lua_v*
和 lua_i*
函數擴充了 lua_call()
、lua_pcall()
和 lua_yield()
void lua_call (lua_State *L, int nargs, int nresults); | void lua_vcall (lua_State *L, int nargs, int nresults, void *ctx); | void lua_icall (lua_State *L, int nargs, int nresults, int ictx); void lua_pcall (lua_State *L, int nargs, int nresults, int ef); | void lua_vpcall (lua_State *L, int nargs, int nresults, int ef, void *ctx); | void lua_ipcall (lua_State *L, int nargs, int nresults, int ef, int ictx); int lua_yield (lua_State *L, int nargs); | int lua_vyield (lua_State *L, int nargs, void *ctx); | int lua_iyield (lua_State *L, int nargs, int ictx);(別擔心,其中大多數僅是具有適當轉型的巨集。)
新的函數會接受一個內容參數 (void *
或 int
),其可用於儲存執行中的 C 函數的當前狀態。搭配非 NULL/非零內容參數使用這些 API 函數,會傳達給 Lua 核心,你的 C 函數是可以恢復的 (也就是說,允許呼叫回饋讓出)。
經典的不可恢復 API 函數 lua_call()
、lua_pcall()
和 lua_yield()
僅是巨集,會將 NULL 內容參數傳遞給等效的 lua_v*
。
可以使用兩個新的 API 函數來擷取內容
| void *lua_vcontext (lua_State *L); | int lua_icontext (lua_State *L);在 C 函數的第一次呼叫中,內容會初始化為 NULL/零。當 coroutine 讓出然後再次恢復時,可恢復的 C 函數會再次被單純呼叫。但這次保證內容為非 NULL/非零,並且反映 C 函數的儲存狀態 (或錯誤編號,如果錯誤已被
lua_vpcall
/lua_ipcall
捕獲)。
當您使用新的 API 回呼函數時,您必須注意,包括您的 C 函數在內的 C 堆疊可能會被解開。只有在回呼函數(或從中呼叫的任何函數)產生時,才會發生這種情況。在此情況下,控制流程絕不會從新的 API 呼叫返回至您的 C 函數。相反地,當 Coroutine 恢復時,您的 C 函數將被呼叫。
這表示您必須儲存 C 函數保留的所有內容。方法是將特定的內容引數傳遞至 API 呼叫(標記、索引/計數器或指標),或者將內容儲存在 Lua 堆疊上(在進行 API 呼叫之前)。請見下方的教學課程內容。
當 Coroutine 恢復時,呼叫堆疊中較高層級的任何函數皆會在恢復(返回)較低層級的函數之前完成。您不受限於在使用任一上述 API 函數之前或之後使用 Lua 值堆疊,因為當 C 函數正在執行時,堆疊中不可能存在更高層級的活動函數(與其處於暫停狀態時不同)。
所有 lua_*yield()
API 函數在某種意義上都是尾端呼叫,意即您必須與 return 陳述式一起使用此類函數,如下所示
return lua_vyield(L, na, ctx);依據是使用標準 yield 還是拋出 yield 機制,呼叫是否有可能在執行 return 陳述式之前返回至您的 C 函數。請勿嘗試透過在 yield 呼叫和 return 陳述式之間加入程式碼,來過度使用此機制。當內容為 NULL/零(或使用
lua_yield
)時,您的函數將不再被呼叫(尾端 yield)。否則,當 Coroutine 恢復時,您的函數將會再次被呼叫。
lua_vpcall
/lua_ipcall
可能會退回建立 C 堆疊上 setjmp 包裝器的經典行為。這會發生在尚未有此類包裝器,或 C 堆疊中存在介入非可恢復呼叫邊界的情況下(例如,從 hooks 或從 __gc
使用時)。當然,產生的呼叫堆疊不可恢復,但無論如何,在此之前它原本就是不可恢復的。您幾乎不會注意到這一點,因為獨立的 Lua 可執行檔 (lua.c
) 始終會使用 lua_pcall()
包裝主區段,而且當然 lua_resume()
也會建立 setjmp 包裝器。
另一個關於 lua_vpcall
/lua_ipcall
(但不是 lua_pcall
)要注意的地方是,當發生錯誤時,它們可能會將回呼函數及其引數留在錯誤訊息下方堆疊中(抱歉,這在 Lua 核心內很難解決)。在受保護的呼叫失敗時,您必須小心,不要對相對堆疊層級做出任何假設。當然,錯誤訊息保證會在最上層堆疊槽(相對索引 -1)中,而且回呼函數(絕對索引 1..(func-1))下方的所有內容也仍然完好無缺。
我已從核心移除了 lua_cpcall()
,並用簡單巨集取代了標準 API 呼叫。我建議將它標註為不建議使用,因為它是多餘的。
以下為一個簡短的教學,將顯示讓 C 函式可繼續(更改/新標示為 **/++)所需的變更。
table.foreach()
static int foreach (lua_State *L) { ++ if (lua_vcontext(L)) goto resume; luaL_checktype(L, 1, LUA_TTABLE); luaL_checktype(L, 2, LUA_TFUNCTION); lua_pushnil(L); /* first key */ for (;;) { if (lua_next(L, 1) == 0) return 0; lua_pushvalue(L, 2); /* function */ lua_pushvalue(L, -3); /* key */ lua_pushvalue(L, -3); /* value */ ** lua_icall(L, 2, 1, 1); ++ resume: if (!lua_isnil(L, -1)) return 1; lua_pop(L, 2); /* remove value and result */ } }在此,繼續所需的一切都已在 Lua 堆疊中(前一個金鑰)。因此,只需將
lua_call()
取代為 lua_icall()
並設定單一旗標 (1) 即可。同時請注意,由於堆疊內容保證在 C 函式繼續時保持不變,因此 goto
可以安全地在初始檢查中跳來跳去。這會省略多餘的檢查(例如,檢查使用者資料 metatable 的標準機制很慢)。
當然,如果你真的非常討厭 goto
,也可以使用 if
/switch
結構。但你必須了解,它們會模糊「標準」控制流程。這是使用 goto
有道理的案例之一。而且,如果你跳到迴圈中並忘記從上下文中擷取迴圈計數器,你的編譯器也會印出一個很大的醒目標誌。
print()
static int luaB_print (lua_State *L) { int n = lua_gettop(L); /* number of arguments */ ** int i = lua_icontext(L); ++ if (i) { ++ n -= 2; /* compensate for tostring function and result */ ++ goto resume; ++ } lua_getglobal(L, "tostring"); for (i=1; i<=n; i++) { const char *s; lua_pushvalue(L, -1); /* function to be called */ lua_pushvalue(L, i); /* value to print */ ** lua_icall(L, 1, 1, i); ++ resume: s = lua_tostring(L, -1); /* get result */ if (s == NULL) return luaL_error(L, "`tostring' must return a string to `print'"); if (i>1) fputs("\t", stdout); fputs(s, stdout); lua_pop(L, 1); /* pop result */ } fputs("\n", stdout); return 0; }在此,我們僅將迴圈計數器儲存在上下文中。你需要小心,僅使用非零索引,因為零上下文引數會標記不可繼續的呼叫(且無法與初始呼叫區分)。
函式執行期間堆疊層級可能會變更,這會導致繼續時出現許多問題。請將其維持在固定層級(可以使用 lua_settop()
確保這一點),或僅使用相對索引
或在上傳文檔後補償它(請見上方)。
pcall()
static int luaB_pcall (lua_State *L) { ++ int status = lua_icontext(L); ++ if (status) goto resume; luaL_checkany(L, 1); ** status = lua_ipcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0, -1); ++ resume: ~~ if (status > 0) { /* error */ ~~ lua_pushboolean(L, 0); ~~ lua_insert(L, -2); /* args may be left on stack with vpcall/ipcall */ ~~ return 2; /* return status + error */ ~~ } ~~ else { /* ok */ ~~ lua_pushboolean(L, 1); ~~ lua_insert(L, 1); ~~ return lua_gettop(L); /* return status + all results */ ~~ } ~~ }會與
lua_vpcall
/lua_ipcall
發生四個案例
除非你需要將上下文引數用於自己的用途(但是避免錯誤號碼範圍),否則你可將上下文設定為 -1,並在繼續執行時將其指定為狀態。這允許簡單檢查大於 0 的值(錯誤)或小於或等於 0 的值(正常)。你無法使用零,因為這會標記無法繼續的 pcall。
以上程式碼可應付 lua_vpcall
/lua_ipcall
可能會將呼叫函式及其引數留在堆疊中的情況(但僅在呼叫設定期間產生錯誤時才會發生)。明確編碼兩種可能結果會比使用內嵌條件式(如前述)來得容易。
typedef struct { ... } mytype_t; static int my_userdata_method (lua_State *L) { mytype_t *ud = (mytype_t *)lua_vcontext(L); ++ if (ud) goto resume; ud = (mytype_t *)luaL_checkudata(L, 1, MYTYPE_HANDLE); if (ud == NULL) ... /* error handling */ ... /* check other args here */ ... /* start processing */ ... /* be sure to save all context in the userdata structure */ ** lua_vcall(L, na, nr, ud); /* pass userdata as context */ ++ resume: ... /* continue processing */ }這是一個 userdata 方法的典型範例。大多數時候,您可以在 userdata 中儲存所有必要的內容,並僅將 userdata 指標本身用作環境引數。當然,userdata 仍留在堆疊中(它必須存在,否則可能會被垃圾收集),因此您可以再次擷取指標。但這會更慢,且環境引數已經存在,所以何不善加利用它呢?
userdata 方法也是 lua_vyield()
的一個完美範例
while (read_operation(ud, ...) == BLOCKING) { ... /* do any processing needed after a blocking indication */ ** return lua_vyield(L, na, ud); ++ resume: ... /* do any processing needed resuming the read */ }在此嘗試了一個潛在的封鎖 I/O 執行。操作並未封鎖,而是傳回一個特殊狀態旗標(封鎖)。這讓您可以儲存環境並讓出(例如讓出給執行續排程器),避免封鎖在 I/O 執行上。在函式復原時,您只需重試、繼續或完成 I/O 執行。如果 I/O 執行可能會重複封鎖,則在迴圈中需要執行此動作。
switch
的狀態機器static int myfunction (lua_State *L) { ++ int state = lua_icontext(L); ++ switch (state) { ++ case 0: /* initial call */ ... ** lua_vcall(L, na, nr, 1); ++ case 1: ... ** lua_vcall(L, na, nr, 2); ++ case 2: ... ** lua_vcall(L, na, nr, 3); ++ case 3: ... return n; } }有時候需要使用許多呼叫回或讓出,穿插在線性控制流程中。在此,使用狀態機器並將目前狀態(控制流程中的位置)儲存在環境中可能是有益的。您可以手動指派狀態來滿足簡單需求(如上所示,但可能使用定義而非數字)。對於較為複雜的需求,您可以使用巨集,甚至一個預編譯器(當然是用 Lua 編寫的?)從您的程式碼中自動產生狀態機器。
goto
的狀態機器如果您正在使用 GCC,您會很高興聽聞它有一個非常有用的(但非標準)擴充,稱為標籤做為值,又稱作計算的 goto。在 GCC 資訊文件上深入了解它。
這最適合與巨集內的區域標籤擴充合併。以下是轉換為使用此功能的範例(更簡潔,也快很多)
++ #define rvcall(L, na, nr) \ ++ ({ __label__ RR; lua_vcall(L, (na), (nr), &&RR); RR: ; }) static int myfunction (lua_State *L) { ++ void *cont = lua_vcontext(L); ++ if (cont) goto *cont; ... ** rvcall(L, na, nr); ... ** rvcall(L, na, nr); ... ** rvcall(L, na, nr); ... return n; }
以下是一個表格,確切地顯示您的程式碼在哪裡可以讓出,以及在哪裡不可以。
請注意,其餘的「否」大多數在實務上無關緊要。
Yield across ... | Yield from ... | Ok? | Rationale ------------------+------------------------+-----+--------------------------- for x in func | iterator function | Yes | VM operations | metamethod except __gc | Yes | (anywhere) | __gc metamethod | No | Difficult, rarely useful (anywhere) | count/line hook | Yes | Only via C API hooks (anywhere) | call/return hooks | No | Does anyone need this? error processing | err. handler/traceback | No | Not very useful ------------------+------------------------+-----+--------------------------- pcall() | protected callback | Yes | xpcall() | protected callback | Yes | [Deprecated] epcall() NEW | protected callback | Yes | Sane API, replaces xpcall print() | tostring() callback | Yes | tostring() | __tostring metamethod | Yes | dofile() | chunk | Yes | It was simple :-) table.foreach() | callback | Yes | table.foreachi() | callback | Yes | string.gsub() | callback | No | Tricky, I have no use case table.sort() | callback, __lt metam. | No | Not very useful require() | chunk | No | Not very useful debug.sethook() | Lua hook function | No | Please use a C hook load() | chunk reader | No | Parser is not resumable ------------------+------------------------+-----+--------------------------- lua_call() | callback | No | For compatibility (macro) lua_vcall() NEW | callback | Yes | lua_icall() NEW | callback | Yes | (macro) lua_pcall() | protected callback | No | For compatibility (macro) lua_cpcall() | protected callback | No | For compatibility (macro) lua_vpcall() NEW | protected callback | Yes | lua_ipcall() NEW | protected callback | Yes | (macro) lua_load() | chunk reader | No | Parser is not resumable ------------------+------------------------+-----+--------------------------- lua_equal() | __eq metamethod | No | (*) lua_lessthan() | __lt metamethod | No | (*) lua_gettable() | __index metamethod | No | (*) lua_getfield() | __index metamethod | No | (*) lua_settable() | __newindex metamethod | No | (*) lua_setfield() | __newindex metamethod | No | (*) lua_concat() | __concat metamethod | No | (*) ------------------+------------------------+-----+---------------------------(*) 不需要。只需使用
lua_vcall
/lua_icall
呼叫元方法即可。或使用 Lua 程式碼(總是可復原的)執行這些類型的執行。
lua_resume(L, -1)
有意義)。但除了引入一個新的 coroutine.eresume(co, err)
函數之外,我對一個美好的 Lua API 仍舊沒有任何概念。
debug.sethook()
的繼承。C hook 函數和 hook 覆蓋已經繼承。但目前的實作並沒有實現這一點,且沒有複製 Lua hook 函數(儲存在註冊表中錨定的特殊表格)。
lua_gettable
/lua_settable
。否則會毀損 Lua 函數的 PC。建議使用 lua_call()
(其中這是絕對沒問題的)呼叫函數來包裝任何此類活動,或改用 lua_rawget
/lua_rawset
。(這會影響所有 Lua 5.1 作業版本,而並非因我的修補程式造成。)