Lua_第27章 User-Defined Types in C

Lua_第27章  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)
28.1 Userdata
       我们首先关心的是如何在 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 */
}<span style="font-size:14px; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>

        (函数 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 的内存。

28.2 Metatables

       我们上面的实现有一个很大的安全漏洞。假如使用者写了如下类似的代码: 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 中类型名的联系。这个关联是q双向的:使用类型名作为表的 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 元方法Cmetamethod)的使用。对于表来说,不管什么 时候只要找不到给定的 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 代码中注册这些元方法。我们只需要修改 我们的初始化函数:

<pre name="code" class="csharp">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;
}
 
  28.5 Light Userdata  
 
       到目前为止我们使用的 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 永远不会被回收掉。)

你可能感兴趣的:(lua)