前言

本节开始,我们将分析 Lua 中各种数据类型的定义与实现。众所周知,Lua 是一种动态弱类型语言,在使用时我们不需要显式地声明变量的类型,而是由 Lua 运行时根据变量的值自动推断其类型。

所谓动态弱类型语言,是指在运行时才确定变量的类型,而且变量的类型可以随时改变。这种特性使得 Lua 在编程时更加灵活,但也增加了一些运行时的开销。为了达到动态类型效果,Lua 需要在内部维护一些数据结构来表示不同的数据类型。本节我们将从 Lua 中的类型定义开始,逐步分析 Lua 中的基本数据类型。

本文基于 Lua 5.4 版本源码进行分析。

Lua 中的类型定义

Lua 中一些比较重要的宏被放在了 lua.h 中,其中就包括了 Lua 中的数据类型定义。我们可以在 lua.h 中找到如下的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
** basic types
*/
#define LUA_TNONE (-1)

#define LUA_TNIL 0
#define LUA_TBOOLEAN 1
#define LUA_TLIGHTUSERDATA 2
#define LUA_TNUMBER 3
#define LUA_TSTRING 4
#define LUA_TTABLE 5
#define LUA_TFUNCTION 6
#define LUA_TUSERDATA 7
#define LUA_TTHREAD 8

#define LUA_NUMTYPES 9

PS: 为了 markdown 渲染的美观性,所有的代码块都使用 C++ 的语法高亮,但实际上 Lua 的源码是使用 C 语言编写的。

可以看到,除了 8 种基本的数据类型外,Lua 还定义了一个 LIGHTUSERDATA 类型,它们各自的作用我们先按下不表。观察上述宏定义的结构可以发现,类型定义的组织方式类似于枚举类型,其中 LUA_NUMTYPES 代表了 Lua 中的数据类型总数。而 LUA_TNONE 则表示一个无效的类型。

TValue

前面我们说到,Lua 会在内部维护一些数据结构来标识不同的数据类型,这个数据结构就是 TValueTValue 意为 “Tagged Value”,维护了真实的数据以及数据的类型标识。我们可以在 lobject.h 中找到 TValue 的定义:

lobject.h 中主要定义了一些 Lua 中的对象类型以及对象的操作函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
** Union of all Lua values
*/
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;


/*
** Tagged Values. This is the basic representation of values in Lua:
** an actual value plus a tag with its type.
*/

#define TValuefields Value value_; lu_byte tt_

typedef struct TValue {
TValuefields;
} TValue;

可以看到,首先定义了一个 Value 联合体用于存放 5 种不同类型的数据。然后定义了一个 TValue 结构体,其中包含了一个 Value 类型的字段 value_ 以及一个 lu_byte 类型的字段 tt_tt_ 意为 “Tag Type”,用于标识 value_ 中存放的数据的类型。

1
2
/* chars used as small naturals (so that 'char' is reserved for characters) */
typedef unsigned char lu_byte;

lu_byte 是一个无符号字符类型(大小为一个字节,也就是八位),用于存放标识数据类型的值。在 Lua 中,lu_byte 用于存放数据类型的标识,lu_byte 可以理解为一个八位的二进制数字。通过对每一位的设置,就可以表示不同的数据类型。

1
2
3
4
5
6
7
8
9
/*
** tags for Tagged Values have the following use of bits:
** bits 0-3: actual tag (a LUA_T* constant)
** bits 4-5: variant bits
** bit 6: whether value is collectable
*/

/* add variant bits to a type */
#define makevariant(t,v) ((t) | ((v) << 4))

lobject.h 的注释中,我们可以看到 lu_byte 每一位的含义:其中 0-3 位用于存放数据类型的标识,4-5 位用于存放变体位,6 位用于标识数据是否可回收。

由于变体位只有两位,所以变体位的取值范围是 0~3。因此一个类型的变体最多只能有 4 种(包括自身)。

所谓变体位,是指对于某一种数据类型,可能会有多种不同的表示方式。例如,对于字符串类型,Lua 中有长字符串和短字符串两种表示方式,这时就可以使用变体位来区分。变体位的设置方式是将数据类型的标识左移 4 位,然后与变体位进行或操作。

此外可以发现,lu_byte 的最后一位是没有被使用的,Lua 中仅使用了 0~6 位。

了解了这些之后,就可以分析具体的类型标识了。我们逐一分析。

LUA_TNIL 0x00

Nil 在 Lua 中代表一个空值,类似于 C 语言中的 NULL

1
2
3
4
5
6
7
8
/* Standard nil */
#define LUA_VNIL makevariant(LUA_TNIL, 0)

/* Empty slot (which might be different from a slot containing nil) */
#define LUA_VEMPTY makevariant(LUA_TNIL, 1)

/* Value returned for a key not found in a table (absent key) */
#define LUA_VABSTKEY makevariant(LUA_TNIL, 2)

在 Lua 中,nil 共有三种变体,分别是 NILEMPTYABSTKEYNIL 代表标准的 nil 值,EMPTY 代表一个空的位置(有别于存放了一个 nil 值的位置),ABSTKEY 则用于没有找到键值时的返回值。

可以看出,分别使用 makevariant 宏通过赋予变体位不同值的方式生成了不同的变体。

此外,后续还定义了一些宏用于将对象设置为 nil 值。

1
2
3
4
5
6
/* set a value's tag */
#define settt_(o,t) ((o)->tt_=(t))

#define setnilvalue(obj) settt_(obj, LUA_VNIL)
/* mark an entry as empty */
#define setempty(v) settt_(v, LUA_VEMPTY)

可以看出,Lua 中将一个对象设置为 nil 的方式仅仅是将其类型标识设置为 NIL(或者 EMPTY),并没有对 Value 联合体中的具体值进行修改。

LUA_TBOOLEAN 0x01

Boolean 类型在 Lua 中代表了布尔值,只有两种取值:truefalse。观察上述的 TValue 结构体可以发现,布尔值并不在 Value 联合体中。在 Lua 中,布尔值通过 tt_ 中变体位的值来直接表示,具体的定义如下:

1
2
#define LUA_VFALSE	makevariant(LUA_TBOOLEAN, 0)
#define LUA_VTRUE makevariant(LUA_TBOOLEAN, 1)

可以看到,FALSETRUE 分别代表了布尔值 falsetrue。它们的区别仅仅在于第一位变体位的值。

1
2
3
4
5
6
7
8
9
10
#define ttisboolean(o)		checktype((o), LUA_TBOOLEAN)
#define ttisfalse(o) checktag((o), LUA_VFALSE)
#define ttistrue(o) checktag((o), LUA_VTRUE)


#define l_isfalse(o) (ttisfalse(o) || ttisnil(o))


#define setbfvalue(obj) settt_(obj, LUA_VFALSE)
#define setbtvalue(obj) settt_(obj, LUA_VTRUE)

此外提供了一些宏用于判断以及设置布尔值。与 nil 的赋值相同,Lua 中设置布尔值的方式也是直接设置对应的类型标识位。

#define l_isfalse(o) (ttisfalse(o) || ttisnil(o)) 从这个宏的定义可以看出,Lua 中只有 falsenil 会被认为是假值,其他的值都会被认为是真值,包括数字 0 和空字符串。

LUA_TLIGHTUSERDATA 0x02

Light Userdata 是轻量级的 userdata 类型,它是一个指向任意 C 语言数据的指针。Light Userdata 的特点是不受 Lua 垃圾回收机制的管理,适合用于简单的数据交换,但同时也需要注意一些内存泄漏的问题。

1
2
3
4
5
6
7
8
9
#define LUA_TNIL		0
#define LUA_TBOOLEAN 1
#define LUA_TLIGHTUSERDATA 2
#define LUA_TNUMBER 3
#define LUA_TSTRING 4
#define LUA_TTABLE 5
#define LUA_TFUNCTION 6
#define LUA_TUSERDATA 7
#define LUA_TTHREAD 8

回顾一下前面的类型定义,事实上在 LUA_STRING 之前的类型都不会被 gc (垃圾回收)管理,它们要么是值类型,要么是轻量级的指针类型。在 lstate.h 中也可以看到这样一个宏:

1
2
3
4
5
/*
** macro to convert a Lua object into a GCObject
** (The access to 'tt' tries to ensure that 'v' is actually a Lua object.)
*/
#define obj2gco(v) check_exp((v)->tt >= LUA_TSTRING, &(cast_u(v)->gc))

obj2gco 宏用于将一个 Lua 对象转换为一个 GCObject 对象,会返回一个指向 GCObject 的指针。可以看到在转换之前会先检查对象的类型是否大于等于 LUA_TSTRING,也就是说只有 LUA_TSTRING 及之后的类型才会被 gc 管理。

回到 LUA_TLIGHTUSERDATA 类型,它的定义如下:

1
2
3
4
5
6
7
8
/*
** Light userdata should be a variant of userdata, but for compatibility
** reasons they are also different types.
*/
#define LUA_VLIGHTUSERDATA makevariant(LUA_TLIGHTUSERDATA, 0)

#define setpvalue(obj,x) \
{ TValue *io=(obj); val_(io).p=(x); settt_(io, LUA_VLIGHTUSERDATA); }

LIGHTUSERDATA 也仅有一个变体,在设置 LIGHTUSERDATA 类型的值时,除了设置类型标识位外,还需要设置具体的指针值。也即 Value 联合体中的 p 字段。

LUA_TNUMBER 0x03

Number 类型在 Lua 中代表数字类型,包括整数和浮点数。

Lua在5.3版本之前,在底层实现中,所有数字都是浮点数,没有整数的概念,整数在底层也是通过浮点数(IEEE表示法)进行表示与数据存储,我们以为的整数的运算在当时可能在多次运算之后会累计产生出我们意料之外的浮点误差。

而在Lua5.3版本开始,Lua添加了对整数的支持,让整数从浮点中独立出来,不再使用浮点数进行表示,并支持了位运算这类整数运算的操作符。

1
2
3
/* Variant tags for numbers */
#define LUA_VNUMINT makevariant(LUA_TNUMBER, 0) /* integer numbers */
#define LUA_VNUMFLT makevariant(LUA_TNUMBER, 1) /* float numbers */

因此,在我们分析的 5.4 版本中,Number 类型有两种变体,分别是 NUMINTNUMFLTNUMINT 代表整数类型,NUMFLT 代表浮点数类型。二者分别在 Value 联合体中的 in 字段中存放具体的数值。

1
2
3
4
5
6
7
8
9
10
11
#define setfltvalue(obj,x) \
{ TValue *io=(obj); val_(io).n=(x); settt_(io, LUA_VNUMFLT); }

#define chgfltvalue(obj,x) \
{ TValue *io=(obj); lua_assert(ttisfloat(io)); val_(io).n=(x); }

#define setivalue(obj,x) \
{ TValue *io=(obj); val_(io).i=(x); settt_(io, LUA_VNUMINT); }

#define chgivalue(obj,x) \
{ TValue *io=(obj); lua_assert(ttisinteger(io)); val_(io).i=(x); }

由于数值类型在 Value 中有对应的字段,因此在设置数字时除了需要设置类型标识位外,还需要设置具体的数值。具体来说就是将 Value 联合体中的 ni 字段设置为对应的数值。

关于 Lua 中的数字,我们可以再深入研究一下它们在 C 中分别是怎么定义的:

1
2
3
4
5
6
7
/* type for integer functions */
typedef LUA_INTEGER lua_Integer;
#define LUA_INTEGER long long

/* type of numbers in Lua */
typedef LUA_NUMBER lua_Number;
#define LUA_NUMBER double

可以看到,lua_Integer 类型在 C 中是一个 long long 类型,lua_Number 类型是一个 double 类型。这也就是说,在 Lua 中整数类型是一个 64 位的整数类型,浮点数类型是一个 64 位的双精度浮点数类型。

LUA_TSTRING 0x04

Lua 中有两种字符串类型:长字符串和短字符串。它们也通过变体位来区分。

1
2
3
4
5
6
7
8
/* Variant tags for strings */
#define LUA_VSHRSTR makevariant(LUA_TSTRING, 0) /* short strings */
#define LUA_VLNGSTR makevariant(LUA_TSTRING, 1) /* long strings */

#define setsvalue(L,obj,x) \
{ TValue *io = (obj); TString *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(x_->tt)); \
checkliveness(L,io); }

setsvalue 宏中可以看出,字符串实际上存放在 value.gc 中。事实上,后续类型的数据均放在 value.gc 中,这是因为这些类型都是可回收的对象,需要被 gc 管理。

LUA_TTABLE 0x05

Table 在 Lua 中是一种比较强大的数据结构,它既可以顺序存放数据,也可以通过键值对的方式存放数据。Table 同时维护了一个数组以及一个哈希表,关于具体的实现我们会在后续的文章中详细分析。

1
2
3
4
5
6
#define LUA_VTABLE	makevariant(LUA_TTABLE, 0)

#define sethvalue(L,obj,x) \
{ TValue *io = (obj); Table *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VTABLE)); \
checkliveness(L,io); }

Table 也仅有一种变体,且存放在 value.gc 中。

LUA_TFUNCTION 0x06

虽然 Lua 并非严格意义上的函数式编程语言,但是 Lua 支持函数式编程的许多特性,如一等值函数、闭包、高阶函数、匿名函数等。Function 类型在 Lua 中代表了函数对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Variant tags for functions */
#define LUA_VLCL makevariant(LUA_TFUNCTION, 0) /* Lua closure */
#define LUA_VLCF makevariant(LUA_TFUNCTION, 1) /* light C function */
#define LUA_VCCL makevariant(LUA_TFUNCTION, 2) /* C closure */

#define setclLvalue(L,obj,x) \
{ TValue *io = (obj); LClosure *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VLCL)); \
checkliveness(L,io); }

#define setclLvalue2s(L,o,cl) setclLvalue(L,s2v(o),cl)

#define setfvalue(obj,x) \
{ TValue *io=(obj); val_(io).f=(x); settt_(io, LUA_VLCF); }

#define setclCvalue(L,obj,x) \
{ TValue *io = (obj); CClosure *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VCCL)); \
checkliveness(L,io); }

Function 类型有三种变体,分别代表不同类型的函数。LCL 代表 Lua 闭包,LCF 代表轻量级 C 函数,CCL 代表 C 闭包。可以看到它们全部存放在 value.gc 中。

这里定义的函数实际上指的是运行时的函数实例,可以理解为 Function 由我们定义的函数原型(Proto)实例化并且绑定了一些环境变量(Upvalue)之后的结果。因此这两种类型在 Lua 中也需要定义:

1
2
3
#define LUA_VPROTO	makevariant(LUA_TPROTO, 0)

#define LUA_VUPVAL makevariant(LUA_TUPVAL, 0)

PROTO 代表函数原型;UPVAL 代表上值,即函数的环境变量。

也正是因此,在 lobject.h 的开始处,定义了一些扩充的类型:

1
2
3
4
5
6
7
8
9
10
11
/*
** Extra types for collectable non-values
*/
#define LUA_TUPVAL LUA_NUMTYPES /* upvalues */
#define LUA_TPROTO (LUA_NUMTYPES+1) /* function prototypes */


/*
** number of all possible types (including LUA_TNONE)
*/
#define LUA_TOTALTYPES (LUA_TPROTO + 2)

LUA_TUSERDATA 0x07

Userdata 类型在 Lua 中代表了用户自定义的数据类型。Userdata 类型是一种通用的数据类型,可以用来表示任意类型的数据。Userdata 类型在 Lua 中是一个黑盒,Lua 无法直接操作 Userdata 类型的数据,只能通过元表(Metatable)来操作。

1
2
3
4
5
6
#define LUA_VUSERDATA		makevariant(LUA_TUSERDATA, 0)

#define setuvalue(L,obj,x) \
{ TValue *io = (obj); Udata *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VUSERDATA)); \
checkliveness(L,io); }

Userdata 也仅有一种变体,存放在 value.gc 中。

LUA_TTHREAD 0x08

Thread 类型在 Lua 中代表了一个线程。线程是 Lua 中的一种特殊对象,它可以被创建、销毁、挂起、恢复等。线程的实现是基于协程的,Lua 中的线程是协程的一种特殊形式。

1
2
3
4
5
6
#define LUA_VTHREAD		makevariant(LUA_TTHREAD, 0)

#define setthvalue(L,obj,x) \
{ TValue *io = (obj); lua_State *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(LUA_VTHREAD)); \
checkliveness(L,io); }

THREAD 并没有其他变体,只有一个标准的线程类型;存放在 value.gc 中。

Collectable Objects

在 Lua 中,除了 NILBOOLEANLIGHTUSERDATANUMBER 四种类型外,其他的数据类型都是可回收的对象,即 value.gc 中所存放的对象。这些可 gc 的对象都共同包含了一个 CommonHeader,用于标识 gc 系统会用到的一些信息。

换言之,可以理解为 TStringTableFunctionUserdataThread 等类型都是 GCObject 的子类,虽然它们并没有继承关系。

CommonHeader 的定义如下:

1
2
3
4
5
6
7
8
9
10
11
/*
** Common Header for all collectable objects (in macro form, to be
** included in other objects)
*/
#define CommonHeader struct GCObject *next; lu_byte tt; lu_byte marked


/* Common type for all collectable objects */
typedef struct GCObject {
CommonHeader;
} GCObject;

可以看到,GCObject 中包含了一个 next 指针,用于连接所有的可回收对象,以及一个 tt 字段,用于存放数据类型的标识。此外还有一个 marked 字段,用于标识对象是否被标记(在 gc 过程中会用到)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Bit mark for collectable types */
#define BIT_ISCOLLECTABLE (1 << 6)

#define iscollectable(o) (rawtt(o) & BIT_ISCOLLECTABLE)

/* mark a tag as collectable */
#define ctb(t) ((t) | BIT_ISCOLLECTABLE)

#define gcvalue(o) check_exp(iscollectable(o), val_(o).gc)

#define gcvalueraw(v) ((v).gc)

#define setgcovalue(L,obj,x) \
{ TValue *io = (obj); GCObject *i_g=(x); \
val_(io).gc = i_g; settt_(io, ctb(i_g->tt)); }

此外有一些宏用于判断对象是否可回收,以及设置对象的 gc 信息。可以看到这里正是使用到了 tt_ 的第 6 位来标识对象是否可回收。

GCObject 相对应的还有一个 GCUnion 联合体,用于存放不同类型的可回收对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
** Union of all collectable objects (only for conversions)
*/
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))

/* macros to convert a GCObject into a specific value */
#define gco2ts(o) \
check_exp(novariant((o)->tt) == LUA_TSTRING, &((cast_u(o))->ts))
#define gco2u(o) check_exp((o)->tt == LUA_VUSERDATA, &((cast_u(o))->u))
#define gco2lcl(o) check_exp((o)->tt == LUA_VLCL, &((cast_u(o))->cl.l))
#define gco2ccl(o) check_exp((o)->tt == LUA_VCCL, &((cast_u(o))->cl.c))
#define gco2cl(o) \
check_exp(novariant((o)->tt) == LUA_TFUNCTION, &((cast_u(o))->cl))
#define gco2t(o) check_exp((o)->tt == LUA_VTABLE, &((cast_u(o))->h))
#define gco2p(o) check_exp((o)->tt == LUA_VPROTO, &((cast_u(o))->p))
#define gco2th(o) check_exp((o)->tt == LUA_VTHREAD, &((cast_u(o))->th))
#define gco2upv(o) check_exp((o)->tt == LUA_VUPVAL, &((cast_u(o))->upv))

以及一些宏用于将 GCObject 转换为具体的类型。

当 Lua 创建一个新的可回收对象时,会先统一创建一个 GCObject 对象,然后根据具体的类型转换为 GCUnion 中的某一特定类型。举例来说,在 table 的创建函数中:

1
2
3
4
5
Table *luaH_new (lua_State *L) {
GCObject *o = luaC_newobj(L, LUA_VTABLE, sizeof(Table));
Table *t = gco2t(o);
// ...
}

可以看到这里先创建了一个 GCObject 对象,然后通过 gco2t 宏将其转换为 Table 类型。

实际上这里使用 GCObject * 来指向一个具体的可回收对象的行为是一种多态的体现,作用与父类指针指向子类对象类似。CommonHeader 的存在也是为了实现这种多态。

总结

本节我们分析了 Lua 中的数据类型定义,以及 TValue 结构体的定义。总体来看,Lua 中有 8 种基本数据类型,分别是 NILBOOLEANLIGHTUSERDATANUMBERSTRINGTABLEFUNCTIONUSERDATATHREAD。此外,还有两种用于函数的扩展类型 PROTOUPVAL

Lua 是用一个名为 TValue 的结构体来表示不同的数据类型,TValue 结构体中包含了一个 Value 联合体和一个 lu_byte 类型的 tt_ 字段。其中 Value 联合体用于存放具体的数据,tt_ 字段用于存放数据的类型标识。

tt_ 字段的每一位都有特定的含义,0-3 位用于存放数据类型的标识,4-5 位用于存放变体位,6 位用于标识数据是否可回收。通过对 tt_ 字段的设置,Lua 可以表示不同的数据类型。

在后续的文章中,我们将逐一分析 Lua 中的每一种数据类型的定义与实现。