Deferred Call |
|
Sometimes it is necessary to define a function call at one point in a program but defer its execution for a later time. For example, an event driven system needs to serialise the response to events occurring asynchronously. We need to enqueue the response functions when the event occurs and then dequeue and execute them once the response to earlier (or higher priority) events has completed. If the response functions are parameterless this is straightforward - just store the functions in a table. However if the response functions require parameters to further characterise the event, there is a problem - the parameters are known when the function is enqueued, but not when it is executed.
This is an archetypical case for the use of closures which is straightforward if the whole system is written in Lua. However we may want to write the queuing system in C with only the event response functions coded in Lua. It is not possible to enclosure a Lua function directly using the C API as you can in Lua using lexical scoping.
The scheme presented here uses a C closure to encapsulate an arbitrary Lua function plus its calling parameters. When the C closure is called, it recovers the Lua function and its parameters from its upvalues, makes the call and returns the results. Since the C closure is just a Lua Function it can be stored in the registry or in any other table.
This scheme can also be surfaced in Lua to simplify the use of closures to create deferred function calls and it is easiest to understand if we approach it from Lua first.
The C code presented later publishes a new global function createdeferredcall which is used as follows:
local func = function(p1, p2, p3) print("func", p1, p2, p3); return "ret1","ret2"; end local dfunc = createdeferredcall(func, "call1", "call2", "call3")
dfunc is just a variable holding a Lua function, but this function now encapsulates the original function func and the values for its three string parameters (there could be any number of parameters of any Lua type). We can treat this exactly as we would any Lua variable. Later, when we want to execute the function, we simply call dfunc without parameters:
print(dfunc())
Which gives the following result:
>func call1 call2 call3 >ret1 ret2
If there is an error during the call, the system will report it and terminate, similar to a normal call operation. However we can pass an error function to the call and it will then behave like pcall:
print(dfunc(function() print("inerrorfunc"); return "errormess"; end))
This gives the following result:
>func call1 call2 call3 >true ret1 ret2
If an error is introduced into func the call will return boolean false followed by the message supplied by the error function:
>inerrorfunc >false errormess
This is already conceptually and practically more straightforward than using lexical scoping to produce a closure in standard Lua. However the real benefit is that createdeferredcall is also available as a new C API function allowing us to do something we could not do before - create a closure in C from a function defined in Lua.
Here is the C code:
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 and luaX_dcall are the two new API functions and they closely follow the conventions used by lua_call. luaX_pushdcall requires a Lua function on the stack underneath nargs parameters with the last parameter at the top of the stack. On exit the parameters have been popped and the function replaced by a C closure over the function Lua_DcallDelegate. luaX_dcall requires the C closure to be at the top of the stack. As for lua_call, nresults specifies the number of return parameters to be taken (it can be the key LUA_MULTRET). The parameter errfunc is 0 to specify no error function or the position of the error function on the stack, similar to lua_pcall.
If errfunc is 0, any error in the encapsulated function is terminal, otherwise the return parameters are on the stack with the last parameter at the top.
If an errfunc is identified, any error in the encapsulated function causes luaX_dcall to return boolean false under the string message returned by errfunc, otherwise it returns boolean true under the return parameters, with the last parameter at the top.
Using LuaK, it is a full Lua 5.x distribution with defer semantic support.
e.g.
function foo() defer defer print("defer in defer") end print("defer") end print("foo") end foo() => output foo defer defer in defer
source code: git@github.com:peterk9999/LuaK.git