构建Lua解释器Part10:userdata

本文转载自Manistein’s Blog

构建Lua解释器Part10:userdata

前言

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

userdata的数据结构

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

1
2
3
4
5
6
7
8
9
// 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中,它的定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
// 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的定义:

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

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

1
2
3
+------------+----------------+
|Header:Udata| user domain |
+------------+----------------+

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

1
2
3
4
5
typedef struct Vector3 {
float x;
float y;
float z;
} Vector3;

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

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

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

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

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

1
2
3
4
+------------+----------------+
|Header:Udata| user domain |
+------------+----------------+
^

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

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

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

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

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

1
2
3
// 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在标记阶段,其处理逻辑如下所示:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 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的清除逻辑,如下所示:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 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是如下所示:

1
2
3
{
__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函数被调用。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 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);
}

执行后,得到的结果为:

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

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

结束语

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

-------------本文结束 感谢阅读-------------