构建Lua解释器Part10:userdata

前言

        本章节,我开始对dummylua的userdata的设计与实现,进行论述。它的大体设计与实现,仍然是仿照了lua5.3的标准,由于,所有的内容,都是我自己理解后,重新实现,因此在一些实现细节上略有不同,但是整体设计思路遵循了lua的设计思想。本章的篇幅不会很长,因为userdata这个部分并不是非常复杂,因此我这里也会速战速决,将userdata的一些核心思想论述清楚,就将本章完结。

userdata的数据结构

        userdata是用来存放,用户自定义的数据结构实例的,userdata的种类有两种,一种是lightuserdata,还有一种则是full userdata。light userdata是Value结构中的一个变量类型,本质是一个void* 指针

// luaobject.h
typedef union lua_Value {
    struct GCObject* gc;
    void* p;
    int b;
    lua_Integer i;
    lua_Number n;
    lua_CFunction f;
} Value;

light userdata的内存,需要用户自行管理,而full userdata则是通过lua的gc机制进行管理,我们本章论述的也正是full userdata。后文中,所有指代userdata的地方,均是指full userdata。对full userdata的操作实现,一般是在c层进行的,后面会有使用的例子。
        首先我们要看的是userdata的数据结构,在dummylua中,它的定义如下所示:

// luaobject.h
#define CommonHeader struct GCObject* next; lu_byte tt_; lu_byte marked

typedef struct Udata {
	CommonHeader;  			// GC公共头部
	struct Table* metatable;// userdata可以设置metatable,一般用于设置__gc域,在userdata被回收之前,
							// __gc函数会被调用,一般用于回收系统资源等
	int ttuv_;				// 相当于TValue的tt_,用来指代user_变量的类型
	int len;				// 自定义域的大小
	Value user_;			// 本质就是TValue的value_部分,这里将TValue拆成了两个部分
} Udata;

注释,对每个字段有了一个大致的说明,后续内容,会对他们进行较为详细的解释和说明。

userdata的接口

        上一小节,介绍了userdata的基本数据结构,本小节则会介绍userdata相关的主要接口,分别是luaS_newuserdata,getudatamem,setuservalue,getuservalue。我们先来看一下luaS_newuserdata的定义:

// luastring.c
Udata* luaS_newuserdata(struct lua_State* L, int size);

这个接口,做了什么事情呢?就是创建了一个userdata实例,这个userdata实例的内存大小,就是Udata头部+传入的size大小,我们可以看一下userdata实例的构成:

+------------+----------------+
|Header:Udata|   user domain  |
+------------+----------------+

前面的Header,就是sizeof(Udata)的大小,而后面的user domain,则是luaS_newuserdata的第二个参数size来指定,比方,现在我们定义了一个Vector3的数据结构:

typedef struct Vector3 {
	float x;
	float y;
	float z;
} Vector3;

现在我们要创建,这个Vector3关联的userdata,那么它的创建代码则如下所示:

Udata* u = luaS_newuserdata(L, sizeof(Vector3));

第二个参数,size的大小则是sizeof(Vector3)的大小,也就是12,那么此时,user domain的大小则是12 bytes的大小。
        在完成了userdata实例的创建之后,我们要在c层,获取我们自定义的结构实例的指针,如何获取它呢,我们需要一个接口getudatamem

// luaobject.h
#define getudatamem(o) (cast(char*,o)+sizeof(Udata))

这个宏的作用在,则是将userdata实例中,user domain部分的指针拿到,获取的结果所指向的位置,如下’^‘所示:

+------------+----------------+
|Header:Udata|   user domain  |
+------------+----------------+
             ^

通过这个宏,我们可以获取结构自定义结构变量的实例,具体方法如下所示:

Vector3* v3 = (Vector3*)getudatamem(u);

接下来,我们就可以对v3所指向的内存块进行对应的处理了。
        userdata还有一个重要的接口,就是setuservalue,这是一个宏,它的作用是,将一个TValue实例,赋值到Udata头部中,分别将TValue实例的tt_赋值给Udata的ttuv_,将TValue的value_赋值给Udata的user_字段中,其定义如下所示:

// luaobject.h
#define setuservalue(u, o) \
			(u)->ttuv_ = (o)->tt_; (u)->user_ = (o)->value_

同样的,获取它的接口如下所示:

// luaobject.h
#define getuservalue(u, o) \
			(o)->tt_ = (u)->ttuv_; (o)->value_ = (u)->user_

到这里,我们就完成了userdata相关的接口的讨论了。

userdata的gc处理

        关于lua的gc机制,我在Part2里已经有非常详细的论述了,在后面的章节中,我还会再出一篇,再次,进一步论述gc算法。有关gc算法的具体步骤,可以查看Part2,本小节,我将集中精力,梳理一下userdata的标记和清除阶段的逻辑处理。
        userdata在标记阶段,其处理逻辑如下所示:

// luagc.c
void reallymarkobject(struct lua_State* L, struct GCObject* gco) {
    struct global_State* g = G(L);
    white2gray(gco);

    switch(gco->tt_) {
        case LUA_TTHREAD:{
            linkgclist(gco2th(gco), g->gray);            
        } break;
        case LUA_TTABLE:{
            linkgclist(gco2tbl(gco), g->gray);
        } break;
		case LUA_TLCL:{
			linkgclist(gco2lclosure(gco), g->gray);
		} break;
		case LUA_TCCL:{
			linkgclist(gco2cclosure(gco), g->gray);
		} break;
		case LUA_TPROTO:{
			linkgclist(gco2proto(gco), g->gray);
		} break;
        case LUA_SHRSTR:{ 
            gray2black(gco);
            struct TString* ts = gco2ts(gco);
            g->GCmemtrav += sizelstring(ts->shrlen);
        } break;
        case LUA_LNGSTR:{
            gray2black(gco);
            struct TString* ts = gco2ts(gco);
            g->GCmemtrav += sizelstring(ts->u.lnglen);
        } break;
		case LUA_TUSERDATA: {
			gray2black(gco);

			TValue uvalue;
			Udata* u = gco2u(gco);
			getuservalue(u, &uvalue);
			if (u->metatable) {
				markobject(L, u->metatable);
			}

			if (iscollectable(&uvalue) && iswhite(gcvalue(&uvalue))) {
				reallymarkobject(L, gcvalue(&uvalue));
			}

			g->GCmemtrav += sizeof(Udata);
			g->GCmemtrav += u->len;
		} break;
        default:break;
    }
}

我们已知了,luaS_newuserdata接口,其实是在luastring模块里的,因为它的创建和使用逻辑和luastring非常类似,在标记阶段也是,userdata实例本身,在标记为灰色后,直接标记成黑色,并且将它的metatable(如果存在的话)标记为灰色,并且放入gray列表中,此外如果userdata的user域存在,且是一个gc实例,那么它也需要被标记为灰色,并且放入gray列表中。这里,我们需要注意几个问题,一个就是userdata在标记扫描阶段,直接整个被标记为黑色,并不会对user domain内部的任何域进行检查和处理。
        接下来,我们来看一下userdata的清除逻辑,如下所示:

// luagc.c
static lu_mem freeobj(struct lua_State* L, struct GCObject* gco) {
    switch(gco->tt_) {
        case LUA_SHRSTR: {
            struct TString* ts = gco2ts(gco);
            luaS_remove(L, ts);
            lu_mem sz = sizelstring(ts->shrlen);
            luaM_free(L, ts, sz); 
            return sz; 
        } break;
        case LUA_LNGSTR: {
            struct TString* ts = gco2ts(gco);
            lu_mem sz = sizelstring(ts->u.lnglen);
            luaM_free(L, ts, sz);
        } break;
        case LUA_TTABLE: {
            struct Table* tbl = gco2tbl(gco);
            lu_mem sz = sizeof(struct Table) + tbl->arraysize * sizeof(TValue) + twoto(tbl->lsizenode) * sizeof(Node);
            luaH_free(L, tbl);
            return sz;
        } break;
		case LUA_TTHREAD: {
			// TODO
		} break;
		case LUA_TLCL: {
			struct LClosure* cl = gco2lclosure(gco);
			lu_mem sz = sizeof(LClosure);
			luaF_freeLclosure(L, cl);
			return sz;
		} break;
		case LUA_TCCL: {
			struct CClosure* cc = gco2cclosure(gco);
			lu_mem sz = sizeof(struct CClosure);
			luaF_freeCclosure(L, cc);
			return sz;
		} break;
		case LUA_TPROTO: {
			struct Proto* f = gco2proto(gco);
			lu_mem sz = luaF_sizeproto(L, f);
			luaF_freeproto(L, f);
			return sz;
		} break;
		case LUA_TUSERDATA: {
			Udata* u = gco2u(gco);
			lu_mem sz = sizeof(Udata) + u->len;
			luaM_free(L, u, sz);
			return sz;
		} break;
        default:{
            lua_assert(0);
        } break;
    }
    return 0;
}

我们可以看case LUA_TUSERDATA的那部分逻辑,可以看到的是,userdata实例是整个被释放掉,未对userdata内部的user domian部分做任何的处理,也就是说如果user domain内部包含了堆内存实例的指针,这部分需要用户自己进行处理。

userdata的user domain域内部的堆内存清理

        前面,我们提到了,user domain域内,如果包含了指向堆内存的指针,那么这部分需要我们进行处理,需要怎么处理呢?lua的清除逻辑,并没有提供这样的机会,但是,我们前面说过,userdata有一个metatable域,为userdata设置一个metatable,并且这个metatable如果包含一个名为__gc的函数,那么在userdata被gc回收之前,会首先调用这个函数,我们来看一个伪代码,假设userdata的metatable是如下所示:

{
	__gc = function(udata) release(udata) end
}

那么在udata实例,被gc回收之前,上面这个__gc函数会被调用,该函数的参数,就是userdata实例本身,release函数是用户自己在c层实现的函数,导出给lua层使用的,这个release函数,将在c层逻辑中,对udata的user domain域中,包含的堆内存实例进行释放操作,避免内存泄露。

userdata的使用例子

        接下来,我们要看的则是userdata的使用例子,这里直接引用《Programing in Lua》的28.1 userdata一节,这里就不再赘述。

本章的example

        本章也写了一个测试用例,这个测试用例创建了一个userdata实例,并且为它设置了一个包含_ _gc函数的metatable,最后将这个userdata实例,从栈中移除,接着调用了fullgc函数,最后显示的结果是,这个__gc函数被调用。

// p10_test.c
#include "p10_test.h"
#include "../common/luastring.h"
#include "../vm/luagc.h"
#include "../common/luatable.h"

typedef struct Vector3 {
	float x;
	float y;
	float z;
} Vector3;

int gcfunc(struct lua_State* L) {
	Udata* u = lua_touserdata(L, -1);
	Vector3* v3 = (Vector3*)getudatamem(u);
	printf("total_size:%d x:%f, y:%f, z:%f", u->len, v3->x, v3->y, v3->z);
	return 0;
}

void test_create_object(struct lua_State* L) {
	Udata* u = luaS_newuserdata(L, sizeof(Vector3));

	Vector3* v3 = (Vector3*)getudatamem(u);
	v3->x = 10.0f;
	v3->y = 10.0f;
	v3->z = 10.0f;

	L->top->tt_ = LUA_TUSERDATA;
	L->top->value_.gc = obj2gco(u);
	increase_top(L);

	struct Table* t = luaH_new(L);
	struct GCObject* gco = obj2gco(t);
	TValue tv;
	tv.tt_ = LUA_TTABLE;
	tv.value_.gc = gco;
	setobj(L->top, &tv);
	increase_top(L);

	lua_pushCclosure(L, gcfunc, 0);
	lua_setfield(L, -2, "__gc");

	lua_setmetatable(L, -2);
	L->top--;

	return;
}

void p10_test_main() {
	struct lua_State* L = luaL_newstate();
	luaL_openlibs(L);

	test_create_object(L);
	luaC_fullgc(L);

	luaL_close(L);
}

执行后,得到的结果为:

total_size:12 x:10.0, y:10.0, z:10.0

这说明,创建出来的userdata,在fullgc过后,被清除掉了。

结束语

        本章,我大致介绍了userdata的设计与实现,与以往的一些重型篇幅的章节相比,本章内容非常少,并且对前面一些章节的论述有所依赖,因此如果读者通读了前面的章节,理解这个章节中关联的部分,也不会非常困难。本章节,我也是在实现了userdata相关的逻辑之后,写下的,因此读者可以在Part10的代码目录中,找到对应的实现,下一个章节,我将论述upvalue的设计与实现。