Lua源碼分析 -- 對象表示

http://blog.csdn.net/INmouse/article/details/1540424

Lua源碼分析 -- 對象表示

Lua是動態類型的語言, 即是說類型附着於值而不變量[1]. Lua的八種基本類型空, 布爾, 數值, 字符串, 表, 函數和用戶數據. 所有類似的值都是虛擬機的第一類值. Lua 解釋器將其表示成爲標籤聯合(tagged union). 如下面代碼示例所示:

lobject.h : 56
/*
** Union of all Lua values
*/
typedef union {
    GCObject*gc;
    void *p;
    lua_Numbern;
    int b;
} Value;

/*
** Tagged Values
*/

#define TValuefields Valuevalue; int tt

typedef struct lua_TValue {
   TValuefields;
} TValue;

lstate.h : 132
/*
** Union of all collectableobjects
*/
union GCObject {
    GCheadergch;
    unionTString ts;
    unionUdata u;
    unionClosure cl;
    structTable h;
    structProto p;
    structUpVal uv;
    structlua_State th; /* thread */
};

lobject.h : 39
/*
** Common Header for allcollectable objects (in macro form, to be
** included in other objects)
*/
#define CommonHeader GCObject*next; lu_byte tt; lu_byte marked


/*
** Common header in structform
*/
typedef struct GCheader {
   CommonHeader;
} GCheader;

首先看到的一個TValue結構,它是由一個Value類型的字段value和int類型字段tt組成,它由於一個宏定義出來.很顯然,這裏的tt就是用於表示這個值的類型,這也是之前所說的,Lua的類型是附着於值上的原因.

接 下來,再打量打量Value的定義,它被定義爲union.這樣做的目的是讓這一個類型可以表示多個類型.從這個定義中可以看出這樣一點:Lua的值可以 分成兩類,第一類是可以被垃圾回收機制回收的對象,它們統一使用GCObject的指針來表示;另一類是原始類型,直接使用C語言的類型來表示相應類型, 如:用void *來表示lightuesrdata, 用lua_Number來表示數值,用int來表示boolean.這裏需要注意的是lua_Number是在如下兩個文件定義出來的.由於Lua是易於 嵌入的語言,在某些特定的環境下,所有數值都用雙精度浮點來表示並不合適,因此,在Lua的配置文件上使用宏來定義數值類型.這使得要改變Lua的數值類 型變得非常簡單.

lua.h:98
/* type of numbers in Lua */
typedef LUA_NUMBERlua_Number;

luaconf.h:504
#define LUA_NUMBER double

接 下來繼續看GCObject的定義,這個類型中的字段在這裏並不做詳細展開,只是說明是用於表示什麼類型的.TString,UData,Table, lua_State分別用於表示字符串,用戶數據,表和協程.而Closure,Proto,UpVal都是用於表示第一類的函數的. 基於棧的,詞法定界的第一類函數在實現上是有一些難度的,看看如下代碼:


functionfoo()
   local a
   return function() return a end
end


由 於Lua是詞法定界的,局部變量a只在函數foo中有效,所以它可以保存在foo的棧中,因此當foo執行完畢a而就隨着棧的銷燬而成爲垃圾; 但問題是foo返回的函數還在引用着它, 這個函數會在棧銷燬後繼續存在,當它返回a的時候又拿什麼返回呢? 這個問題將在函數的實現中介紹. 這也是爲什麼實現函數用了三個類型的原因.

另外, 這些類型的開頭都是GCHeader, 它的所有字段由宏CommonHeader給出來了. 字段next說明可回收對象是可以放到鏈表中去的, 而marked是在GC中用於標記的. 具體的GC算法在這一章就不做介紹了.

值得注意是在CommonHeader中還有一個tt用於表示值的類型, 在TValue中不是有一個嗎? 這樣數據不是冗餘了? 我是這樣看這個問題的:
第一: TValue是所有值的集合, 而GC中如果每個對象都要判斷是否是可回收的, 必然會非常影響效率, 因此將GCObject獨立出來. 可以省去這一層判斷.
第二: 對於基本類型來說, 所需要的空間相對較小, 如果將複雜的對象也做爲一union放在一起, 就會使得空間效率低,因此在TValue中只使用了一個指針來表示GCObject.
這樣在GC對看到的對象就不再是TValue了,所以對應的類型標識也不在了,所以在CommonHeader中加了一個字段來表示類型.

最後,給出一副圖來表於Lua的內存表示:

 

Lua源碼分析 -- 虛擬機

Lua首先將源程序編譯成爲字節碼,然後交由虛擬機解釋執行.對於每一個函數,Lua的編譯器將創建一個原型(prototype),它由一組指令及其使用到的常量組成[1].最初的Lua虛擬機是基於棧的.到1993年,Lua5.0版本,採用了基於寄存器的虛擬機,使得Lua的解釋效率得到提升,

體系結構與指令系統

與虛擬機和指令相關的文件主要有兩個: lopcodes.c 和lvm.c. 從名稱可以看出來,這兩個文件分別用於描述操作碼(指令)和虛擬機.
首先來看指令:
Lua共有38條指令,在下面兩處地方分別描述了這些指令的名稱和模式, 如下:
lopcodes.c:16
const char*const luaP_opnames[NUM_OPCODES+1] = {
  "MOVE",
  "LOADK",
  "LOADBOOL",
  "LOADNIL",
  "GETUPVAL",
  "GETGLOBAL",
  "GETTABLE",
  "SETGLOBAL",
  "SETUPVAL",
  "SETTABLE",
  "NEWTABLE",
  "SELF",
  "ADD",
  "SUB",
  "MUL",
  "DIV",
  "MOD",
  "POW",
  "UNM",
  "NOT",
  "LEN",
  "CONCAT",
  "JMP",
  "EQ",
  "LT",
  "LE",
  "TEST",
  "TESTSET",
  "CALL",
  "TAILCALL",
  "RETURN",
  "FORLOOP",
  "FORPREP",
  "TFORLOOP",
  "SETLIST",
  "CLOSE",
  "CLOSURE",
  "VARARG",
  NULL
};

#define opmode(t,a,b,c,m) (((t)<<7) | ((a)<<6) | ((b)<<4) |((c)<<2) | (m))

const lu_byte luaP_opmodes[NUM_OPCODES] = {
/*       T  A   B       C    mode           opcode   */
  opmode(0, 1, OpArgR, OpArgN, iABC)        /* OP_MOVE */
 ,opmode(0, 1, OpArgK, OpArgN, iABx)       /* OP_LOADK */
 ,opmode(0, 1, OpArgU, OpArgU, iABC)       /* OP_LOADBOOL */
 ,opmode(0, 1, OpArgR, OpArgN, iABC)       /* OP_LOADNIL */
 ,opmode(0, 1, OpArgU, OpArgN, iABC)       /* OP_GETUPVAL */
 ,opmode(0, 1, OpArgK, OpArgN, iABx)       /* OP_GETGLOBAL */
 ,opmode(0, 1, OpArgR, OpArgK, iABC)       /* OP_GETTABLE */
 ,opmode(0, 0, OpArgK, OpArgN, iABx)       /* OP_SETGLOBAL */
 ,opmode(0, 0, OpArgU, OpArgN, iABC)       /* OP_SETUPVAL */
 ,opmode(0, 0, OpArgK, OpArgK, iABC)       /* OP_SETTABLE */
 ,opmode(0, 1, OpArgU, OpArgU, iABC)       /* OP_NEWTABLE */
 ,opmode(0, 1, OpArgR, OpArgK, iABC)       /* OP_SELF */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)       /* OP_ADD */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)       /* OP_SUB */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)       /* OP_MUL */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)       /* OP_DIV */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)       /* OP_MOD */
 ,opmode(0, 1, OpArgK, OpArgK, iABC)       /* OP_POW */
 ,opmode(0, 1, OpArgR, OpArgN, iABC)       /* OP_UNM */
 ,opmode(0, 1, OpArgR, OpArgN, iABC)       /* OP_NOT */
 ,opmode(0, 1, OpArgR, OpArgN, iABC)       /* OP_LEN */
 ,opmode(0, 1, OpArgR, OpArgR, iABC)       /* OP_CONCAT */
 ,opmode(0, 0, OpArgR, OpArgN, iAsBx)       /* OP_JMP */
 ,opmode(1, 0, OpArgK, OpArgK, iABC)       /* OP_EQ */
 ,opmode(1, 0, OpArgK, OpArgK, iABC)       /* OP_LT */
 ,opmode(1, 0, OpArgK, OpArgK, iABC)       /* OP_LE */
 ,opmode(1, 1, OpArgR, OpArgU, iABC)       /* OP_TEST */
 ,opmode(1, 1, OpArgR, OpArgU, iABC)       /* OP_TESTSET */
 ,opmode(0, 1, OpArgU, OpArgU, iABC)       /* OP_CALL */
 ,opmode(0, 1, OpArgU, OpArgU, iABC)       /* OP_TAILCALL */
 ,opmode(0, 0, OpArgU, OpArgN, iABC)       /* OP_RETURN */
 ,opmode(0, 1, OpArgR, OpArgN, iAsBx)       /* OP_FORLOOP */
 ,opmode(0, 1, OpArgR, OpArgN, iAsBx)       /* OP_FORPREP */
 ,opmode(1, 0, OpArgN, OpArgU, iABC)       /* OP_TFORLOOP */
 ,opmode(0, 0, OpArgU, OpArgU, iABC)       /* OP_SETLIST */
 ,opmode(0, 0, OpArgN, OpArgN, iABC)       /* OP_CLOSE */
 ,opmode(0, 1, OpArgU, OpArgN, iABx)       /* OP_CLOSURE */
 ,opmode(0, 1, OpArgU, OpArgN, iABC)       /* OP_VARARG */
};

前面一個數組容易理解, 表示了每條指令的名稱. 後面一個數組表示的是指令的模式. 奇怪的符號讓人有些費解. 在看模式之前, 首先來看Lua指令的格式:

如上圖,Lua的指令可以分成三種形式. 即在上面的模式數組中也可以看到的iABC, iABx 和 iAsBx. 對於三種形式的指令來說, 前兩部分都是一樣的, 分別是6位的操作碼和8位A操作數; 區別在於, 後面部是分割成爲兩個長度爲9位的操作符(B, C),一個無符號的18位操作符Bx還是有符號的18位操作符sBx. 這些定義的代碼如下:
lopcodes.c : 34
/*
** size and position of opcode arguments.
*/
#define SIZE_C        9
#define SIZE_B        9
#define SIZE_Bx        (SIZE_C + SIZE_B)
#define SIZE_A        8

#define SIZE_OP        6

#define POS_OP        0
#define POS_A        (POS_OP + SIZE_OP)
#define POS_C        (POS_A + SIZE_A)
#define POS_B        (POS_C + SIZE_C)
#define POS_Bx        POS_C

再來看指令的操作模式, Lua使用一個字節來表示指令的操作模式. 具體的含義如下:
1.使用最高位來表示是否是一條測試指令. 之所以將這一類型的指令特別地標識出來, 是因爲Lua的指令長度是32位,對於分支指令來說, 要想在這32位中既表示兩個操作數來做比較, 同時還要表示一個跳轉的地址, 是很困難的. 因此將這種指令分成兩條, 第一條是測試指令, 緊接着一條無條件跳轉. 如果判斷條件成立則將PC(Program Counter, 指示下一條要執行的指令)加一, 跳過下一條無條件跳轉指令, 繼續執行; 否則跳轉.
2. 第二位用於表示A操作數是否被設置
3. 接下來的二位用於表示操作數B的格式,OpArgN表示操作數未被使用, OpArgU表示操作數被使用(立即數?), OpArgR表示表示操作數是寄存器或者跳轉的偏移量, OpArgK表示操作數是寄存器或者常量.

最後,給出Lua虛擬機的體系結構圖(根據源代碼分析得出):

首先,我們注意到,Lua的解釋器還是一個以棧爲中心的結構. 在lua_State這個結構中,有許多個字段用於描述這個結構.stack用於指向絕對棧底, 而base指向了當前正在執行的函數的第一個參數, 而top指向棧頂的第一個空元素.
我們可以看到,這個體系結構中並沒有獨立出來的寄存器. 從以下代碼來看:

lvm.c:343
#define RA(i)   (base+GETARG_A(i))
/* to be used after possible stack reallocation */
#define RB(i)    check_exp(getBMode(GET_OPCODE(i)) == OpArgR,base+GETARG_B(i))
#define RC(i)    check_exp(getCMode(GET_OPCODE(i)) == OpArgR,base+GETARG_C(i))
#define RKB(i)    check_exp(getBMode(GET_OPCODE(i)) == OpArgK, /
    ISK(GETARG_B(i)) ? k+INDEXK(GETARG_B(i)) : base+GETARG_B(i))
#define RKC(i)    check_exp(getCMode(GET_OPCODE(i)) == OpArgK, /
    ISK(GETARG_C(i)) ? k+INDEXK(GETARG_C(i)) : base+GETARG_C(i))
#define KBx(i)    check_exp(getBMode(GET_OPCODE(i)) == OpArgK,k+GETARG_Bx(i))

當指令操作數的類型是寄存器時,它的內容是以base爲基址在棧上的索引值.如圖所示.寄存器實際是base之上棧元素的別名;當指令操作數的類型的常數時, 它首先判斷B操作數的最位是否爲零.如果是零,則按照和寄存器的處理方法一樣做,如果不是零,則在常數表中找相應的值.
我們知道Lua中函數的執行過程是這樣的. 首先將函數壓棧,然後依次將參數壓棧,形成圖中所示的棧的內容. 因此R[0]到R[n]也分別表示了Arg[1]到Arg[N+1].在第一個參數之下,就是當前正在執行的函數,對於Lua的函數(相對C函數)來說,它是指向類型爲 Prototype的TValue, 在Prototype中字段code指向了一個數組用來表示組成這個函數的所有指令,字段k指向一個數組來表示這個函數使用到的所有常量.最後,Lua在解釋執行過程中有專門的變量pc來指向下一條要執行的指令.

指令解釋器

有了前面對指令格式和體系結構的介紹,現在我們可以進入正題, 來看看Lua的指令是如何執行的了.主函數如下:
lvm.c:373
void luaV_execute (lua_State *L, int nexeccalls){
  LClosure *cl;
  StkId base;
  TValue *k;
  const Instruction *pc;
 reentry:  /* entry point */
  lua_assert(isLua(L->ci));
  pc = L->savedpc;
  cl = &clvalue(L->ci->func)->l;
  base = L->base;
  k = cl->p->k;
這是最開始的初始化過程.其中, pc被初始化成爲了L->savedpc,base被初始化成爲了L->base, 即程序從L->savedpc開始執行(在下一篇專題中,將會介紹到 L->savedpc在函數調用的預處理過程中指向了當前函數的code),而L->base指向棧中當前函數的下一個位置.cl表示當前正在執行閉包(當前可以理解成爲函數),k指向當前閉包的常量表.
接下來(注意,爲了專注主要邏輯, 我將其中用於Debugger支持,斷言等代碼省略了):
  /* main loop of interpreter */
  for (;;) {
    const Instruction i = *pc++;
    StkId ra;
    /* 省略Debugger支持和Coroutine支持*/
    /* warning!! several calls may realloc the stack andinvalidate `ra' */
    ra = RA(i);
    /* 省略斷言 */
    switch (GET_OPCODE(i)) {
進 入到解釋器的主循環,處理很簡單,取得當前指令,pc遞增,初始化ra,然後根據指令的操作碼進行選擇. 接下來的代碼是什麼樣的, 估計大家都能想到,一大串的case來指示每條指令的執行.具體的實現可以參考源碼, 在這裏不對每一條指令展開, 只是對其中有主要的幾類指令進行說明:

傳值類的指令,與MOVE爲代表:
lvm.c:403
      case OP_MOVE: {
        setobjs2s(L, ra, RB(i));
        continue;
      }
lopcodes:154
OP_MOVE,/*    A B    R(A) :=R(B)                   */
lobject.h:161
#define setobj(L,obj1,obj2) /
  { const TValue *o2=(obj2); TValue *o1=(obj1); /
    o1->value = o2->value; o1->tt=o2->tt; /
    checkliveness(G(L),o1); }


/*
** different types of sets, according to destination
*/

/* from stack to (same) stack */
#define setobjs2s    setobj
從註釋來看,這條指令是將操作數A,B都做爲寄存器,然後將B的值給A. 而實現也是簡單明瞭,只使用了一句. 宏展開以後, 可以看到, R[A],R[B]的類型是TValue, 只是將這兩域的值傳過來即可. 對於可回收對象來說,真實值不會保存在棧上,所以只是改了指針,而對於非可回收對象來說,則是直接將值從R[B]賦到R[A].

數值運算類指令,與ADD爲代表:
lvm.c:470
      case OP_ADD: {
        arith_op(luai_numadd, TM_ADD);
        continue;
      }
lvm.c:360
#define arith_op(op,tm) { /
        TValue *rb = RKB(i); /
        TValue *rc = RKC(i); /
        if (ttisnumber(rb) &&ttisnumber(rc)) { /
          lua_Number nb =nvalue(rb), nc = nvalue(rc); /
          setnvalue(ra, op(nb,nc)); /
        } /
        else /
          Protect(Arith(L, ra, rb,rc, tm)); /
      }
lopcodes.c:171
OP_ADD,/*    A B C    R(A) := RK(B) +RK(C)               */
如果兩個操作數都是數值的話,關鍵的一行是:
setnvalue(ra,op(nb,nc));
即兩個操作數相加以後,把值賦給R[A].值得注意的是,操作數B,C都是RK, 即可能是寄存器也可能是常量,這最決於最B和C的最高位是否爲1,如果是1,則是常量,反之則是寄存器.具體可以參考宏ISK的實現.
如果兩個操作數不是數值,即調用了Arith函數,它嘗試將兩個操作轉換成數值進行計算,如果無法轉換,則使用元表機制.該函數的實現如下:
lvm.c:313
static void Arith (lua_State *L, StkId ra, const TValue *rb,
                  const TValue *rc, TMS op) {
  TValue tempb, tempc;
  const TValue *b, *c;
  if ((b = luaV_tonumber(rb, &tempb)) != NULL &&
      (c = luaV_tonumber(rc, &tempc)) != NULL) {
    lua_Number nb = nvalue(b), nc = nvalue(c);
    switch (op) {
      case TM_ADD: setnvalue(ra, luai_numadd(nb, nc));break;
      case TM_SUB: setnvalue(ra, luai_numsub(nb, nc));break;
      case TM_MUL: setnvalue(ra, luai_nummul(nb, nc));break;
      case TM_DIV: setnvalue(ra, luai_numdiv(nb, nc));break;
      case TM_MOD: setnvalue(ra, luai_nummod(nb, nc));break;
      case TM_POW: setnvalue(ra, luai_numpow(nb, nc));break;
      case TM_UNM: setnvalue(ra, luai_numunm(nb));break;
      default: lua_assert(0); break;
    }
  }
  else if (!call_binTM(L, rb, rc, ra, op))
    luaG_aritherror(L, rb, rc);
}
在上面call_binTM用於調用到元表中的元方法,因爲在Lua以前的版本中元方法也被叫做tag method, 所以函數最後是以TM結尾的.
lvm:163
static int call_binTM (lua_State *L, const TValue *p1, const TValue *p2,
                      StkId res, TMS event) {
  const TValue *tm = luaT_gettmbyobj(L, p1, event);  /* try firstoperand */
  if (ttisnil(tm))
    tm = luaT_gettmbyobj(L, p2, event);  /* try secondoperand */
  if (!ttisfunction(tm)) return 0;
  callTMres(L, res, tm, p1, p2);
  return 1;
}
在 這個函數中,試着從二個操作數中找到其中一個操作數的元方法(第一個操作數優先), 這裏event表示具體哪一個元方法,找到了之後,再使用函數callTMres()去調用相應的元方法. callTMres()的實現很簡單,只是將元方法,第一,第二操作數先後壓棧,再調用並取因返回值.具體如下:
lvm.c:82
static void callTMres (lua_State *L, StkId res, const TValue *f,
                       const TValue *p1, const TValue *p2) {
  ptrdiff_t result = savestack(L, res);
  setobj2s(L, L->top, f);  /* push function */
  setobj2s(L, L->top+1, p1);  /* 1st argument */
  setobj2s(L, L->top+2, p2);  /* 2nd argument */
  luaD_checkstack(L, 3);
  L->top += 3;
  luaD_call(L, L->top - 3, 1);
  res = restorestack(L, result);
  L->top--;
  setobjs2s(L, res, L->top);
}

邏輯運算類指令,與EQ爲代表:
lvm.c:541
      case OP_EQ: {
        TValue *rb = RKB(i);
        TValue *rc = RKC(i);
        Protect(
          if (equalobj(L, rb, rc)== GETARG_A(i))
            dojump(L,pc, GETARG_sBx(*pc));
        )
        pc++;
        continue;
      }
lopcodes.c:185
OP_EQ,/*    A B C    if ((RK(B) == RK(C)) ~= A)then pc++        */
在 這條指令實現的過程中,equalobj與之前的算術運算類似,讀者可以自行分析.關鍵看它是如果實現中跳轉的,如果RK[B]==RK[C]並且A爲1的情況下(即條件爲真),則會使用pc取出下一條指令,調用dojump進行跳轉,否則pc++,掛空緊接着的無條件跳轉指令. dojump的實現如下:
lvm.c:354
#define dojump(L,pc,i)    {(pc) += (i); luai_threadyield(L);}
luai_threadyield只是順序地調用lua_unlock和lua_lock,這裏爲釋放一次鎖,使得別的線程可以得到調度.

函數調用類指令,與CALL爲代表:
lvm.c:582
      case OP_CALL: {
        int b = GETARG_B(i);
        int nresults = GETARG_C(i) - 1;
        if (b != 0) L->top = ra+b; /* else previous instruction set top */
        L->savedpc = pc;
        switch (luaD_precall(L, ra,nresults)) {
          case PCRLUA: {
           nexeccalls++;
            gotoreentry;  /* restart luaV_execute over new Lua function */
          }
          case PCRC: {
            /* it was aC function (`precall' called it); adjust results */
            if (nresults>= 0) L->top = L->ci->top;
            base =L->base;
            continue;
          }
          default: {
           return;  /* yield */
          }
        }
      }
lopcodes.c:192
OP_CALL,

/*   A B C    R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */
這一條指令將在下一個介紹Lua函數調用規範的專題中詳細介紹. 在這裏只是簡單地說明CALL指令的R[A]表示的是即將要調用的函數,而B和C則分別表示參數個數加1,和返回值個數加1. 之所以這裏需要加1,其原因是:B和C使用零來表示變長的參數和變長的返回值,而實際參數個數就向後推了一個.

指令的介紹就先到此爲止了, 其它的指令的實現也比較類似.仔細閱讀源碼就可很容易地分析出它的意義來. 下一篇將是一個專題, 詳細地介紹Lua中函數的調用是如何實現的.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章