可恢復 Vm Patch

lua-users home
Wiki

這是一個補丁,用以使 Lua VM 完全可恢復。它解決了在 [Lua 5.1] 中程序仍然存在的大部分問題。

具體來說,現在您可以:

這是一個針對此問題的新型且完全可移植的方法。它結合了來自 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/錯誤特性

請注意,因為許多單行變更和一些在 ldo.c 中必須移動的程式碼,所以此修補程式看來有點長。事實上,除了新的堆疊解開機制和 VM 指令碼的 resume 處理程式之外,沒那麼多新的程式碼。

Lua API 擴充套件

僅有一個新的函數

  epcall(err, f, arg1, arg2, ...)
它旨在替換奇怪的 xpcall() API,後者不允許你在想要設定錯誤處理程式時,傳遞參數給受保護的函數。這表示你必須人工建立封閉函式才能搭配 xpcall() 使用。epcall() 沒有這種限制。

epcall() 在各方面都像 pcall() 一樣,但它會設定一個錯誤處理器函數,在 Lua 堆疊解開前呼叫 (就像 xpcall() 所做的那樣)。

xpcall() 僅保留相容性。

另一個小變更是,當讓出失敗時,你會得到不同的錯誤訊息

這在錯誤發生時 (如果發生了) 更能反映情況。

C API 擴充套件

一組新的 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 函式可繼續(更改/新標示為 **/++)所需的變更。

範例 #1:單一旗標 - 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 有道理的案例之一。而且,如果你跳到迴圈中並忘記從上下文中擷取迴圈計數器,你的編譯器也會印出一個很大的醒目標誌。

範例 #2:迴圈計數器 - 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() 確保這一點),或僅使用相對索引

或在上傳文檔後補償它(請見上方)。

範例 #3:受保護的呼叫 - 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 可能會將呼叫函式及其引數留在堆疊中的情況(但僅在呼叫設定期間產生錯誤時才會發生)。明確編碼兩種可能結果會比使用內嵌條件式(如前述)來得容易。

範例 #4:使用者資料指標

    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 執行可能會重複封鎖,則在迴圈中需要執行此動作。

範例 #5a:具有 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 編寫的?)從您的程式碼中自動產生狀態機器。

範例 #5b:具有 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 程式碼(總是可復原的)執行這些類型的執行。

待辦事項


最新變更 · 偏好設定
編輯 · 歷史
上次於 July 4, 2010 12:32 am GMT 編輯 (差異)