在面的一章,我们讨论了如何使用
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
永远不会被回收掉。)