Lua 5.3 源碼分析 (七) 閉包 Closure

Lua 5.3 源碼分析 (七) 閉包 Closure

概述

閉包(Closure)在函數式編程中是一個重要概念,如果說 C+ + 的面向對象編程是把一組函數綁定到特定的數據類型上的話,那麼閉包就是把一組數據綁定到特定的函數上。

這裏寫圖片描述

當調用 counter 後,會得到一個函數。這個函數每調用一次,返回值會加一。
我們把這個返回的你們函數記作一個計數器。counter 可以產生多個計數器,每個都獨立計算。也就是說,每個計數器函數都獨享一個變量 t ,相互不干擾。這個 t
被稱爲計數器函數的 upvalue ,被綁定到計數器函數中。 擁有upvalue 的函數就是閉包。

在Lua中, 只有閉包, 函數只是 upvalue 數量爲零的閉包。

函數原型 Proto

閉包是函數和 upvalue 的結合體。在Lua 中,閉包統一被稱爲函數,而函數就是這裏所指的函數原型。 函數原型 Proto 是不可以直接被調用,值有和upvalue 綁定後才變成了 Lua VM 能夠解釋的 函數對象。

在公開的 API 定義中,是不存在函數原型 Proto 類型的。
它只存在於實現中(定義在 lobject.h 中),但是它也是一種 GCObject 被GC 管理。
define LUA_TPROTO   LUA_NUMTAGS

Lua 的函數是有層級的,允許嵌套,所以在Proto 結構中,能看到對內層 Proto 的 引用 p。

生成可用的 Lua 的閉包的過程就是將 Proto 與 upvalue 綁定的過程,爲了避免重複生成不必要的閉包,當生成一次閉包對象後,都將被 cache 引用,下次再通過這個Proto 生成 閉包時,通過比較 upvalue 是否一致來決定是否複用。cache 是一個 弱引用,一旦GC 過程中發現引用的閉包已經不存在了,cache 將被置空。

Proto *luaF_newproto (lua_State *L) {
  GCObject *o = luaC_newobj(L, LUA_TPROTO, sizeof(Proto));
  Proto *f = gco2p(o);
  f->k = NULL;
  f->sizek = 0;
  f->p = NULL;
  f->sizep = 0;
  f->code = NULL;
  f->cache = NULL;
  f->sizecode = 0;
  f->lineinfo = NULL;
  f->sizelineinfo = 0;
  f->upvalues = NULL;
  f->sizeupvalues = 0;
  f->numparams = 0;
  f->is_vararg = 0;
  f->maxstacksize = 0;
  f->locvars = NULL;
  f->sizelocvars = 0;
  f->linedefined = 0;
  f->lastlinedefined = 0;
  f->source = NULL;
  return f;
}

在析構函數中能看到 :在釋放一個 Proto結構時,順帶回收了內部數據結構佔用的內存。因爲Proto 結構中 記錄了每個內存塊的尺寸,使用Lua 的 LuaM_xxxx API 能精準的管理內存。

void luaF_freeproto (lua_State *L, Proto *f) {
  luaM_freearray(L, f->code, f->sizecode);
  luaM_freearray(L, f->p, f->sizep);
  luaM_freearray(L, f->k, f->sizek);
  luaM_freearray(L, f->lineinfo, f->sizelineinfo);
  luaM_freearray(L, f->locvars, f->sizelocvars);
  luaM_freearray(L, f->upvalues, f->sizeupvalues);
  luaM_free(L, f);
}

Upvalue

Upvalue 指在Lua 閉包生成的那一刻,與函數原型Proto 綁定在一起的那些外部變量。這些變量原本是上一層函數的局部變量或 upvalue,可以在上層返回後,繼續被閉包引用。

當外層函數並沒有退出時,調用剛生成的閉包,這個時候閉包更像一個普通的內嵌函數。外層函數的局部變量只是數據棧上的一個普通變量,Lua VM 用一個數據棧上的索引 映射局部變量, 內嵌函數可以通過數據棧自由訪問它。

而一旦外層函數返回,數據棧空間收縮,原有的局部變量消失了。這個時候閉包需要用其它方式訪問這些upvalue。

如果將數據棧上的每個變量都實現成一個獨立的對象是沒有必要的,尤其是數字、布爾類型,沒有必要實成複雜對象。

Lua 的 upvalue 的實現更像是 C 語言中的 指針。它引用了另外一個對象。多個閉包可以共享同一個 upvalue ,猶如 C 語言中,可以有多個指針指向同一個結構體。

typedef struct UpVal UpVal;

struct UpVal {
  TValue *v;  /* points to stack or to its own value */
  lu_mem refcount;  /* reference counter */
  union {
    struct {  /* (when open) */
      UpVal *next;  /* linked list */
      int touched;  /* mark to avoid cycles with dead threads */
    } open;
    TValue value;  /* the value (when closed) */
  } u;
};

UpVal 結構直接用一個 TValue 指針引用了一個 Lua 值變量。

當被引用的變量還在數據棧上時,這個指針直接指向棧上的地址。這個upvalue 被稱爲開放的。

遍歷當前所有open 的 upvalue 利用的是當前 lua_State ->openupval 鏈表。

鏈表指針 保存在一個 聯合中。

當 upvalue 被關閉後,就不再需要這兩個指針了。所謂close 狀態 upvalue是指:當upvalue 引用的數據棧上的數據不再存在於數據棧時(通常是由申請局部變量的函數返回引起的), 需要把 upvalue 從 lua_State ->openupval 開放鏈表中剔除,並把其引用的數據棧上的變量值存到另外一個安全的內存空間(UpVal 結構體內)。

upvalue 的 open 與 close 狀態 在UpVal 結構中 不需要用一個標記位區分 。因爲當upvalue close 時, UpVal 中的 v 指針一定指向結構體內部的 value。

所以下面的宏可以判斷upvalue 的open 與 close 狀態。
define upisopen(up) ((up)->v != &(up)->u.value)

這裏寫圖片描述

由於Lua 的數據棧是可以擴展的,當數據棧內存空間擴展時,其內存地址會發生變化。這個時候需要 修正 UpVal 結構中的指針。 correctstack 函數處理修正的操作。

static void correctstack (lua_State *L, TValue *oldstack) {
  CallInfo *ci;
  UpVal *up;
  L->top = (L->top - oldstack) + L->stack;
  for (up = L->openupval; up != NULL; up = up->u.open.next)
    up->v = (up->v - oldstack) + L->stack;
  for (ci = L->ci; ci != NULL; ci = ci->previous) {
    ci->top = (ci->top - oldstack) + L->stack;
    ci->func = (ci->func - oldstack) + L->stack;
    if (isLua(ci))
      ci->u.l.base = (ci->u.l.base - oldstack) + L->stack;
  }
}

閉包

Lua 支持兩種閉包:
1. Lua 語言實現的。
2. C 語言實現的。
從 外部數據類型看,它們屬於同一種類型(Lua_TFUNCTION)。
從 GC 角度看,它們同屬於 GCObject 類型。

typedef union Closure {
  CClosure c;
  LClosure l;
} Closure;

Lua 閉包

Lua 閉包僅僅是 Proto 和 UpVal 綁定的集合。

typedef struct LClosure {
  ClosureHeader;
  struct Proto *p;
  UpVal *upvals[1];  /* list of upvalues */
} LClosure;

LClosure *luaF_newLclosure (lua_State *L, int n) {
  GCObject *o = luaC_newobj(L, LUA_TLCL, sizeLclosure(n));
  LClosure *c = gco2lcl(o);
  c->p = NULL;
  c->nupvalues = cast_byte(n);
  while (n--) c->upvals[n] = NULL;
  return c;
}

luaF_newLclosure 構造函數 只綁定了 Proto 而沒有初始化upvalue對象。這是因爲 構造 LClosure 有兩種可能的途徑。
1. LClosure 一般在 Lua VM 運行過程中被動態的構造出來。這時候,LClosure 需要引用 upvalue 都在當前的數據棧上。利用luaF_findupval 函數可以把數據棧上的 TValue 值轉換爲 upvalue。

luaF_findupval 函數邏輯:先在當前的openupval 鏈表中尋找是否已經轉換過,如果已經存在則複用。反之就構造一個新的 UpVal 對象,並將它鏈接到 openupval 鏈表中。

    UpVal *luaF_findupval (lua_State *L, StkId level) {
      UpVal **pp = &L->openupval;
      UpVal *p;
      UpVal *uv;
      lua_assert(isintwups(L) || L->openupval == NULL);
      while (*pp != NULL && (p = *pp)->v >= level) {
        lua_assert(upisopen(p));
        if (p->v == level)  /* found a corresponding upvalue? */
          return p;  /* return it */
        pp = &p->u.open.next;
      }
      /* not found: create a new upvalue */
      uv = luaM_new(L, UpVal);
      uv->refcount = 0;
      uv->u.open.next = *pp;  /* link it to list of open upvalues */
      uv->u.open.touched = 1;
      *pp = uv;
      uv->v = level;  /* current value lives in the stack */
      if (!isintwups(L)) {  /* thread not in list of threads with upvalues? */
        L->twups = G(L)->twups;  /* link it to the list */
        G(L)->twups = L;
      }
      return uv;
    }

 當離開一個代碼塊後,這個代碼塊中定義的局部變量就變爲不可見的。Lua 會調整數據棧指針,銷燬掉這些變量。若這些棧值還被某些閉包 以 open 狀態 的 upvalue 的形式引用,就需要把它們關閉。

luaF_close 函數邏輯:先將當前 UpVal 從 L->openipval 鏈表中剔除掉,然後判斷 當前UpVal ->refcount 查看是否還有被其他閉包 引用, 如果refcount == 0 則釋放UpVal 結構;如果還有引用則需要 把數據(uv->v 這時候在數據棧上)從數據棧上 copy 到 UpVal 結構中的 (uv->u.value)中,最後修正 UpVal 中的指針 v(uv->v 現在指向UpVal 結構中  uv->u.value  所在地址)。

void luaF_close (lua_State *L, StkId level) {
  UpVal *uv;
  while (L->openupval != NULL && (uv = L->openupval)->v >= level) {
    lua_assert(upisopen(uv));
    L->openupval = uv->u.open.next;  /* remove from 'open' list */
    if (uv->refcount == 0)  /* no references? */
      luaM_free(L, uv);  /* free upvalue */
    else {
      setobj(L, &uv->u.value, uv->v);  /* move value to upvalue slot */
      uv->v = &uv->u.value;  /* now current value lives here */
      luaC_upvalbarrier(L, uv);
    }
  }
}
  1. Lua 運行時,加載一段Lua 代碼,也會生成 LClosure 。每段Lua 代碼都會被編譯成一個 函數函數原型Proto,但Lua 的Public API 是不返回 Proto 對象的(只在 Lua VM 中消費),而是將這個 Proto 包裝成一個 LClosure 對象返回。 如果從源代碼加載的話,不可能有用戶構建出來的 upvalue。 但是任何一個代碼塊都至少有一個 upvalue (_ENV 表, 必須對每一個Lua 函數可見,所以被放在Lua 代碼塊的第一個 upvalue 中)。

    static void f_parser (lua_State *L, void *ud) {
    LClosure *cl;
    struct SParser p = cast(struct SParser , ud);
    int c = zgetc(p->z); /* read first character */
    if (c == LUA_SIGNATURE[0]) {
    checkmode(L, p->mode, “binary”);
    cl = luaU_undump(L, p->z, &p->buff, p->name);
    }
    else {
    checkmode(L, p->mode, “text”);
    cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c);
    }
    lua_assert(cl->nupvalues == cl->p->sizeupvalues);
    luaF_initupvals(L, cl);
    }

這些upvalue 並不是來至數據棧,所有用 luaF_initupvals 函數將這些 upvalue 填充到這個 LClosure,這時候這些 UpVal 一定是 closed 狀態。

void luaF_initupvals (lua_State *L, LClosure *cl) {
  int i;
  for (i = 0; i < cl->nupvalues; i++) {
    UpVal *uv = luaM_new(L, UpVal);
    uv->refcount = 1;
    uv->v = &uv->u.value;  /* make it closed */
    setnilvalue(uv->v);
    cl->upvals[i] = uv;
  }
}

C 閉包

和Lua 閉包不同,在C 函數中不會去引用外層函數中的局部變量。所以 C 閉包中的 upvalue 一定是 closed 狀態。 Lua 也就不需要用單獨的UpVal 對象來引用它們。

所以,對於 C 閉包,upvalue 是直接存放在 CClosure 結構體中的。

#define ClosureHeader \
    CommonHeader; lu_byte nupvalues; GCObject *gclist

typedef struct CClosure {
  ClosureHeader;
  lua_CFunction f;
  TValue upvalue[1];  /* list of upvalues */
} CClosure;

C 閉包的創建很簡單 luaF_newCclosure 完成構造操作。

CClosure *luaF_newCclosure (lua_State *L, int n) {
  GCObject *o = luaC_newobj(L, LUA_TCCL, sizeCclosure(n));
  CClosure *c = gco2ccl(o);
  c->nupvalues = cast_byte(n);
  return c;
}

輕量 C 函數 LUA_TLCF_

當 C 閉包一個 upvalue 都沒有的時候,直接用 C 函數指針表達,而且相同的C 函數 是可以複用的,不必每次構造出來時都生成一個 新的 CClosure 對象。

所以 light C 函數不需要 GC 管理內存,自然也不需要創建出 CClosure對象。

LUA_API void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n) {
  lua_lock(L);
  if (n == 0) {
    setfvalue(L->top, fn);
  }
  else {
    CClosure *cl;
    api_checknelems(L, n);
    api_check(n <= MAXUPVAL, "upvalue index too large");
    luaC_checkGC(L);
    cl = luaF_newCclosure(L, n);
    cl->f = fn;
    L->top -= n;
    while (n--) {
      setobj2n(L, &cl->upvalue[n], L->top + n);
      /* does not need barrier because closure is white */
    }
    setclCvalue(L, L->top, cl);
  }
  api_incr_top(L);
  lua_unlock(L);
}

對於 LUA_TLCF 函數,直接把函數指針用宏 setfvalue 壓入堆棧即可。

define setfvalue(obj,x) \
  { TValue *io=(obj); val_(io).f=(x); settt_(io, LUA_TLCF); }
發佈了198 篇原創文章 · 獲贊 36 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章