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 永远不会被回收掉。)

时间: 2024-12-10 11:39:58

Lua_第27章 User-Defined Types in C的相关文章

第27章 CSS传统布局(上)

第 27章 CSS传统布局[上]学习要点:1.布局模型2.表格布局3.浮动布局 本章主要探讨 HTML5中 CSS早期所使用的传统布局,很多情况下,这些布局方式还是非常有用的.一.布局模型在早期没有平板和智能手机等移动设备大行其道的时期,Web页面的设计主要是面向PC端电脑分辨率展开的.这种分辨率比例比较单一,基本上只要满足最低分辨率设计即可.一般来说有 4:3.16:10.16:9这样的主要分辨率.那么,从这种比例上来看,长度总是大于宽度的.从最低分辨率 1024 * 768设计即可.为了使浏

Lua_第19章 String 库(上)

Lua_第19章String 库 Lua解释器对字符串的支持很有限.一个程序可以创建字符串并连接字符串,但不能截取子串,检查字符串的大小,检测字符串的内容.在 Lua中操纵字符串的功能基本来自于 string 库. String 库中的一些函数是非常简单的:string.len(s)返回字符串 s 的长度;string.rep(s, n)返回重复 n 次字符串 s 的串;你使用 string.rep("a", 2^20)可以创建一个 1M bytes 的字符 串(比如,为了测试需要);

Lua_第19章 String 库(下)

Lua_第19章 String 库(下) 19.3捕获(Captures) Capture(下面译为捕获或者capture,模式中捕获的概念指,使用临时变量来保存匹配的子模式,常用于 向前引用.)是这样一种机制:可以使用模式串的一部分匹配目标串的一部分.将你想捕 获的模式用圆括号括起来,就指定了一个capture.在 string.find 使用captures 的时候,函数会返回捕获的值作为额外的结果.这常被用 来将一个目标串拆分成多个: pair = "name =Anna" _,

第27章 CSS传统布局(下)

第 27章 CSS传统布局[下]学习要点:1.定位布局2.box-sizing3.resize 本章主要探讨 HTML5中 CSS早期所使用的传统布局,很多情况下,这些布局方式还是非常有用的.一.定位布局在使用定位布局前,我们先了解一下定位属性的用法.CSS2提供了position属性来实现元素的绝对定位和相对定位. 属性 说明static 默认值,无定位.absolute 绝对定位,使用 top.right.bottom.left进行位移.relative 相对定位,使用 top.right.

Lua_第16 章 Weak 表

Lua_第16 章 Weak 表 Lua自动进行内存的管理.程序只能创建对象(表,函数等),而没有执行删除对象 的函数.通过使用垃圾收集技术,Lua 会自动删除那些失效的对象.这可以使你从内存 管理的负担中解脱出来.更重要的,可以让你从那些由此引发的大部分 BUG中解脱出 来,比如指针挂起(dangling   pointers)和内存溢出. 和其他的不同,Lua 的垃圾收集器不存在循环的问题.在使用循环性的数据结构的 时候,你无须加入特殊的操作;他们会像其他数据一样被收集.当然,有些时候即使更

Lua_第28章 资源管理(上)

Lua_第28章  资源管理 (上)  在前面一章介绍的数组实现方法,我们不必担心如何管理资源,只需要分配内存. 每一个表示数组的 userdatum 都有自己的内存,这个内存由 Lua 管理.当数组变为垃圾 (也就是说,当程序不需要)的时候,Lua 会自动收集并释放内存. 生活总是不那么如意.有时候,一个对象除了需要物理内存以外,还需要文件描述符.窗口句柄等类似的资源.(通常这些资源也是内存,但由系统的其他部分来管理). 在这种情况下,当一个对象成为垃圾并被收集的时候,这些相关的资源也应该被释

第27章解释器模式

一 概念 解释器模式,给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子. 解释器要解决的问题是,如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子,这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题. 二 UML图 Context 包含解释器之外的一些全局信息,对全局的一些内容进行描述 AbstractExpression 抽象表达式,声明一个抽象的解释操作,这个接口为抽象语法树中所有的节点

Lua_第23章 C API 纵览

第23章 C  API 纵览 Lua是一个嵌入式的语言,意味着 Lua 不仅可以是一个独立运行的程序包也可以是一个用来嵌入其他应用的程序库.你可能觉得奇怪:如果 Lua 不只是独立的程序,为什么到目前为止贯穿整本书我们都是在使用 Lua 独立程序呢? 这个问题的答案在于 Lua 解释器(可执行的 lua).Lua解释器是一个使用 Lua 标准库实现的独立的解释器,它是一 个很小的应用(总共不超过500 行的代码).解释器负责程序和使用者的接口:从使用者那里获取文件或者字符串,并传给 Lua 标准

Oracle 10.2数据库管理员指南-27章

27使用调度程序 Oracle Database provides database job capabilities through Oracle Scheduler (the Scheduler). This chapter explains how to use the various Scheduler components, and discusses the following topics: 调度程序对象和它们的命名 使用Jobs 使用Programs 使用Schedules 使用