遞延呼叫

lua-users home
wiki

使用 C 封閉呼叫遞延函式呼叫。

描述

有時需要在程式中一個地方定義函式呼叫,但也需要將其執行遞延到稍後時間。例如,事件驅動系統需要序列化對事件非同步發生的回應。我們需要在事件發生時對應答函式進行排隊,然後在對較早(或優先順序較高)的事件的回應完成後對其進行出隊和執行。如果回應函式沒有參數,這很直接 - 只需將函式儲存在表格中即可。然而,如果回應函式需要參數進一步描述事件,則會出現問題 - 參數在函式排隊時已知,但在函式執行時卻未知。

這是使用封閉的典型範例,如果整個系統是用 Lua 編寫的,這就很簡單。但是,我們可能希望使用 C 編寫排隊系統,而僅使用 Lua 編碼事件回應函式。由於透過 C API 無法直接使用封閉式函式,就像在 Lua 中使用詞彙範圍一樣。

這裡介紹的方案使用 C 封閉式呼叫來封裝任意 Lua 函式及其呼叫參數。當呼叫 C 封閉時,它會從其上值中復原 Lua 函式及其參數,進行呼叫並傳回結果。由於 C 封閉僅是 Lua 函式,因此可以儲存在註冊表或任何其他表格中。

這個方案也可以在 Lua 中浮現出來,以簡化使用閉包來建立遞延函式呼叫,如果我們首先從 Lua 來處理這個問題,會最容易理解。

Lua 程式碼

稍後呈現的 C 程式碼會發布新的全域函式 createdeferredcall,其用法如下

local func = function(p1, p2, p3) print("func", p1, p2, p3); return "ret1","ret2"; end
local dfunc = createdeferredcall(func, "call1", "call2", "call3")

dfunc 只是一個保存 Lua 函式的變數,但這個函式現在封裝了原始函式 func 及其三個字串參數的值(可以有任何數量的任何 Lua 類型的參數)。我們可以將它視為與任何 Lua 變數完全相同。稍後,當要執行函式時,我們只需呼叫 dfunc 而無需參數

print(dfunc())

會出現以下結果

>func call1 call2 call3
>ret1 ret2

如果呼叫期間發生錯誤,系統會報告錯誤並終止,與正常的呼叫操作類似。但是,我們可以將錯誤函式傳遞到呼叫,然後它將表現得像 pcall

print(dfunc(function() print("inerrorfunc"); return "errormess"; end))

這會出現以下結果

>func call1 call2 call3
>true ret1 ret2

如果在 func 中引發錯誤,呼叫會傳回布林值 false,後跟錯誤函式提供的訊息

>inerrorfunc
>false errormess

這在概念上和實務上比在標準 Lua 中使用字彙範圍產生閉包更直接。不過,真正的優點在於 *createdeferredcall* 也可用作新的 C API 函式,讓我們可以執行過去無法執行的操作,也就是透過 Lua 中定義的函式在 C 中建立閉包。

C 程式碼

以下是 C 程式碼

static int Lua_DcallDelegate(lua_State* L)
{
  int efun = (lua_isfunction(L, 1))? 1 : luaL_optint(L, 1, 0);
  int nret = luaL_optint(L, 2, LUA_MULTRET);
  lua_checkstack(L, 1);
  lua_pushboolean(L, TRUE);
  int sm = lua_gettop(L);
  int ix = 1;
  while (!lua_isnone(L, lua_upvalueindex(ix)))
  {
    lua_checkstack(L, 1);
    lua_pushvalue(L, lua_upvalueindex(ix++));
  }
  ix--;
  if ((ix < 1) || (!lua_isfunction(L, (-1 * ix)))) return luaL_error(L, "Bad Deferred Call");
  if (lua_pcall(L, ix - 1, nret, efun) == 0)
    return (efun == 0)? lua_gettop(L) - sm : lua_gettop(L) - sm + 1; 
  else
  {
    lua_pushboolean(L, FALSE);
    lua_replace(L, sm);
    return (efun == 0)? lua_error(L) : 2;
  }
}

static void luaX_pushdcall(lua_State* L, int nargs)
{
  luaL_checktype(L, 1, LUA_TFUNCTION);
  lua_pushcclosure(L, Lua_DcallDelegate, nargs + 1);
}

static int luaX_dcall(lua_State* L, int nresults, int errfunc)
{
  lua_checkstack(L, 2);
  lua_pushinteger(L, errfunc);
  lua_pushinteger(L, nresults);
  return lua_pcall(L, 2, nresults, errfunc);
}

static int LuaMakeDeferredCall(lua_State* L)
{
  luaX_pushdcall(L, lua_gettop(L) - 1);
  return 1;
}

lua_pushcfunction(L, LuaMakeDeferredCall);
lua_setglobal(L, "makedeferredcall");

*luaX_pushdcall* 和 *luaX_dcall* 是兩個新的 API 函式,它們密切地遵循 *lua_call* 使用的慣例。*luaX_pushdcall* 需要函式堆疊在 *nargs* 參數的下方,且最後一個參數在堆疊最上方。結束時,這些參數會彈出,而函式則會更換成擁有 *Lua_DcallDelegate* 函式的 C 閉包。*luaX_dcall* 需要 C 閉包放在堆疊的最上方。至於 *lua_call*,*nresults* 會指定需要取用的回傳參數數量(它可以是 LUA_MULTRET 鍵)。參數 *errfunc* 可設定為 0 來指定沒有錯誤函式或指定錯誤函式在堆疊中的位置,這和 *lua_pcall* 類似。

如果 *errfunc* 為 0,封裝函式中的任何錯誤都是終止性的,否則回傳參數會在堆疊中,其最後一個參數在堆疊的最上方。

如果已識別出 *errfunc*,封裝函式中的任何錯誤都會導致 *luaX_dcall* 在 *errfunc* 所回傳的字串訊息下回傳布林值 *false*,否則,它會在回傳參數下回傳布林值 *true*,而最後一個參數在堆疊的最上方。

JohnHind

使用 LuaK,它是支援延期語意的完整 Lua 5.x 版本。

例如

function foo()
  defer
    defer
      print("defer in defer")
    end
    print("defer")
  end
  print("foo")
end

foo() => output
foo
defer
defer in defer

原始碼:git@github.com:peterk9999/LuaK.git

如果你想查看這個版本和 lua 5.1.1 之間的差異,請參閱此 git@github.com:rst256/luaK__diff.git 這不是分岔,也不是分支,這個儲存庫只用於查看差異。實際上,程式碼是在 15.03.2015 13:17:43。


RecentChanges · 喜好設定
編輯 · 歷程記錄
最後編輯於 2015 年 3 月 15 日上午 8:05 GMT (diff)