之前看得比较多的都是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::totalbytes
luaM_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
数据类型、marked
GC标志位。
用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_getstr
luaH_get
LUA_TSTRING
调用luaH_getstr
。LUA_TNUMBER
且是int
调用luaH_getnum
。否则在哈希表中查找luaH_setstr
、luaH_setnum
调用luaH_get*
进行查找,找到直接返回&TValue
,找不到则创建,调用newkey
luaH_set
同luaH_set*
。创建前会检查key
是否为nil
或NaN
newkey
查找key
在哈希表中是否能直接放入,能则放入;否则检查被占用位置上元素A
的哈希结果是否就在该位置,是则寻找空闲位置插入新key
,并修改A::i_key.nk.next
指向空闲位置;否则将A
移动到空闲位置,修改A
前驱节点的i_key.nk.next
,然后原被占用位置插入新key
。最后返回新key
对应的&TValue
。寻找空闲位置的过程可能触发rehash
。rehash
计算出新数组和新哈希表的大小,调用resize
luaH_resizearray
哈希表的大小不变,调用resize
修改数组大小resize
重新分配哈希表和数组的空间,把旧哈希表的元素移到新哈希表。旧数组的元素会丢失?luaH_next
查找下一个key
,先查数组后查哈希表。这里的下一个,在哈希表中不是指同一个hash
桶的下一个,而仅仅只是偏移值增加的下一个。返回的结果是写入栈中,在实参StkId
指明的位置写入下一个key
的索引,在StkId
指明位置的下一个栈位置上写入key
对应的value
luaH_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
,否则返回NULL
luaT_gettm
在实参指向的表(这里的表是元表)中查找方法名对应的方法,如果找不到,则修改Table::flags
luaT_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
恢复为previous
luaD_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_luaopen
f_luaopen
初始化数据栈调用栈、全局表、寄存器、字符串对象池、常用元方法名数组、关键字字符串等luaE_newthread
和lua_newstate
大致相同,进行一系列初始化。typedef TValue *StkId;
struct lua_State {
...
StkId stack_last;
StkId stack;
int stacksize;
};
luaD_growstack
带上新的栈大小作为实参,调用luaD_reallocstack
luaD_reallocstack
调用luaM_reallocvector
去分配空间,修改lua_State
栈相关成员,调用correctstack
。怎么没看到把旧栈的值移动到新栈?correctstack
将外部结构对数据栈的引用需要修正为正确的新地址,有lua_State::openupval
、所有CallInfo
的成员变量、lua_State::base
和lua_State::top
luaD_checkstack
宏,检查数据栈能否放下n
个元素,否则调用luaD_growstack
CallInfo
保存正在调用函数的运行状态,调用栈以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->ci
inc_ci
宏。如果空间不够则调用growCI
,否则返回++L->ci
upvalue
原始值还在数据栈上,则这个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::nCcalls
tryfuncTM
调用luaT_gettmbyobj
查找实参类型的__call
方法,在数据栈中存放实参对象的位置之前插入该方法(对象自身作为第一个参数传入),并返回。adjust_varargs
如果传入的参数小于固定参数数,会把不足的补为nil
。把存于上一级数据栈帧尾部的,被调用函数的固定参数,都移动到新数据栈帧。
函数接收变长参数时,这部分的参数是放在上一级数据栈帧尾部的,需要将固定参数复制到被调用的函数的新一级数据栈帧上,而变长参数留在原地。
luaD_precall
如果实参不是函数,则调用tryfuncTM
尝试获得可调用的元方法。接着判断是不是C
函数,不是C
函数的话会检查是否是可变参数,是可变参数的话需要调用adjust_varargs
,然后初始化新的callinfo
。如果是C
函数的话,也是初始化新的callinfo
,然后直接调用该函数(n = (*curr_func(L)->c.f)(L);
),如果调用结束,会调用luaD_poscall
luaD_poscall
结束函数调用,修改相关成员,把返回值从被调用的函数数据栈帧栈压入调用者的数据栈帧?返回值多余的抛弃,不足的补为nil
。实参位置在func
的前面还是后面?返回值呢?luaD_callhook
TODOcallrethooks
TODOlua_resume
TODOlua_yield
TODO寄存器式的虚拟机,指令格式及含义见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
改成callTM
arith_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访问且其引用的其他对象也被访问。
其中白色又被细分为双白色,即当前白色和非当前白色,两种白色的状态交替使用,只有对象时当前白色才会被回收。
一遍扫描就生成字节码