Lua中的值及其类型

  Lua是一门动态类型的语言,这意味着Lua中的变量没有类型,而值才有类型。一个变量可以在不同时刻指向不同类型的值。下面将对Lua中的值及其类型做一些总结。

基本数据类型及其子类型

  在lua-5.3.5版本中,有9中基本的数据类型,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
#define LUA_TNIL		0		// 空类型
#define LUA_TBOOLEAN 1 // 布尔类型
#define LUA_TLIGHTUSERDATA 2 // 指针类型(void *)
#define LUA_TNUMBER 3 // 数字类型
#define LUA_TSTRING 4 // 字符串类型
#define LUA_TTABLE 5 // 表类型
#define LUA_TFUNCTION 6 // 函数类型
#define LUA_TUSERDATA 7 // 指针类型(void *)
#define LUA_TTHREAD 8 // Lua虚拟机、协程

#define LUA_NUMTAGS 9

  其中需要说明的是LUA_TLIGHTUSERDATA和LUA_TUSERDATA的区别是:前者的所有对象共享一个元表,且其内部所指向的内存的申请及释放不需要由Lua来完成;后者的每个对象都有自己的元表,需要进行垃圾回收,并且其内部所指向的内存的申请和释放需要由Lua来完成。除此之外,上述的一些基本类型有子类型(变种类型),一些基本类型需要GC,例如LUA_TNUMBER类型可以进一步分为整数类型和实数类型,现将这些子类型及GC标记的定义一并罗列如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define LUA_TLCL	(LUA_TFUNCTION | (0 << 4))  /* Lua closure */
#define LUA_TLCF (LUA_TFUNCTION | (1 << 4)) /* light C function */
#define LUA_TCCL (LUA_TFUNCTION | (2 << 4)) /* C closure */

#define LUA_TSHRSTR (LUA_TSTRING | (0 << 4)) /* short strings */
#define LUA_TLNGSTR (LUA_TSTRING | (1 << 4)) /* long strings */

#define LUA_TNUMFLT (LUA_TNUMBER | (0 << 4)) /* float numbers */
#define LUA_TNUMINT (LUA_TNUMBER | (1 << 4)) /* integer numbers *

/* Bit mark for collectable types */
#define BIT_ISCOLLECTABLE (1 << 6)

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

  Lua中是如何实现对基本类型、子类型、是否需要GC等的表示的呢?其实从子类型的定义我们也可以大概了解到,具体如下:

  • 基本类型有9种,因此可以用低四位,即bits 0-3来表示;
  • 每种基本类型的子类型不超过4种,因此可以用中间两位,即bits 4-5来表示;
  • 用于标记某种类型是否需要进行GC,只需要一个位,因此可以用bit 6来表示;

值表示的统一数据结构

  Lua中所有的值都是第一类值,即所有的值都可以存储在变量中,也可以作为参数传递给函数,也可以作为函数的返回值。为了更好地管理Lua中的值,Lua使用了一个通用的数据结构TValue来存储Lua中可能出现的任何值及其类型,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef union Value {
GCObject *gc; /* collectable objects */
void *p; /* light userdata */
int b; /* booleans */
lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
} Value;

#define TValuefields Value value_; int tt_

typedef struct lua_TValue {
TValuefields;
} TValue;

  联合体Value将Lua中所有可能的值都囊括了进来,比如通过包含GCObject *类型的成员gc,可以将所有需要GC的值都包含进来。结构体TValue包含了两个成员,一个是联合体类型Value的对象value_,用于表示Lua中所有可能出现的值,另一个是int类型的tt_,用来表示Value具体表示哪种类型的值。这样通过TValue就是可以表示Lua中所有的值及其类型了。
  那如何将TValue与某种具体类型的值之间做转换呢?其主要的逻辑是将TValue中的value_及tt_与具体的数据及其类型对应起来做转换。以TValue和LUA_TNUMFLT之间的转换为例:

1
2
3
4
5
6
7
8
9
10
11
12
/* Macro to test type */
#define ttisfloat(o) checktag((o), LUA_TNUMFLT)

/* Macro to access values */
#define fltvalue(o) check_exp(ttisfloat(o), val_(o).n)

#define val_(o) ((o)->value_)
#define settt_(o,t) ((o)->tt_=(t))

/* Macro to set value */
#define setfltvalue(obj,x) \
{ TValue *io=(obj); val_(io).n=(x); settt_(io, LUA_TNUMFLT); }

  我们可以通过宏fltvalue()从TValue对象中取出其中存放的实数,通过宏setfltvalue()将实数存放到TValue对象中,并将其内部类型设置为LUA_TNUMFLT。

垃圾回收类型及其“继承”

  上面说到,Value将Lua中所有可能的值都包含了进来,从它的定义中我们不难看出,它确实将布尔类型、整数类型、实数类型、轻量级函数类型等类型的值包含了进来,只要给Value类型的对象赋其中某一个类型的值就行,但是为什么说通过包含GCObject *类型的gc成员就将所有需要GC的类型的值也包含进来了呢?我们先来看下GCObject的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Common type for all collectable objects */
typedef struct GCObject GCObject;

/* Common Header for all collectable objects (in macro form, to be included in other objects) */
#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked

/* Common type has only the common header */
struct GCObject {
CommonHeader;
};

typedef struct TString {
CommonHeader;

lu_byte extra; /* reserved words for short strings; "has hash" for longs */
lu_byte shrlen; /* length for short strings */
unsigned int hash;
union {
size_t lnglen; /* length for long strings */
struct TString *hnext; /* linked list for hash table */
} u;
} TString;

  为了更好地说明上面提出来的问题,我们通过一个需要进行GC的类型TString(即lua中的字符串类型)来进行辅助说明。从上面GCObject类型和TString类型的定义中可以看出,两者均在开头包含了一个宏CommonHeader。这就使得这两个类型所对应的对象的内存布局的头部是相同的,两者的内存布局如图1所示,而TString类型的对象中多出来的内容则是其私有数据,因而所有需要GCObject类型的对象地方就都可以将TString类型的对象传进去,当然需要做一个强制类型转换(毕竟是伪继承),将TString类型强转伪GCObject类型,在实际使用的时候,我们再将其强转回TString类型即可。所有其他需要进行GC的类型都和TString一样,会在定义的开头加上CommonHeader,从而实现类似的功能。从另外一方面看,我们可以将GCObject看成TString、Table等的父类,这也很好地体现了在C中如何实现继承等面向对象编程的方法。

  从上面的分析我们可以知道,GCObject类型可以说是所有需要进行垃圾回收的类型的代表。在一些接口方面,都是以GCObject类型进行呈现的。例如在Lua中,进程要创建一个需要进行垃圾回收类型的对象时,都是申请一个GCObject对象,同时将具体的类型和所需要的内存大小通过参数传递进去。申请GCObject对象成功后,在函数外层再根据上下文环境将GCObject对象转换为具体类型的对象,再进行类型私有数据的初始化等操作。还是以TString为例,假设要创建一个TString类型的对象,lua源码中有如下例程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 创建一个新的字符串对象,未填入具体的字符串内容,只是申请了内存空间 */
static TString *createstrobj (lua_State *L, size_t l, int tag, unsigned int h) {
TString *ts;
GCObject *o;
size_t totalsize; /* total size of TString object */

/* 计算字符串对应需要的内存大小,包括头部和内容,内容紧跟在头部之后 */
totalsize = sizelstring(l);

/* 根据存放字符串所需的内存大小和类型标记创建一个新的GCObject对象 */
o = luaC_newobj(L, tag, totalsize);

/* 将GCObject类型转换为具体的TString类型 */
ts = gco2ts(o);

/* 保存字符串对应的hash值 */
ts->hash = h;
ts->extra = 0;

/* 字符串以'\0'结尾 */
getstr(ts)[l] = '\0'; /* ending 0 */
return ts;
}

  注意到上面的例程中有一条将GCObject类型对象强转为TString类型对象的语句。为了方便做类似的转换,Lua专门定义了一个联合体及一系列的宏来辅助进行这样的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 */
};

#define cast_u(o) cast(union GCUnion *, (o))

#define gco2ts(o) check_exp(novariant((o)->tt) == LUA_TSTRING, &((cast_u(o))->ts))

  为了节省篇幅,我们还是以GCObject类型强转TString为例。在createstrobj()这个创建TString类型的对象的例程中,我们先计算了我们要创建的TString对象的大小,包括TString结构体本身(字符串管理结构)以及实际内容的总大小,然后以字符串类型(可以是长字符串,也可以是短字符串)及对象大小来调用luaC_newobj()函数创建一个GCObject类型的对象(在当前上下文中,这是一个伪装成GCObject的TString),然后调用gco2ts()这个宏将其转换为TString类型的对象。这个宏的关键点是“&((cast_u(o))->ts)”,由于GCUnion是一个联合体,而该联合体内囊括的所有类型又都是需要GC的类型,那么可以说联合体GCUnion的一个对象和所有需要进行GC的类型的对象的内存布局的开头是一样的,正如GCObject和TString的内存布局开头那样,那么关键点中的“cast_u(o)”是可行的,而且不会引起错误,然后我们再以TString的方式来访问这个联合体,即关键点的剩余部分“&((cast_u(o))->ts)”,这样的效果就是将GCObject对象转换为了TString对象。其他需要GC的类型也可以用类似方式进行转换。
  本文关于Lua中值及其类型的总结就到这里了。
参考:
1、 Lua 5.3 Reference Manual
2、 《Lua设计与实现》

TitenWang wechat
业精于勤荒于嬉,行成于思毁于随。