如何在 Lua 函数闭包中使用尾调用?

在 Lua 中,可以通过函数指针和额外的值(_upvalues_)创建 C 闭包,当闭包被调用时,这些值将可用。

在我的应用程序中,我使用这个功能将 getter 方法表传递给 __index 元方法。如果键存在为方法,则会调用该方法并传递其原始参数。如果我直接调用该函数,则闭包内的 upvalues 仍然可用于被调用者,因此在同一函数闭包中执行。

是否可以离开函数闭包或以某种方式消除 upvalues?目标是减少不必要的 upvalues 暴露,而不会引入重大开销。

这是一个 MWE(42 行),演示了问题,并用 TODO 强调了问题。目前会打印两个 upvalue: 42。预期结果是一个 upvalue: 42 和另一个 upvalue: 0(表示无效值)。

#include <stdlib.h>
#include <stdio.h>
#include <lua.h>

static void *allocfn(void *ud, void *ptr, size_t osize, size_t nsize) {
    if (nsize == 0) {
        free(ptr);
        return NULL;
    } else {
        return realloc(ptr, nsize);
    }
}

static int myfunc(lua_State *L) {
    lua_CFunction cfunc = lua_tocfunction(L, 1);
    printf("myfunc called with %p\n", cfunc);
    printf("upvalue: %d\n", (int)lua_tointeger(L, lua_upvalueindex(1)));
    // TODO how to drop upvalue (tail call, leaving the closure)?
    return cfunc(L);
}

static int otherfunc(lua_State *L) {
    printf("otherfunc called with %p\n", lua_tocfunction(L, 1));
    printf("upvalue: %d\n", (int)lua_tointeger(L, lua_upvalueindex(1)));
    return 0;
}

int main() {
    lua_State *L = lua_newstate(allocfn, NULL);

    /* Create closure for myfunc with upvalue 42. */
    lua_pushinteger(L, 42);
    lua_pushcclosure(L, myfunc, 1);

    /* Argument 1 for myfunc. */
    lua_pushcclosure(L, otherfunc, 0);

    /* Invoke myfunc(otherfunc) with "42" in its closure. */
    lua_call(L, 1, 0);

    lua_close(L);
}
点赞
用户106104
用户106104

将函数通过 Lua 调用,而不是直接调用 C 函数:

static int myfunc(lua_State *L) {
    // 在此仍使用 cfunc 进行打印   
    lua_CFunction cfunc = lua_tocfunction(L, 1);
    printf("myfunc called with %p\n", cfunc);
    printf("upvalue: %d\n", (int)lua_tointeger(L, lua_upvalueindex(1)));

    // 将 C 函数像调用 Lua 函数一样调用
    int stackSizeBefore = lua_gettop(L);
    lua_call(L, 0, LUA_MULTRET);
    return lua_gettop(L) - stackSizeBefore;
}

(注: 未经测试的代码)

2016-10-02 20:41:28
用户427545
用户427545

Lua 的 upvalues 实质上仅限于闭包内使用,你无法在其他闭包中访问 upvalues。调用 C 闭包(通过 lua_pushcclosure 创建)或调用 Lua 闭包(通过 "block"lua_load 等创建)可以进入闭包。

要在 C 中进入新的闭包,可以使用 lua_call 函数系列来调用,详见 immibis' 答案

另一个想法是“擦除”当前范围内的 upvalues。这样做无法达到预期效果,因为 upvalues 绑定在闭包中,因此一旦你“清除”它(将值设置为 nil),它将持久到下一次调用(这就是 Programming in Lua 文档中等同于静态变量的含义)。

于是又出现了一个新问题:能否通过从 C 函数调用另一个函数来覆盖现有的闭包?答案是不行。当闭包被调用时,它最终会调用 luaD_precall。此 C 函数基本上在当前 Lua 状态上创建一个新的“函数范围”,然后调用 C 函数指针:

int luaD_precall (lua_State *L, StkId func, int nresults) {
  lua_CFunction f;
  CallInfo *ci;
  // ...
  switch (ttype(func)) {
    // ...
    case LUA_TCCL: {  /* C closure */
      f = clCvalue(func)->f;/* obtain C function pointer */
      // ...
      ci = next_ci(L);      /* now 'enter' new function ("nested scope") */
      // ...
      n = (*f)(L);          /* do the actual call to a C function */
      // ...
      luaD_poscall(L, L->top - n);
      return 1;             /* not Lua code, do not invoke Lua bytecode */

只有在从函数返回后,它才会移动回到之前的“函数范围”:

int luaD_poscall (lua_State *L, StkId firstResult) {
  // ...
  CallInfo *ci = L->ci;
  // ...
  L->ci = ci = ci->previous;  /* back to caller ("function scope") */

除非在某种方式下改变程序计数器到某个自定义函数,否则你将无法在当前闭包之外调用你的函数。此外,你将会有一个新问题:函数现在在父闭包中执行。此时的两种选择是接受第二个函数仅在当前范围内执行,或者创建一个新范围。

在 Lua 中类似的问题是是否重用范围:

function myfunc()
    print("myfunc")
    print("otherfunc")
end

或者创建一个新的范围:

function myfunc()
    print("myfunc")
    do  -- 注意:创建了新的 Lua 闭包
        print("otherfunc")
    end
end

为了减少开销,可以简单地重用当前闭包(“函数范围”)。如果隐藏本地 upvalues 很重要(以防止意外修改),则调用 C 闭包。无论如何,你都不能通过 lua_upvalueindex 获取其他 upvalues 的伪索引直接访问它们,这需要通过 lua_getupvalue 进行查询。

2016-10-02 22:40:51