lua闭包

lua中有两种闭包, c闭包和lua闭包

两种闭包的公共部分:

#define ClosureHeader CommonHeader;\
lu_byte isC; \  是否c闭包
lua_byte nupvalues; \ upvalue的个数
GCObject* gclist; \
struct Table env 闭包的环境

C闭包的结构

struct CClosure{
   ClosureHeader;
   lua_CFunction f;
   TValue upvalue[1];
}

结构比较简单, f是一个满足 int lua_func(lua_State*) 类型的c函数

upvalue是创建C闭包时压入的upvalue, 类型是TValue, 可以得知, upvalue可以是任意的lua类型

Lua闭包结构

struct LClosure{
  ClosureHeader;
  strcut Proto* p;
  UpVal* upvals[1];
}

Proto的结构比较复杂, 这里先不做分析

统一的闭包结构, 一个联合体, 说明一个闭包要么是C闭包, 要么是lua闭包, 这个是用isC表识出来的.

union Closure{
    CClosure c;
    LClosure  l;
}

闭包 == {功能抽象, upvalue, env}

向lua中注册c函数的过程是通过lua_pushcclosure(L, f, n)函数实现的

流程:

  1. 创建一个 sizeof(CClosure) + (n - 1) * sizeof(TValue)大小的内存, 这段内存是 CClosure + TValue[n],, isC= 1 标示其是一个C闭包.

  2. c->f = f绑定c函数. ——— 闭包.功能抽象 = f

  3. env = 当前闭包的env. ———- 闭包.env = env

  4. 把栈上的n个元素赋值到c->upvalue[]数组中, 顺序是越先入栈的值放在upvalue数组的越开始位置, c->nupvalues指定改闭包upvalue的个数. ———- 闭包.upvalue = upvalue

  5. 弹出栈上n个元素, 并压入新建的Closure到栈顶.

整个流程是: 分配内存, 填写属性, 链入gc监控, 绑定c函数, 绑定upvalue, 绑定env一个C闭包就ok了

C闭包被调用的过程

lua 闭包调用信息结构:

struct CallInfo{
    StkId base; ----闭包调用的栈基
    StkId func; ----要调用的闭包在栈上的位置
    StkId top;  ----闭包的栈使用限制
    const Instruction *savedpc; ----如果在本闭包中再次调用别的闭包, 那么该值就保存下一条指令以便在返回时继续执行
    int nresults; ----闭包要返回的值个数
    int tailcalls;----尾递归用, 暂时不管
}

这个结构是比较简单的, 它的作用就是维护一个函数调用的有关信息, 其实和c函数调用的栈帧是一样的, 重要的信息base –> ebp, func –> 要调用的函数的栈index, savedpc –> eip, top, nresults和tailcalls没有明显的对应.

在lua初始化的时候, 分配了一个CallInfo数组, 并用L->base_ci指向该数组第一个元素, 用L->end_ci指向该数组最后一个指针, 用L->size_ci记录数组当前的大小, L->ci记录的是当前被调用的闭包的调用信息.

下面讲解一个c闭包的调用的过程:
情景: c 函数

int lua_test(lua_State* L){
    int a = lua_tonumber(L, 1);
    int b = lua_tonumber(L, 2);
    a = a + b;
    lua_pushnumber(L, a);
}

已经注册到了lua 中, 形成了一个C闭包, 起名为”test”, 下面去调用它
luaL_dostring(L, "c = test(3, 4)")

调用过程堆栈变化情况如下:

1.初始栈

2.压入了函数和参数的堆栈

lua_getglobal(L, “test”)
lua_pushnumber(L, 3)
lua_pushnumber(L, 4) 

3.调用lua_test开始时的堆栈 lua_call(L,3, 4)

4.调用结束的堆栈

  1. 取出结果的栈 lua_setglobal(L, “c”)

lua_call函数的过程

  1. lua具有很强一致性, 不管是dostring, 还是dofile, 都会形成一个闭包, 也就是说, 闭包是lua中用来组织结构的基本构件, 这个特点使得lua中的结构具有一致性, 是一种简明而强大的概念.
  2. 根据1, a = test(3, 4)其实是被组织成为一个闭包放在lua栈顶[方便期间, 给这个lua闭包起名为bb], 也就说dostring真正调用的是bb闭包, 然后bb闭包执行时才调用的是test[保存当前信息到当前函数的CallInfo中]
  3. 在调用test的时刻, L->ci记载着bb闭包的调用信息, 所以, 先把下一个要执行的指令放在L->ci->savedpc中, 以供从test返回后继续执行.
  4. 取栈上的test C闭包 cl, 用 cl->isC == 1断定它的确是一个C闭包[进入一个新的CallInfo, 布置堆栈]
  5. 从L中新分配一个CallInfo ci来记录test的调用信息, 并把它的值设置到L->ci, 这表明一个新的函数调用开始了, 这里还要指定test在栈中的位置, L->base = ci->base = ci->func+1, 注意, 这几个赋值很重要, 导致的堆栈状态由图2转化到图3, 从图中可以看出, L->base指向了第一个参数, ci->base也指向了第一个参数, 所以在test中, 我们调用lua_gettop函数返回的值就是2, 因为在调用它的时候, 它的栈帧上只有2个元素, 实现了lua向c语言中传参数.
    [调用实际的函数]

  6. 安排好堆栈, 下面就是根据L->ci->func指向的栈上的闭包(及test的C闭包), 找到对应的cl->c->f, 并调用, 就进入了c函数lua_test [获取返回值调整堆栈, 返回原来的CallInfo]

  7. 根据lua_test的返回值, 把test闭包和参数弹出栈, 并把返回值压入并调整L->top

  8. 恢复 L->base, L->ci 和 L->savedpc, 继续执行.

调用一个新的闭包时:

  1. 保存当前信息到当前函数的CallInfo中 (CallInfo函数调用的状态信息)
  2. 进入一个新的CallInfo, 布置堆栈
  3. 调用实际的函数
  4. 获取返回值调整堆栈, 返回原来的CallInfo
坚持原创技术分享,您的支持将鼓励我继续创作!