« 一个内存泄露 bug | 返回首页 | 共享 lua vm 间的小字符串 »

希望 Lua 可以增加一个新特性 userdata slice

Lua 是一门嵌入式语言,和 host 的联动非常重要。Lua 使用 userdata 来保存 host 里的数据,userdata 非常强大,可以有 metatable 还可以关联一个 uservalue ,可以封装一切 C/C++ 对象,非常强大。但有的时候却稍显不足,似乎缺了点什么,导致一些简单的需求要用很繁琐的方式解决。

有个想法想过很久,今天动了念头用英文写了一遍投递到 lua 邮件列表里去了。

那就是,如果我们可以给 userdata 的值关联一个整数,而不是把 uservalue 关联到 userdata 的对象里那样,可以简化很多事情。

看这样一个例子:

如果我们有一个数据结构:

struct foo {
  struct foo1 foo1;
  struct foo2 *foo2;
};

如何保存到 lua 里被 lua 引用呢?通常我们使用一个 userdata 封装它:

struct foo * f = lua_newuserdata(L, sizeof(*f));

但是,如果我们想进一步引用这个结构中的一个子结构怎么办?即,如果我们在 lua 里有了 userdata foo, 希望可以通过 foo.foo1 引用其子结构 foo1, 用 foo.foo2 引用 foo2 怎么办?

当然,我们先要给 foo 配置一个 metatable 并实现 index 元方法。这样在语法上就可以用 foo.foo1 和 foo.foo2 了。

但是 foo.foo1 和 foo.foo2 导致是什么?在目前的 lua 中,必须也返回一个 userdata 。那么我们就得在 foo.foo1 时,生成一个新的 userdata 作为 proxy 来访问 foo 对象的一部分。而且一旦访问过 foo.foo1 还得 cache 住这个新生成的 userdata ,保证下次再引用时还能返回同一个 userdata 。

为了保证 foo 和 foo.foo1 的生命期绑定,也就是在 foo.foo1 还在用的时候 foo 不被回收掉,往往我们还需要在 foo.foo1 的 uservalue 中保存一份 foo 的引用。这样才可以让 gc 能正确工作。


如果我们可以给 userdata 的值(而不是 object)关联一个整数就不一样了。例如,我们用 0 表示 foo 本身,1 表示 foo.foo1 ,2 表示 foo.foo2 。当 foo 被创建出来的时候,关联值默认是 0 ,也就是指代 foo 本身。

当我们运行到 foo.foo1 时,在 index 元方法中可以返回同一个 foo userdata ,但关联上新值 1 。注意,这个 1 是跟着返回值走的,并不在 userdata object 上,所以并不影响之前的 foo 对象。

这样,同一个 foo userdata 就有了对 C 对象的多种表达,在 lua 中也可以正常的做比较,做 table 的键。

为了实现这个新特性,并不需要修改 lua 原有的定义,也不需要增加新类型。只需要把以前类型为 LUA_TUSERDATA 的值在实现上变成两个整数组。第一个整数是一个 userdata 的 handle ,而所有真正的 userdata object 指针都保存在 lua vm 的 global state 里即可。第二个整数是关联其上的 slice 值,表示这个值应用 userdata object 的哪一个切片。

这一对值可以随 lua 的赋值质量做值拷贝,而不像之前的 userdata 只是做引用拷贝,这样就可以作到对同一个 C 对象的不同表达。


我们只需要增加两个新的 C API 就够了:

int lua_getuserslice(lua_State *L, int index);
void lua_setuserslice(lua_State *L, int index, int slice);

lua_getuservalue 的用法也基本一致。

有了这个特性,封装 GUI 对象或 3d engine 中的对象要容易的多。

当我们在 lua 里写 OBJECT 和 OBJECT.X 或 OBJECT.X.Y 时,它们可以都指向同一个 userdata object ,只是 slice 不同。这样 OBJECT.X 获得 OBJECT 下的一个子节点也就轻量的多了。

Comments

@louis
一把辛酸泪啊,昨天就被这个问题卡了一天

估计Lua三巨头会拒绝,说lua是用来做嵌入小脚本的,而不是大型程序。

请教云风,如果需要绑定到LUA中的C++类型中成员有复合结构,即类型中有其他C++类型成员,此类类型的绑定实现是有有好的思路?觉得使用模板来实现泛型的类型绑定还是有些难度!

记得前一阵子还被所有lightuserdata共用一个metatable的特性坑过...元表用得少,忽略了这一点。

这个问题这几天在 lua 邮件列表被讨论的很充分了。

我最喜欢 Roberto 老大提的方案:

让每个 lightuserdata 的值(甚至所有的值)都可以拥有独立的 metatable 。

如果我理解得没错,您的意思是:

1. 需要在Lua中存储一些userdata,其内容是C的struct。
2. 需要在Lua中引用这些userdata的部分,比如

f1 = foo.foo1

而且,f1对于foo.foo1是引用语意,即:如果foo中的foo1内容发生了变化,f1可以读到;修改f1会导致foo.foo1的内容发生改变。

3. 这个引用本身需要大量拷贝,所以希望f1本身比较轻量级,尤其是不要因为拷贝了f1而创建大量的对象。
4. 希望f1可以用于table key。两个这样的东西相等当且仅当它们引用同一个userdata的同一个部分。
5. 希望这个引用可以保持这个userdata活着,也就是不希望在f1活着的时候foo已经被回收了。

--

不过,我觉得还有几点可能会影响设计决策,包括:

a. 哪个lua版本?
b. 这些userdata数量多不多?每个userdata大不大?
c. 比如f1 = foo.foo1,那么
c1. 拿到f1,是否能从f1得到foo?比如get_origin(f1) == foo?
c2. 是否只要拿到f1就可以对foo.foo1进行读写操作,而不需要同时再有foo变量?即,可否直接进行get(f1), set(f1, v),而不需要get(foo, f1)或者set(foo, f1, v)
d. 是否要频繁比较这些slice是否相等?
e. 是否要频繁对这些slice进行读写操作?
f. slice被频繁创建吗?被频繁拷贝吗?是否介意 *偶尔* 有几个不同的堆对象表示同一个slice,而它们逻辑上判断相等?

发现了什么也不重要,怎么做才重要。

编程久了会发现,智商不重要情商才重要.

Post a comment

非这个主题相关的留言请到:留言本