延迟调用

lua-users home
wiki

使用 C 闭包实现延迟函数调用。

描述

有时有必要在一个程序的某个点定义一个函数调用,但将其执行推迟到以后。例如,一个事件驱动的系统需要串行化对异步发生的事件的响应。我们需要在事件发生时将响应函数入队,然后在对早期(或更高优先级)事件的响应完成后出队并执行它们。如果响应函数是无参数的,那么这很简单——只需将函数存储在一个表中。但是,如果响应函数需要参数来进一步表征事件,那么就会出现问题——参数在函数入队时已知,但在执行时却未知。

这是使用闭包的一个典型案例,如果整个系统是用 Lua 编写的,那么这很容易。但是,我们可能希望用 C 编写队列系统,而事件响应函数仅用 Lua 编写。使用 C API 不可能直接封装 Lua 函数,正如在 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_pushdcallluaX_dcall 是两个新的 API 函数,它们密切遵循 lua_call 使用的约定。luaX_pushdcall 需要堆栈上有一个 Lua 函数,其下方有 nargs 个参数,最后一个参数位于堆栈的顶部。退出时,参数已被弹出,函数被一个 C 闭包替换,该闭包封装了函数 Lua_DcallDelegateluaX_dcall 需要 C 闭包位于堆栈的顶部。与 lua_call 一样,nresults 指定要获取的返回参数的数量(它可以是键 LUA_MULTRET)。参数 errfunc 为 0 表示没有错误函数,或者表示错误函数在堆栈中的位置,这与 lua_pcall 类似。

如果 errfunc 为 0,则封装函数中的任何错误都将导致程序终止;否则,返回参数位于堆栈上,最后一个参数位于顶部。

如果指定了 errfunc,则封装函数中的任何错误都会导致 luaX_dcall 返回布尔值 false,后跟 errfunc 返回的字符串消息;否则,它返回布尔值 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 这不是 fork 也不是分支,这个仓库只是为了查看差异。代码实际在 2015 年 3 月 15 日 13:17:43。


RecentChanges · preferences
编辑 · 历史
最后编辑于 2015 年 3 月 15 日上午 3:05 GMT (差异)