第28章 User-Defined Types in C

28 User-Defined Types in C
在面的一章,我们讨论了如何使用 C 函数扩展 Lua 的功能,现在我们讨论如何使用 C 中新创建的类型来扩展 Lua 。我们从一个小例子开始,本章后续部分将以这个小例子为基础逐步加入 metamethods 等其他内容来介绍如何使用 C 中新类型扩展 Lua
我们的例子涉及的类型非常简单,数字数组。这个例子的目的在于将目光集中到 API 问题上,所以不涉及复杂的算法。尽管例子中的类型很简单,但很多应用中都会用到这种类型。一般情况下, Lua 中并不需要外部的数组,因为哈希表很好的实现了数组。但是对于非常大的数组而言,哈希表可能导致内存不足,因为对于每一个元素必须保存一个范性的( generic )值,一个链接地址,加上一些以备将来增长的额外空间。在 C 中的直接存储数字值不需要额外的空间,将比哈希表的实现方式节省 50% 的内存空间。
我们使用下面的结构表示我们的数组:
typedef struct NumArray {
    int size;
    double values[1]; /* variable part */
} NumArray;
我们使用大小 1 声明数组的 values ,由于 C 语言不允许大小为 0 的数组,这个 1 只是一个占位符;我们在后面定义数组分配空间的实际大小。对于一个有 n 个元素的数组来说,我们需要
sizeof(NumArray) + (n-1)*sizeof(double) bytes
(由于原始的结构中已经包含了一个元素的空间,所以我们从 n 中减去 1
我们首先关心的是如何在 Lua 中表示数组的值。 Lua 为这种情况提供专门提供一个基本的类型: userdata 。一个 userdatum 提供了一个在 Lua 中没有预定义操作的 raw 内存区域。
Lua API 提供了下面的函数用来创建一个 userdatum
void *lua_newuserdata (lua_State *L, size_t size);
lua_newuserdata 函数按照指定的大小分配一块内存,将对应的 userdatum 放到栈内,并返回内存块的地址。如果出于某些原因你需要通过其他的方法分配内存的话,很容易创建一个指针大小的 userdatum ,然后将指向实际内存块的指针保存到 userdatum 里。我们将在下一章看到这种技术的例子。
使用 lua_newuserdata 函数,创建新数组的函数实现如下:
static int newarray (lua_State *L) {
    int n = luaL_checkint(L, 1);
    size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double);
    NumArray *a = (NumArray *)lua_newuserdata(L, nbytes);
    a->size = n;
    return 1; /* new userdatum is already on the stack */
}
(函数 luaL_checkint 是用来检查整数的 luaL_checknumber 的变体)一旦 newarray Lua 中被注册之后,你就可以使用类似 a = array.new(1000) 的语句创建一个新的数组了。
为了存储元素,我们使用类似 array.set(array, index, value) 调用,后面我们将看到如何使用 metatables 来支持常规的写法 array[index] = value 。对于这两种写法,下面的函数是一样的,数组下标从1开始:
static int setarray (lua_State *L) {
    NumArray *a = (NumArray *)lua_touserdata(L, 1);
    int index = luaL_checkint(L, 2);
    double value = luaL_checknumber(L, 3);
 
    luaL_argcheck(L, a != NULL, 1, "`array' expected");
 
    luaL_argcheck(L, 1 <= index && index <= a->size, 2,
              "index out of range");
 
    a->values[index-1] = value;
    return 0;
}
luaL_argcheck 函数检查给定的条件,如果有必要的话抛出错误。因此,如果我们使用错误的参数调用 setarray ,我们将得到一个错误信息:
array.set(a, 11, 0)
--> stdin:1: bad argument #1 to 'set' ('array' expected)
下面的函数获取一个数组元素:
static int getarray (lua_State *L) {
    NumArray *a = (NumArray *)lua_touserdata(L, 1);
    int index = luaL_checkint(L, 2);
 
    luaL_argcheck(L, a != NULL, 1, "'array' expected");
 
    luaL_argcheck(L, 1 <= index && index <= a->size, 2,
                         "index out of range");
 
    lua_pushnumber(L, a->values[index-1]);
    return 1;
}
我们定义另一个函数来获取数组的大小:
static int getsize (lua_State *L) {
    NumArray *a = (NumArray *)lua_touserdata(L, 1);
    luaL_argcheck(L, a != NULL, 1, "`array' expected");
    lua_pushnumber(L, a->size);
    return 1;
}
最后,我们需要一些额外的代码来初始化我们的库:
static const struct luaL_reg arraylib [] = {
    {"new", newarray},
    {"set", setarray},
    {"get", getarray},
    {"size", getsize},
    {NULL, NULL}
};
 
int luaopen_array (lua_State *L) {
    luaL_openlib(L, "array", arraylib, 0);
    return 1;
}
这儿我们再次使用了辅助库的 luaL_openlib 函数,他根据给定的名字创建一个表,并使用 arraylib 数组中的 name-function 对填充这个表。
打开上面定义的库之后,我们就可以在 Lua 中使用我们新定义的类型了:
a = array.new(1000)
print(a)                 --> userdata: 0x8064d48
print(array.size(a))     --> 1000
for i=1,1000 do
    array.set(a, i, 1/i)
end
print(array.get(a, 10)) --> 0.1
在一个 Pentium/Linux 环境中运行这个程序,一个有 100K 元素的数组大概占用 800KB 的内存,同样的条件由 Lua 表实现的数组需要 1.5MB 的内存。
我们上面的实现有一个很大的安全漏洞。假如使用者写了如下类似的代码: array.set(io.stdin, 1, 0) io.stdin 中的值是一个带有指向流 (FILE*) 的指针的 userdatum 。因为它是一个 userdatum ,所以 array.set 很乐意接受它作为参数,程序运行的结果可能导致内存 core dump (如果你够幸运的话,你可能得到一个访问越界( index-out-of-range )错误)。这样的错误对于任何一个 Lua 库来说都是不能忍受的。不论你如何使用一个 C 库,都不应该破坏 C 数据或者从 Lua 产生 core dump
为了区分数组和其他的 userdata ,我们单独为数组创建了一个 metatable (记住 userdata 也可以拥有 metatables )。下面,我们每次创建一个新的数组的时候,我们将这个单独的 metatable 标记为数组的 metatable 。每次我们访问数组的时候,我们都要检查他是否有一个正确的 metatable 。因为 Lua 代码不能改变 userdatum metatable ,所以他不会伪造我们的代码。
我们还需要一个地方来保存这个新的 metatable ,这样我们才能够当创建新数组和检查一个给定的 userdatum 是否是一个数组的时候,可以访问这个 metatable 。正如我们前面介绍过的,有两种方法可以保存 metatable :在 registry 中,或者在库中作为函数的 upvalue 。在 Lua 中一般习惯于在 registry 中注册新的 C 类型,使用类型名作为索引, metatable 作为值。和其他的 registry 中的索引一样,我们必须选择一个唯一的类型名,避免冲突。我们将这个新的类型称为 "LuaBook.array"
辅助库提供了一些函数来帮助我们解决问题,我们这儿将用到的前面未提到的辅助函数有:
int   luaL_newmetatable (lua_State *L, const char *tname);
void luaL_getmetatable (lua_State *L, const char *tname);
void *luaL_checkudata (lua_State *L, int index,
                                       const char *tname);
luaL_newmetatable 函数创建一个新表(将用作 metatable ),将新表放到栈顶并建立表和 registry 中类型名的联系。这个关联是双向的:使用类型名作为表的 key ;同时使用表作为类型名的 key (这种双向的关联,使得其他的两个函数的实现效率更高)。 luaL_getmetatable 函数获取 registry 中的 tname 对应的 metatable 。最后, luaL_checkudata 检查在栈中指定位置的对象是否为带有给定名字的 metatable usertatum 。如果对象不存在正确的 metatable ,返回 NULL (或者它不是一个 userdata );否则,返回 userdata 的地址。
下面来看具体的实现。第一步修改打开库的函数,新版本必须创建一个用作数组 metatable 的表:
int luaopen_array (lua_State *L) {
    luaL_newmetatable(L, "LuaBook.array");
    luaL_openlib(L, "array", arraylib, 0);
    return 1;
}
第二步,修改 newarray ,使得在创建数组的时候设置数组的 metatable
static int newarray (lua_State *L) {
    int n = luaL_checkint(L, 1);
    size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double);
    NumArray *a = (NumArray *)lua_newuserdata(L, nbytes);
 
    luaL_getmetatable(L, "LuaBook.array");
    lua_setmetatable(L, -2);
 
    a->size = n;
    return 1; /* new userdatum is already on the stack */
}
lua_setmetatable 函数将表出栈,并将其设置为给定位置的对象的 metatable 。在我们的例子中,这个对象就是新的 userdatum
最后一步, setarray getarray getsize 检查他们的第一个参数是否是一个有效的数组。因为我们打算在参数错误的情况下抛出一个错误信息,我们定义了下面的辅助函数:
static NumArray *checkarray (lua_State *L) {
    void *ud = luaL_checkudata(L, 1, "LuaBook.array");
    luaL_argcheck(L, ud != NULL, 1, "`array' expected");
    return (NumArray *)ud;
}
使用 checkarray ,新定义的 getsize 是更直观、更清楚:
static int getsize (lua_State *L) {
    NumArray *a = checkarray(L);
    lua_pushnumber(L, a->size);
    return 1;
}
由于 setarray getarray 检查第二个参数 index 的代码相同,我们抽象出他们的共同部分,在一个单独的函数中完成:
static double *getelem (lua_State *L) {
    NumArray *a = checkarray(L);
    int index = luaL_checkint(L, 2);
 
    luaL_argcheck(L, 1 <= index && index <= a->size, 2,
              "index out of range");
 
    /* return element address */
    return &a->values[index - 1];
}
使用这个 getelem ,函数 setarray getarray 更加直观易懂:
static int setarray (lua_State *L) {
    double newvalue = luaL_checknumber(L, 3);
    *getelem(L) = newvalue;
    return 0;
}
 
static int getarray (lua_State *L) {
    lua_pushnumber(L, *getelem(L));
    return 1;
}
现在,假如你尝试类似 array.get(io.stdin, 10) 的代码,你将会得到正确的错误信息:
error: bad argument #1 to 'getarray' ('array' expected)
28.3 访问面向对象的数据
下面我们来看看如何定义类型为对象的 userdata ,以致我们就可以使用面向对象的语法来操作对象的实例,比如:
a = array.new(1000)
print(a:size())      --> 1000
a:set(10, 3.4)
print(a:get(10))     --> 3.4
记住 a:size() 等价于 a.size(a) 。所以,我们必须使得表达式 a.size 调用我们的 getsize 函数。这儿的关键在于 __index 元方法( metamethod )的使用。对于表来说,不管什么时候只要找不到给定的 key ,这个元方法就会被调用。对于 userdata 来讲,每次被访问的时候元方法都会被调用,因为 userdata 根本就没有任何 key
假如我们运行下面的代码:
local metaarray = getmetatable(array.new(1))
metaarray.__index = metaarray
metaarray.set = array.set
metaarray.get = array.get
metaarray.size = array.size
第一行,我们仅仅创建一个数组并获取他的 metatable metatable 被赋值给 metaarray (我们不能从 Lua 中设置 userdata metatable ,但是我们在 Lua 中无限制的访问 metatable )。接下来,我们设置 metaarray.__index metaarray 。当我们计算 a.size 的时候, Lua 在对象 a 中找不到 size 这个键值,因为对象是一个 userdatum 。所以, Lua 试着从对象 a metatable __index 域获取这个值,正好 __index 就是 metaarray 。但是 metaarray.size 就是 array.size, 因此 a.size(a) 如我们预期的返回 array.size(a)
当然,我们可以在 C 中完成同样的事情,甚至可以做得更好:现在数组是对象,他有自己的操作,我们在表数组中不需要这些操作。我们实现的库唯一需要对外提供的函数就是 new ,用来创建一个新的数组。所有其他的操作作为方法实现。 C 代码可以直接注册他们。
getsize getarray setarray 与我们前面的实现一样,不需要改变。我们需要改变的只是如何注册他们。也就是说,我们必须改变打开库的函数。首先,我们需要分离函数列表,一个作为普通函数,一个作为方法:
static const struct luaL_reg arraylib_f [] = {
    {"new", newarray},
    {NULL, NULL}
};
 
static const struct luaL_reg arraylib_m [] = {
    {"set", setarray},
    {"get", getarray},
    {"size", getsize},
    {NULL, NULL}
};
新版本打开库的函数 luaopen_array ,必须创建一个 metatable ,并将其赋值给自己的 __index 域,在那儿注册所有的方法,创建并填充数组表:
int luaopen_array (lua_State *L) {
    luaL_newmetatable(L, "LuaBook.array");
 
    lua_pushstring(L, "__index");
    lua_pushvalue(L, -2);    /* pushes the metatable */
    lua_settable(L, -3); /* metatable.__index = metatable */
 
    luaL_openlib(L, NULL, arraylib_m, 0);
 
    luaL_openlib(L, "array", arraylib_f, 0);
    return 1;
}
这里我们使用了 luaL_openlib 的另一个特征,第一次调用,当我们传递一个 NULL 作为库名时, luaL_openlib 并没有创建任何包含函数的表;相反,他认为封装函数的表在栈内,位于临时的 upvalues 的下面。在这个例子中,封装函数的表是 metatable 本身,也就是 luaL_openlib 放置方法的地方。第二次调用 luaL_openlib 正常工作:根据给定的数组名创建一个新表,并在表中注册指定的函数(例子中只有一个函数 new )。
下面的代码,我们为我们的新类型添加一个 __tostring 方法,这样一来 print(a) 将打印数组加上数组的大小,大小两边带有圆括号(比如, array(1000) ):
int array2string (lua_State *L) {
    NumArray *a = checkarray(L);
    lua_pushfstring(L, "array(%d)", a->size);
    return 1;
}
函数 lua_pushfstring 格式化字符串,并将其放到栈顶。为了在数组对象的 metatable 中包含 array2string ,我们还必须在 arraylib_m 列表中添加 array2string
static const struct luaL_reg arraylib_m [] = {
    {"__tostring", array2string},
    {"set", setarray},
    ...
};
28.4 访问数组
除了上面介绍的使用面向对象的写法来访问数组以外,还可以使用传统的写法来访问数组元素,不是 a:get(i) ,而是 a[i] 。对于我们上面的例子,很容易实现这个,因为我们的 setarray getarray 函数已经依次接受了与他们的元方法对应的参数。一个快速的解决方法是在我们的 Lua 代码中正确的定义这些元方法:
local metaarray = getmetatable(newarray(1))
metaarray.__index = array.get
metaarray.__newindex = array.set
(这段代码必须运行在前面的最初的数组实现基础上,不能使用为了面向对象访问的修改的那段代码)
我们要做的只是使用传统的语法:
a = array.new(1000)
a[10] = 3.4       -- setarray
print(a[10])      -- getarray   --> 3.4
如果我们喜欢的话,我们可以在我们的 C 代码中注册这些元方法。我们只需要修改我们的初始化函数:
int luaopen_array (lua_State *L) {
    luaL_newmetatable(L, "LuaBook.array");
    luaL_openlib(L, "array", arraylib, 0);
 
    /* now the stack has the metatable at index 1 and
        'array' at index 2 */
    lua_pushstring(L, "__index");
    lua_pushstring(L, "get");
    lua_gettable(L, 2); /* get array.get */
    lua_settable(L, 1); /* metatable.__index = array.get */
 
    lua_pushstring(L, "__newindex");
    lua_pushstring(L, "set");
    lua_gettable(L, 2); /* get array.set */
    lua_settable(L, 1); /* metatable.__newindex = array.set */
 
    return 0;
}
到目前为止我们使用的 userdata 称为 full userdata Lua 还提供了另一种 userdata: light userdata
一个 light userdatum 是一个表示 C 指针的值(也就是一个 void * 类型的值)。由于它是一个值,我们不能创建他们(同样的,我们也不能创建一个数字)。可以使用函数 lua_pushlightuserdata 将一个 light userdatum 入栈:
void lua_pushlightuserdata (lua_State *L, void *p);
尽管都是 userdata light userdata full userdata 有很大不同。 Light userdata 不是一个缓冲区,仅仅是一个指针,没有 metatables 。像数字一样, light userdata 不需要垃圾收集器来管理她。
有些人把 light userdata 作为一个低代价的替代实现,来代替 full userdata ,但是这不是 light userdata 的典型应用。首先,使用 light userdata 你必须自己管理内存,因为他们和垃圾收集器无关。第二,尽管从名字上看有轻重之分,但 full userdata 实现的代价也并不大,比较而言,他只是在分配给定大小的内存时候,有一点点额外的代价。
Light userdata 真正的用处在于可以表示不同类型的对象。当 full userdata 是一个对象的时候,它等于对象自身;另一方面, light userdata 表示的是一个指向对象的指针,同样的,它等于指针指向的任何类型的 userdata 。所以,我们在 Lua 中使用 light userdata 表示 C 对象。
看一个典型的例子,假定我们要实现: Lua 和窗口系统的绑定。这种情况下,我们使用 full userdata 表示窗口(每一个 userdatum 可以包含整个窗口结构或者一个有系统创建的指向单个窗口的指针)。当在窗口有一个事件发生(比如按下鼠标),系统会根据窗口的地址调用专门的回调函数。为了将这个回调函数传递给 Lua ,我们必须找到表示指定窗口的 userdata 。为了找到这个 userdata, 我们可以使用一个表:索引为表示窗口地址的 light userdata ,值为在 Lua 中表示窗口的 full userdata 。一旦我们有了窗口的地址,我们将窗口地址作为 light userdata 放到栈内,并且将 userdata 作为表的索引存到表内。(注意这个表应该有一个 weak 值,否则,这些 full userdata 永远不会被回收掉。)
 
 
 

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值