Lua简介
Lua是一个强大、高效、轻量、可嵌入的脚本编程语言,体积小、速度快,采用ANSI C语言编写,并且开放源代码。主要应用于游戏开发行业,在移动游戏开发领域,主流的引擎如Cocos2d-x、Unity都支持Lua作为脚本语言,以实现快速开发和热更新的功能。
作为一门动态语言,Lua提供了一个虚拟机,程序员创建的代码,需要通过一个代码解释器,最终转换为字节码,然后交由虚拟机解释执行。
Lua的官方版本还提供了一些常用的库,同时还提供了一系列的C API,方便第三方开发者与虚拟机交互,以及实现宿主语言(如C++)和Lua的交互。
作为一名游戏开发者,Lua是一个会经常使用到的语言。要用好一项技术,就一定要做到“知其然、知其所以然”,懂得它的实现原理,才能最大化的发挥它的优势,规避劣势。而要学习一项技术的实现原理,莫过于阅读源代码,它会告诉你最准确的信息和所有的细节。
Lua的源代码可以从其官方网站获得:https://www.lua.org/home.html
Lua中有三种最复杂和重要的数据类型:function、string和table,其中table是用户实现复杂数据结构和面向对象编程的基础。本文就基于当前Lua的最新发布版5.4.3
来介绍table的实现机制,阅读代码时请注意版本差异。
源码结构
下载Lua源码后解压,会发现所有的代码都放在src
文件夹下,其中一些重要文件的介绍如下:
模块 | 文件 | 描述 |
---|---|---|
虚拟机核心 | lapi.c | C API接口 |
lctype.c | C 标准库中的ctype相关实现 | |
ldebug.c | 调试接口 | |
ldo.c | 函数调用及堆栈管理 | |
lfunc.c | 函数原型和闭包 | |
lgc.c | 垃圾回收 | |
lmem.c | 内存管理 | |
lobject.c | 对象操作 | |
lopcodes.c | 字节码定义 | |
lstate.c | 状态机 | |
lstring.c | 字符串池 | |
ltable.c | 表的实现 | |
ltm.c | 元表的实现 | |
lvm.c | 虚拟机 | |
lzio.c | 输入流 | |
内嵌库 | lauxlib.c | 辅助库函数 |
lbaselib.c | 基础库 | |
lcorolib.c | 协程库 | |
ldblib.c | 调试库 | |
linit.c | 初始化内嵌库 | |
liolib.c | IO库 | |
lmathlib.c | 数学库 | |
loadlib.c | 动态扩展库管理 | |
loslib.c | 系统库 | |
lstrlib.c | 字符串库 | |
ltablib.c | 表处理库 | |
源代码解析 | lcode.c | 代码生成器 |
ldump.c | 序列化预编译的字节码 | |
llex.c | 词法分析器 | |
lparser.c | 解释器 | |
lundump.c | 反编译字节码 | |
解释器和编译器 | lua.c | 解释器 |
luac.c | 字节码编译器 |
Lua的源代码模块划分十分清晰,基本上是以一个.c
为一个模块,以同名的.h
文件描述导出的接口。代码风格采用K&R的风格。
Lua内部模块暴露的接口以luaX_xxx
命名,其中X
表示内部模块名,如string库以luaS_
开头,后跟一串小写字母描述方法名。
而供外部使用的接口则以lua_xxx
命名。此外还有供库开发者使用的luaL_xxx
系列的接口,其余均是内部接口,禁止外部调用。
Lua的核心是虚拟机,对于用户来说,Lua虚拟机的外在数据形式是一个lua_State结构体,这个结构体代表了虚拟机当前的状态和运行所需的所有数据。调用Lua的C API均需提供这个结构体作为第一个参数。
用户编写好的代码,需通过解释器解析文本并最终生成字节码(opcode和常量),供虚拟机解释执行。
opcode的定义(lopcodes.h/lopcodes.c)
和解析(lvm.c)
、状态机(lstate.c)
、function/table/string数据类型(lfunc.c/ltable.c/lstring.c)
、内存管理(lmem.c/lzio.c)
和最重要最复杂的垃圾回收(lgc.c)
,共同构成了虚拟机的核心。
Lua一般用来嵌入到其它系统中使用,但在官方版本里,也提供了一个独立解析器,源码在lua.c
中。
闭散列
一般我们使用的哈希表,如C++的std::<unordered_map>,他们内部有一个数组,放入的元素根据key的hash值算出存储位置,如果说两个不同的key拥有同样的hash值,则称为发生了hash冲突,则通过链表或数组的形式将它们串起来,如下图所示(带颜色圆圈中的值表示的是key的hash值):
这种叫做开散列,如果发生冲突的时候,不把冲突的键值串起来,而是寻找一个空闲的位置放入新的键值,如下图所示:
这种叫做闭散列,Lua中的table中关于hash表部分的实现,采用的就是闭散列的算法。闭散列插入新值的过程如下:
- 计算key的hash值,算出存储位置
- 如果当前位置空闲,则插入,算法完成
- 如果当前位置占用,则使用probing算法,寻找空闲位置
- 如果找到空闲位置,则插入,算法完成
- 如果没有找到空闲位置,则先进行扩容和rehash,再从第1步重新开始
其中probing算法有很多种,常见的有:
- 线性probing:如果冲突发生在
i
的位置,则搜索序列为:
- i + 1
- i + 2
- i + 3
- … - 二次方式probing:搜索序列为:
- i + 1
- i + 2x2
- i + 3x3
- …
每个probing算法各有所长,可以根据需要选取。
闭散列查询时的过程如下:
- 计算key的hash值,算出存储位置
- 如果当前位置空闲,则没有找到,算法完成
- 如果当前位置被占用,则检查key值是否相等,如果相等,则返回相应值,算法完成
- 如果key值不相等,则使用probing算法,寻找下一个位置
- 如果下一个位置的key值相等,则返回相应值,算法完成,否则返回步骤4,直到遇到空闲位置,则没有找到,算法完成
闭散列的冲突键值,是通过probing算法串起来的,有一种叫做合并散列的算法是对闭散列的进一步发展,它主要提升了发生键值聚集时的性能,它的改动有以下两项:
- 每个节点增加了一个
next_pointer
指针,指向冲突链的下一个位置 - probing算法为从最后一个位置往前查找
Lua的table
实现对合并散列算法进行了优化,进一步提升了插入和查询时的性能,接下来介绍table
的具体实现。
实现分析
Lua使用table作为统一的数据结构,这也是Lua的一大特色,通过table
,用户可以方便的实现queue
、set
和map
等数据结构。
Lua中的table
有一个高效的实现,它的内部分为了数组部分和哈希部分,数组部分提供了高效且紧凑的随机访问,而其他不能放入数组部分数据则全部放入哈希表中,table
的哈希表采用闭散列,实现的非常高效。
对于源码的解释,有些会单独来讲,有些会放在代码的注释里,对于直白的部分,就不再赘述了。
数据结构
首先是基础类型Value
,它是Lua中所有基础类型的联合:
// Lua中所有基础类型的联合
typedef union Value {
struct GCObject *gc; /* collectable objects */
void *p; /* light userdata */
lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
} Value;
然后是标记Value
,叫做TValue
,它是Lua中Value
的基础表示,由一个Value
和一个Tag
组成:
#define TValuefields Value value_; lu_byte tt_ //标记类型成员的宏定义,其他地方也会用到
typedef struct TValue {
TValuefields;
} TValue;
#define val_(o) ((o)->value_) // 取Value的宏
#define valraw(o) (&val_(o)) // 取value地址的宏
#define rawtt(o) ((o)->tt_) // 取标记的宏
然后是所有可回收对象的通用头和一个GCObject
定义:
#define CommonHeader struct GCObject *next; lu_byte tt; lu_byte marked
typedef struct GCObject {
CommonHeader;
} GCObject;
有了以上几个定义,我们就可以看table
的具体定义了,首先是节点Node
的定义:
typedef union Node {
struct NodeKey {
TValuefields; /* fields for value */
lu_byte key_tt; /* key type */
int next; /* for chaining */
Value key_val; /* key value */
} u;
TValue i_val; /* direct access to node's value as a proper 'TValue' */
} Node;
#define keytt(node) ((node)->u.key_tt) // 取key标记的宏
#define keyval(node) ((node)->u.key_val) // 取key值的宏
注意这是一个union
。其中key_val
、key_tt
组成了键值中key
的值,i_val
或TValuefields
组成了键值中Value
的值。next
指向冲突链的下一个位置。NodeKey
之所以按照这个顺序写是为了内存对齐的考虑。
现在我们可以看看Table
的定义了:
typedef struct Table {
CommonHeader;
lu_byte flags; /* 1<<p means tagmethod(p) is not present */
lu_byte lsizenode; /* log2 of size of 'node' array */
unsigned int alimit; /* "limit" of 'array' array */
TValue *array; /* array part */
Node *node;
Node *lastfree; /* any free position is before this position */
struct Table *metatable;
GCObject *gclist;
} Table;
其中CommonHeader
和gclist
为垃圾回收使用,其他字段的说明如下:
flags
为元表查找方法时使用,但首位用来标识alimit
是否为数组部分的真实长度lsizenode
为哈希部分长度的log2值,由于2的0次方为1,所以哈希部分最小长度为1alimit
为数组部分的限制长度,它小于等于数组部分的真实长度array
为数组部分node
为哈希部分lastfree
为probing时起始的位置metatable
指向元表
初始化和销毁
初始化通过luaH_new
来进行,定义如下:
Table *luaH_new (lua_State *L) {
GCObject *o = luaC_newobj(L, LUA_VTABLE, sizeof(Table));
Table *t = gco2t(o);
t->metatable = NULL;
t->flags = cast_byte(maskflags); /* table has no metamethod fields */
t->array = NULL;
t->alimit = 0;
setnodevector(L, t, 0);
return t;
}
对其中使用到的方法来逐个进行解释,首先是luaC_newobj
,它负责从内存中分配一个新的对象,第二个参数指定类型,第三个指定大小。它返回的是一个GCObject
,通过gco2t
宏转换为Table
指针,来看一下这个宏的定义:
union GCUnion {
GCObject gc; /* common header */
struct TString ts;
struct Udata u;
union Closure cl;
struct Table h;
struct Proto p;
struct lua_State th; /* thread */
struct UpVal upv;
};
#define cast_u(o) cast(union GCUnion *, (o))
#define gco2t(o) check_exp((o)->tt == LUA_VTABLE, &((cast_u(o))->h))
就是将GCObject
对象转换为GCUnion
对象,然后再通过取h
成员的方法转换为Table
指针。这样做的目的是由于这些集合里的对象,都拥有同样的头部结构,也就是CommonHeader
,那么在任一代码位置,只要其中一个类型的完整声明是可见的,那么就可以查看这个头部中的字段。
然后设置元表flags的初始值,maskflags
的定义如下:
#define maskflags (~(~0u << (TM_EQ + 1)))
这个主要是设置了6个可以快速获取的方法的标记位。
然后是设置数组部分array
、alimit
的初值。
最后是通过setnodevector
初始化哈希部分,其定义如下:
static void setnodevector (lua_State *L, Table *t, unsigned int size) {
if (size == 0) {
/* no elements to hash part? */
t->node = cast(Node *, dummynode); /* use common 'dummynode' */
t->lsizenode = 0;
t->lastfree = NULL; /* signal that it is using dummy node */
}
else {
int i;
int lsize = luaO_ceillog2(size); // 求size的log2值
if (lsize > MAXHBITS || (1u << lsize) > MAXHSIZE)
luaG_runerror(L, "table overflow");
size = twoto(lsize); // 求2的lsize次方
t->node = luaM_newvector(L, size, Node); // 分配内存
// 初始化哈希部分数据
for (i = 0; i < (int)size; i++) {
Node