之前看得比较多的都是OOP的源码,像C这种面向过程的代码看得不多,这次正好学习一下面向过程的最佳实践方法。
宏没有类型系统,再加上各种缩写,看得头痛
Lua允许在创建虚拟机时(调用lua_newstate),提供自定义内存分配函数,接口如下:void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize)。当编写自定义的函数时,接口的实参为我们提供了以下信息(或者说要求):
ud 虚拟机的堆指针,允许我们工作在不同的堆上,避免线程安全问题。如何使用?ptr 已分配的内存指针,可以为NULL。osize 已分配内存块的大小,这样就不用在内存前面加一个cookie来记录大小。nsize要分配的大小,为0则释放ptr的内存这个分配函数会在虚拟机分配内存、释放内存、扩展内存时被调用,根据不同操作传入的实参不同,来判断应该进行哪种处理。默认的内存分配函数是l_alloc,封装了::free和::realloc:
static void *l_alloc (void *ud, void *ptr, size_t osize, size_t nsize) {
(void)ud;
(void)osize;
if (nsize == 0) {
free(ptr);
return NULL;
}
else
return realloc(ptr, nsize);
}
内存分配模块(lmem.h/c)相关的接口如下:
luaM_realloc_ 调用虚拟机的内存分配函数global_State::frealloc(在创建虚拟机时设置),修改虚拟机的已分配空间计数global_State::totalbytesluaM_free*、luaM_new*、luaM_malloc 都是调用luaM_realloc_luaM_growaux_ 分配大小为原来的两倍的空间并返回luaM_growvector 调用luaM_growaux_多态的实现方式
// 1. 这种方式在python源码也见过
struct base { int type };
struct derive {
struct base info;
...
};
// 2. 使用union
struct deriveA { ... };
struct deriveB { ... };
struct base {
int type;
union {
deriveA d_a;
deriveB d_b;
} value;
};
每个需要GC的类型都会以CommonHeader宏作为起始,宏定义如下:#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked。定义中成员的含义:next下一个链表成员、tt数据类型、markedGC标志位。
用GCHeader包装CommonHeader。用意何在?答:没有的话,会把三个成员都直接放到GCObject中,三个成员变互斥了
用GCObject联合体把所有需要GC的类型囊括,为什么需要gch这个字段,调用具体类型的CommonHeader不也可以?
union GCObject {
GCHeader gch;
union TString ts;
...
};
用Value联合体把所有类型囊括。用TValue封装Value,CommonHeader的tt和TValuefields的tt重复了?
#define TValuefields Value value; int tt
typedef struct lua_TValue {
TValuefields;
} TValue;
字符串使用union进行定义,并加入union { double u; void *s; long l;};。这是为了对齐。
typedef union TString {
union { double u; void *s; long l;};
struct {
CommonHeader;
...
} tsv;
} TString;
Lua有个字符串对象池(stringtable global_State::strt,用开链表实现),里面的对象没被引用则被回收,关键字除外。stringtable实现如下:
typedef struct stringtable {
GCObject **hash;
lu_int32 nuse; /* number of elements */
int size;
} stringtable;
字符串相关接口如下:
luaS_resize哈希表修改大小,然后再哈希。通过遍历GCHeader::next挨个修改旧哈希表中的元素,头插法进入新哈希表对应索引的链表。使用接口(luaM_*)分配新哈希表空间和销毁旧哈希表。当处于GCSsweepstring状态,则不会改变大小luaS_newlstr计算字符串哈希值,然后查找对象池,如果对象池中有则返回该对象,否则调用newlstr。对于长字符串,计算哈希值不是逐位,而是有步长的step=(l>>5)+1。对于没被引用但还没回收的对象,也能直接复用,需要修改对应位。newlstr 为TString分配空间,字符串直接存放在TString后面:memcpy(ts+1, str, l*sizeof(char))。然后插入对象池,可能会触发luaS_resize长字符串逐位计算哈希的方法可能带来Hash Dos的风险,在5.2.1时引入随机种子进行修复。
表定义如下:
typedef union TKey {
struct {
TValuefields;
struct Node *next;
} nk;
TValue tvk;
} TKey;
typedef struct Node {
TValue i_val;
TKey i_key;
} Node;
typedef struct Table {
CommonHeader;
TValue *array;
Node *node;
...
} Table;
整数数据可以存在array指向的数组中,也可以存在node指向的开放寻址法哈希表中。非整数类型全部存在哈希表中。TKey中的nk主要是用来当Key的哈希值相同时,开链用。
相关接口如下:
luaH_getstr 计算哈希,然后从node指向哈希表中节点next链中查找,找到则返回该&Node::i_val。luaH_getnum key的值小于array数组大小且大于1,则返回&array[key-1],否则同luaH_getstrluaH_get LUA_TSTRING调用luaH_getstr。LUA_TNUMBER且是int调用luaH_getnum。否则在哈希表中查找luaH_setstr、luaH_setnum 调用luaH_get*进行查找,找到直接返回&TValue,找不到则创建,调用newkeyluaH_set 同luaH_set*。创建前会检查key是否为nil或NaNnewkey 查找key在哈希表中是否能直接放入,能则放入;否则检查被占用位置上元素A的哈希结果是否就在该位置,是则寻找空闲位置插入新key,并修改A::i_key.nk.next指向空闲位置;否则将A移动到空闲位置,修改A前驱节点的i_key.nk.next,然后原被占用位置插入新key。最后返回新key对应的&TValue。寻找空闲位置的过程可能触发rehash。rehash 计算出新数组和新哈希表的大小,调用resizeluaH_resizearray 哈希表的大小不变,调用resize修改数组大小resize重新分配哈希表和数组的空间,把旧哈希表的元素移到新哈希表。旧数组的元素会丢失?luaH_next查找下一个key,先查数组后查哈希表。这里的下一个,在哈希表中不是指同一个hash桶的下一个,而仅仅只是偏移值增加的下一个。返回的结果是写入栈中,在实参StkId指明的位置写入下一个key的索引,在StkId指明位置的下一个栈位置上写入key对应的valueluaH_getn 返回第一个本身不为空而后一个元素为空的位置。需要连续存放,不能有空洞?
没看到把数据存入数组的操作?typedef struct global_State {
struct Table *mt[NUM_TAGS]; /* metatables for basic types */
TString *tmname[TM_N]; /* array with tag-method names */
} global_State;
typedef struct Table {
...
lu_byte flags;
struct Table *metatable;
} Table;
metatable的键值对是<TString,TValue>。常用的方法名位于global_State::tmname数组,该数组被luaT_init赋值。Table::flags按位来标明该表是否有某个常用的方法。当要查找某元表是否有某个方法,会先调用fasttm进行查找。
luaT_init 为global_State::tmname字符串数组赋值常用的方法名fasttm 宏,检查Table::flags(这里的表是元表)是否有要找的方法名,有则调用luaT_gettm,否则返回NULLluaT_gettm 在实参指向的表(这里的表是元表)中查找方法名对应的方法,如果找不到,则修改Table::flagsluaT_gettmbyobj 如果实参类型是表,则查找Table::metatable;如果是UserData,查找Udata::uv::metatable;否则查找global_State::mt数组中对应类型指向的元表。为了能够实现嵌套的异常,用lua_longjmp封装了::jmp_buf,由previous串起了异常链。而lua_State::errorJmp提供一个全局变量的作用,用它来记录当前的恢复点。
struct lua_longjmp {
struct lua_longjmp *previous;
luai_jmpbuf b;
volatile int status; /* error code */
};
struct lua_State {
...
struct lua_longjmp *errorJmp; /* current error recover point */
};
try和catch实现方式如下:
#if defined(__cplusplus)
#define LUAI_THROW(L,c) throw(c)
#define LUAI_TRY(L,c,a) try { a } catch(...) \
{ if ((c)->status == 0) (c)->status = -1; }
#else
/* default handling with long jumps */
#define LUAI_THROW(L,c) longjmp((c)->b, 1)
#define LUAI_TRY(L,c,a) if (setjmp((c)->b) == 0) { a }
#endif
提供了luaD_throw和luaD_rawrunprotected接口来搭配使用异常,它们的主要工作是对lua_longjmp *lua_State::errorJmp使用try和catch的宏
luaD_rawrunprotected 向异常链插入新lua_longjmp恢复点(修改lua_State::errorJmp)、调用LUAI_TRY执行实参对应的函数、不抛出异常执行完毕后lua_longjmp恢复为previousluaD_throw 检查lua_State::errorJmp,如果有恢复点则调用LUAI_THROW,否则退出程序luaD_pcall luaD_rawrunprotected异常发生时只是跳出,需要luaD_pcall做栈的恢复。因此先保存栈的相关数据、调用luaD_rawrunprotected。如果捕捉到异常则恢复栈,并调用luaF_close删去栈顶上面(发生异常的函数使用的栈数据)的upvalue,往数据栈推入错误信息,修改调用栈大小Lua虚拟机对象就是一个lua_State,它代表一个线程,有独立的数据栈和调用链。同一个虚拟机的所有执行线程共享一个global_State
lua_newstate 创建虚拟机。根据传入的自定义内存分配函数(默认是l_alloc),为lua_State和global_State分配空间,然后进行一系列初始化,在有异常保护情况下调用f_luaopenf_luaopen 初始化数据栈调用栈、全局表、寄存器、字符串对象池、常用元方法名数组、关键字字符串等luaE_newthread 和lua_newstate大致相同,进行一系列初始化。typedef TValue *StkId;
struct lua_State {
...
StkId stack_last;
StkId stack;
int stacksize;
};
luaD_growstack 带上新的栈大小作为实参,调用luaD_reallocstackluaD_reallocstack 调用luaM_reallocvector去分配空间,修改lua_State栈相关成员,调用correctstack。怎么没看到把旧栈的值移动到新栈?correctstack 将外部结构对数据栈的引用需要修正为正确的新地址,有lua_State::openupval、所有CallInfo 的成员变量、lua_State::base和lua_State::topluaD_checkstack 宏,检查数据栈能否放下n个元素,否则调用luaD_growstackCallInfo保存正在调用函数的运行状态,调用栈以CallInfo数组的方式存储在lua_State::base_ci中。
struct lua_State {
...
CallInfo *end_ci;
CallInfo *base_ci;
int size_ci;
};
每个CallInfo有相关成员指明该函数在运行时,数据在数据栈的位置:
typedef struct CallInfo {
StkId base;
StkId func;
StkId top;
const Instruction *savedpc;
...
} CallInfo;
同时lua_State也有一些成员指明当前正在运行的CallInfo以及当前栈的位置
struct lua_State {
...
StkId top;
StkId base;
CallInfo *ci;
const Instruction *savedpc;
};
luaD_reallocCI 调用luaM_reallocvector去分配空间,修改lua_State栈相关成员。怎么没看到把旧栈的值移动到新栈?growCI 调用luaD_reallocCI分配两倍大的空间,返回++L->ciinc_ci 宏。如果空间不够则调用growCI,否则返回++L->ciupvalue原始值还在数据栈上,则这个upvalue是open的,否则是close的
typedef struct UpVal {
...
TValue *v;
union {
TValue value;
struct {
struct UpVal *prev;
struct UpVal *next;
} l;
} u;
} UpVal;
当处于代码块中,UpVal::v指向数据栈上的对象,UpVal::u::l为串联UpValue的指针。当离开代码块,栈上对象被销毁,由UpVal::u::value保存原数据栈上的对象,UpVal::v指向UpVal::u::value
闭包有两种:
typedef struct CClosure {
ClosureHeader;
lua_CFunction f;
TValue upvalue[1];
} CClosure;
typedef struct LClosure {
ClosureHeader;
struct Proto *p;
UpVal *upvals[1];
} LClosure;
typedef union Closure {
CClosure c;
LClosure l;
} Closure;
CClosure的upvalue是直接存放在结构体中的。
struct lua_State {
...
GCObject *openupval; /* list of open upvalues in this stack */
};
typedef struct global_State {
...
UpVal uvhead; /* head of double-linked list of all open upvalues */
} global_State;
lua_State::openupval 是当前线程的open upvalues单向链表(链表顺序按照数据栈从高到低?),global_State::uvhead是全部open upvalues的双向链表
luaF_findupval 在openupval查找地址等于实参的UpValue,找到则返回,否则创建并插入openupval和uvhead。luaF_freeupval 调用luaM_free。如果UpValue是open,则需要从global_State::uvhead移除自己luaF_close 删掉地址高于实参的无人引用的UpValue、把数据从数据栈上复制到UpVal::u::value结构,并修正UpVal::v
lua闭包有两种生成方法,一种在虚拟机运行的过程中被动态构造出来的。这时,闭包需引用的upvalue都在当前的数据栈上。利用luaF_findupval可以把数据栈上的值转换为upvalue。另一种 TODO:yunfeng
luaD_call 增加调用函数数量lua_State::nCcalls、调用luaD_precall、如果是lua函数则调用luaV_execute、减少lua_State::nCcallstryfuncTM 调用luaT_gettmbyobj查找实参类型的__call方法,在数据栈中存放实参对象的位置之前插入该方法(对象自身作为第一个参数传入),并返回。adjust_varargs 如果传入的参数小于固定参数数,会把不足的补为nil。把存于上一级数据栈帧尾部的,被调用函数的固定参数,都移动到新数据栈帧。
函数接收变长参数时,这部分的参数是放在上一级数据栈帧尾部的,需要将固定参数复制到被调用的函数的新一级数据栈帧上,而变长参数留在原地。
luaD_precall 如果实参不是函数,则调用tryfuncTM尝试获得可调用的元方法。接着判断是不是C函数,不是C函数的话会检查是否是可变参数,是可变参数的话需要调用adjust_varargs,然后初始化新的callinfo。如果是C函数的话,也是初始化新的callinfo,然后直接调用该函数(n = (*curr_func(L)->c.f)(L);),如果调用结束,会调用luaD_poscallluaD_poscall 结束函数调用,修改相关成员,把返回值从被调用的函数数据栈帧栈压入调用者的数据栈帧?返回值多余的抛弃,不足的补为nil。实参位置在func的前面还是后面?返回值呢?luaD_callhook TODOcallrethooksTODOlua_resumeTODOlua_yieldTODO寄存器式的虚拟机,指令格式及含义见lopcodes.*。
luaV_execute 循环执行指令,虚拟机核心luaV_tostring 调用luaS_new,把数字类型的TValue转成字符串类型luaV_tonumber luaV_tostring的逆操作callTMres 在数据栈上保存函数及实参,调用luaD_call执行保存的函数,把结果存到数据栈上callTM 在数据栈上保存函数及实参,调用luaD_call执行保存的函数luaV_gettable 从实参对象中查找key,如果找不到且该对象元表__index对应的是函数,则执行该函数,在实参对象中查找key(调用callTMres)。如果元表__index对应的不是函数,则循环上面的步骤(对象中查找key、调用__index对应的函数)luaV_settable 过程同luaV_gettable,只不过查找改成设置,__index改成__newindex,callTMres改成callTMarith_op 如果实参是数字则调用luai_num*,否则转成数字(luaV_tonumber)再调用luai_num*。转换不了则尝试相对应的元方法(call_binTM)。luaV_concat 将数据栈某范围内的对象都以字符串方式连接起来,结果放在该范围位置的下面部分字节码的执行逻辑如下:
OP_LOADBOOL指令有一个参数指示是否需要对pc++,这是因为lua没有==对应的字节码,因此local t = ( a == b )会被翻译成如下:local t
if a == b then
t = true
else
t = false
end
// 1: EQ 1 0 1
// 2: JMP 0 1 ; to 4
// 3: LOADBOOL 0 0 1
// 4: LOADBOOL 0 1 0
有了一个参数让pc++,可以避免额外插入一条JMP指令。
TODO
定义同TString,有个metatable表记录元方法
typedef union Udata {
L_Umaxalign dummy;
struct {
CommonHeader;
struct Table *metatable;
...
} uv;
} Udata;
luaS_newudata 分配空间并初始化,数据和字符串一样,都是紧跟在Udata后面
当C/C++想调用lua中一个值时,lua将数值压入lua虚拟栈中,然后通过lua提供api来读取,当C/C++想向lua传入一个值时,C++通过lua提供的api将数值压入lua虚拟栈中,lua便可进行调用,通过这样交互完成相互调用。也就是说,C/C++并没有直接和lua数据完全的绝对互通,而是通过这个lua虚拟栈作桥梁。一旦数据进入lua那边,就是lua自己维护了,包含lua自己产生的变量或者管理的数据,,C++要想访问只能先让lua将其放到栈上再通过lua api来访问
我们向Lua注册的C函数应该是这种格式的:typedef int (*lua_CFunction) (lua_State *L)
TODO
使用三色增量标记清除算法,白色表示该对象没有被GC标记,灰色表示被GC访问过但其引用的其他对象还没有被访问,黑色表示对象已经被GC访问且其引用的其他对象也被访问。
其中白色又被细分为双白色,即当前白色和非当前白色,两种白色的状态交替使用,只有对象时当前白色才会被回收。
一遍扫描就生成字节码