Lua的设计与实现——基础数据类型(Lua中的数据类型)

出自书籍:李创.Lua设计与实现[M].北京.人民邮电出版社.2017:186.

  在这一部分中,探讨Lua中的基础数据结构。Lua内部采用一种通用的基础数据结构来表示数据类型。Lua语言及其精简,只有字符串和表两种最基本的数据结构。然而,精简并不代表简陋,在这些基础数据结构的实现中,处处可以看到设计者为了性能和可扩展性(这是Lua从一开始就坚持的目标)所做的努力。

  Lua是一门动态类型的脚本语言,这意味着同一个变量可以在不同时刻指向不同类型的数据。

  在Lua中,我们使用一个通用的数据结构lua_TValue来统一表示所有在Lua虚拟机中需要保存的数据类型,这里将这个通用数据结构一层一层地拆解开来介绍。但是开始讨论Lua的实现之前,为了便于对比,我们先来看看在C语言实现相似的功能,一般做法是怎样的。

C语言实现通用数据结构的一般做法

  在开始阅读具体代码之前,首先需要想想,如果要使用一个通用的数据结构来表示不同的数据类型,一般的做法应该是这样的。

  • 需要一个字段来存储数据的类型。
  • 需要存储不同的数据类型的数据。

这里又有两种比较常见的做法。

  • 定义一个公共的数据结构作为基础类型,里面存储的都是表达这个数据的基础信息,其它具体的类型是从这里派生出来的。这就是一般的面向对象的思路。
    鉴于Lua使用的是C语言,可以使用类似下面的代码来模拟实现面向对象:
struct base //定义基础的数据信息
{
    int type;
};

struct string
{
    struct base info;
    int len;
    char *data[0];
};
  • 使用联合(union)来讲所有数据包进来,类似下面的代码:
struct string
{
    int len;
    char* data[0];
};

struct number
{
    double num;
};

struct value
{
    int type;
    union
    {
        string str;
        number num;
    }value;
};

两种做法相结合的方式。

Lua通用数据结构的实现

  在Lua一开始的设计中,主要有以下几种类型:数字(使用double类型表示)、字符串、关联表、nil、userdata、Lua函数以及C函数。一开始,我们并没有加入布尔类型的数据,同时Lua函数和C函数是分开表示的。

  在演进到5.1.4版本时,加入了THREAD类型以及布尔类型(详见表2-1),同时也将两种函数合并在了一起:

#define LUA_TNONE		(-1) --无类型

#define LUA_TNIL		0 --nil
#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 --共9中类型(不包含LUA_TNONE)


表2-1 Lua中的数据类型


类型

对应数据结构

LUA_TNONE

无类型


LUA_TNIL

空类型


LUA_TBOOLEAN

布尔类型


LUA_TLIGHTUSERDATA

指针

void *

LUA_TNUMBER

数据

lua_Number

LUA_TSTRING

字符串

TString

LUA_TTABLE


Table

LUA_TFUNCTION

函数

CClosure、LClosure

LUA_TUSERDATA

指针

void *

LUA_TTHREAD

Lua虚拟机、协程

lua_State

区别在于前者分配释放由Lua外部的使用者来完成,而后者则是由Lua内部来完成的。换言之,前者不需要Lua去关心它的生存期,由使用者自己去关注,后者则反之。

  Lua内部用一个宏表示哪些数据类型需要进行GC(Garbage Collection,垃圾回收)操作:

//(lobject. h) 
#define iscollectable(o) (ttype(o) >= LUA_TSTRING)

  可以看到,LUA_TSRING(包括LUA_TSTRING)之后的数据类型都需要进行GC操作。

  那么,这些需要进行GC操作的数据类型,在Lua中是如何表示的呢?

  这些需要进行GC操作的数据类型都会有一个CommonHeader宏定义的成员,并且这个成员在结构体定义的最开始部分。比如,用于表示表结构类型的Table是这么定义的:

//(lobject. h) 
typedef struct Table { 
	CommonHeader; 
	lu _byte flags; /* 1< p means tag 「「1ethod ( p) is not present */
	lu_byte lsizenode; /* log2 of size of node ’ array */ 
	struct Table netatable;
	TValue *array; /* array part */
	Node *node; 
	Node *lastfree; /* any free position is before this position */ 
	GCObject *gclist; 
	int sizearray; /* size array array */ 
} Table;

  其中CommonHeader的定义如下:

//(lobject. h) 
/*
** 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

里面的几个成员定义如下。

  • next:指向下一个GC链表的成员。
  • tt:表示数据的类型,及前面的那些表示数据类型的宏。
  • marked:GC相关的标记位。

同时,还有一个名为GCheader的结构体,其中的成员只有CommonHeader;

//(lobject. h) 
/* 
** Common header in struct form 
*/ 
typedef struct GCheader { 
	CommonHeader; 
} GCheader;

于是,在Lua中就使用了GCObject联合体将所有需要进行垃圾回收的数据类型囊括了进来:

//(lstate.h) 
/* 
Union of all collectable objects 
*/ 
union GCObject { 
	GCheader gch; 
	union TString ts; 
	union Udata u; 
    union Closure cl; 
	struct Table h; 
	struct Proto p; 
	struct UpVal uv; 
	struct lua_State th; /* thread */ 
};

  整理一下前面提到的这几个结构体,可以得到这样的结论。

  • 任何需要进行垃圾回收处理的Lua数据类型,必然以CommonHeader作为该结构体定义的最开始部分。如果熟悉C++类的实现原理,可以将CommonHeader这个成员理解为一个基类的所有成员,而其他需要回收处理的数据类型均从这个基类继承下来,所以它们的结构体定义的开始部分都是这个成员。
  • GCObject这个联合体,将所有需要进行垃圾回收的数据类型囊括其中,这样定位和查找不同类型的数据时就方便多了。而如果只想要它们的GC部分,可以通过GCheader gch,如:
//(lobject.h) 
#define gcvalue(o) check_exp(iscollectable(o), (o)->value.gc)

仅表示需要进行垃圾回收的数据类型还不够,还有几种数据类型是不需要进行垃圾回收的,Lua中将GCObject和它们一起放在了联合体Value中:

//(lobject.h) 
/* 
** Union of all Lua values 
*/ 
typedef union { 
	GCObject *gc; 
	void *p; 
	lua_Number n; 
	int b; 
} Value;

到了这一步,差不多可以表示Lua中所有的数据类型了。但是还欠缺了一点东西,那就是这些数据到底是什么类型的。于是Lua代码中又有了TValuefields,它用于将Value和类型结合在一起:

//(lobject.h) 
#define TValuefields Value value; int tt

这最后形成了Lua中的TValue结构体,Lua中的任何数据都可以通过该结构体表示:

//(lobject.h) 
typedef struct lua TValue { 
	TValuefields; 
} TValue;

Lua通用数据结构的组织如图2-1所示。

lua 定义对象 lua中的数据类型_lua


图2-1 Lua通用数据结构的组织

  前面提到过,Lua同时采用了两种方式来做到数据统一。根据前面的分析,这表现在以下两个方面。

  • 具体类型中有CommonHeader,用于存放所有数据类型都通用的字段。
  • TValue作为统一表示所有数据的数据结构,内部使用了联合体Value将所有数据都包起来。

  在具体的代码视线中,TValue用于统一表示数据,而一旦知道了具体的类型,就需要使用具体的类型了。因此,代码中有不少涉及TValue与具体类型之间转换的代码,其主要逻辑都是将TValue中的tt、value与具体类型的数据进行转换。比如,将lua_Number转换为TValue的宏setnvalue的代码是这样的:

//(lobject. h) 
#define setnvalue(obj,x) \ 
{ TValue *i_o = (obj); i_o->value.n=(x); i_o- >tt=LUA_TNUMBER; }

  这部分的代码逻辑和命名都差不多,这里就不再列出了。