Q:什么是”userdata”?
A:”userdata”分为两类,”full userdata”和”light userdata”。Lua使用他们来表示C中一些特殊的类型。前面的章节中,我们看到了如何通过C编写新的函数来扩展Lua;使用”userdata”,我们将可以通过C编写新的类新来扩展Lua。
Q:两种”userdata”的区别?
A:
\ | “full userdata” | “light userdata” |
本质 | 一段在被创建时可以指定大小的内存区域,通常用来表示C中的结构体。 | 一小段固定的内存区域,通常用来表示C中的指针( |
使用 | 需要显式的创建一块儿内存,该段内存由Lua的垃圾回收器管理,使用者无需关心。 | 无需创建内存,它就相当于一个值(就像Lua中的数值一样),它所使用的内存空间不由Lua的垃圾回收器管理,所以使用者需要关心其内存使用。 |
创建 |
|
|
其他 | 可以指定其”metatable”和”metamethods”。 | 不能指定其”metatable”和”metamethods”。 |
Q:如何使用”full userdata”?
A:我们将使用数组来举例,因为其不涉及复杂的算法。
typedef struct NumArray
{
int size;
double values[1]; // 可变部分。
} NumArray;
数组声明为一个长度只是为了占位,因为C中不允许声明长度为0的数组,在实际程序中,我们会根据指定的大小来申请合适的空间,
// "n"为指定的大小。因为"NumArray"中已包含了一个元素的大小,所以需要减去。
sizeof(NumArray) + (n - 1) * sizeof(double)
首先来看几个在程序中会用到的函数,
/* 分配一块大小为"size"的内存空间作为"full userdata"使用,
* 之后将内存空间的地址入栈,函数返回此地址。
*/
void *lua_newuserdata(lua_State *L, size_t size);
/* 检查"cond"是否为"true",如果为"false"则报错,并返回形如如下格式的错误,
* "bad argument #arg to 'funcname' (extramsg)"
*/
void luaL_argcheck (lua_State *L, int cond, int arg, const char *extramsg);
“mylib.c”文件中:
#include <stdio.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
typedef struct NumArray
{
int size;
double values[1];
} NumArray;
static int newarray(lua_State *L)
{
// 检查待创建数组大小参数是否为整数。
int n = luaL_checkinteger(L, 1);
size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double);
NumArray *a = (NumArray *)lua_newuserdata(L, nbytes);
a->size = n; // 设置数组的大小。
return 1; // 函数返回创建的"userdata"。
}
static int setarray(lua_State *L)
{
// 获取传递的数组("userdata")的地址。
NumArray *a = (NumArray *)lua_touserdata(L, 1);
// 检查传递的"key"是否为整数。
int index = luaL_checkinteger(L, 2);
// 检查传递的"value"是否为数值。
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;
}
static int getarray(lua_State *L)
{
NumArray *a = (NumArray *)lua_touserdata(L, 1);
int index = luaL_checkinteger(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}
};
extern int luaopen_mylib(lua_State* L)
{
luaL_newlib(L, arraylib);
return 1;
}
将”mylib.c”编译为动态连接库,
prompt> gcc mylib.c -fPIC -shared -o mylib.so -Wall
prompt> ls
mylib.c mylib.so a.lua
“a.lua”文件中:
local array = require "mylib"
a = array.new(1000)
print(a) --> userdata: 0x8064d48
print(array.size(a)) --> 1000
for i = 1, 1000 do
array.set(io.stdin, i, 1/i)
end
print(array.get(a, 10)) --> 0.1
Q:如何使用”userdata”的”metatables”?
A:上面例子的实现方式存在一个很大的安全漏洞。假如使用者编写了类似于如下的代码,array.set(io.stdin, 1, 0)
,将导致段错误(如果你足够幸运的话,可能会得到一个数组访问越界的错误)。因为array.set
的第一个参数接收一个”userdata”,而io.stdin
也是一个”userdata”,所以在参数检查的过程中不会报错,然而io.stdin
却并不是我们所创建的数组。
为了解决这个问题,我们需要为我们所创建的数组增加一个特殊的标志,用来与其他的”userdata”加以区分。因为Lua代码不能更改”userdata”的”metatable”,所以我们使用一个全局唯一的”metatable”来作为这个特殊的标志。
还是先来看几个在程序中会用到的函数,
/* 在"registry"中创建一个供"userdata"使用的索引为"tname"的"metatable",
* 并将此"metatable"的"__name"域设置为"tname"
* (有些错误处理函数会使用"__name"域)。"metatable"创建成功,函数返回1。
* 如果"registry"中已存在索引为"tname"的元素,则函数返回0。
* 以上两种情况,函数均会将"tname"所对应的值入栈。
*/
int luaL_newmetatable(lua_State *L, const char *tname);
/* 将"registry"中索引为"tname"的"metatable"入栈。
* 如果"registry"中没有索引为"tname"的元素,
* 或是索引"tname"所对应的元素不是"metatable",那么函数将"nil"入栈。
* 函数返回入栈值的类型。
*/
int luaL_getmetatable(lua_State *L, const char *tname);
// 从虚拟栈中弹出一个"table"作为索引"index"处值的新的"metatable"。
void lua_setmetatable(lua_State *L, int index);
/* 检查虚拟栈中索引"arg"处的值是否为一个"userdata",
* 并且此"userdata"具有一个名为"tname"的"metatable"。
* 如果是,则返回此"userdata"的地址,否则返回"NULL"。
*/
void *luaL_checkudata (lua_State *L, int arg, const char *tname);
“mylib.c”文件中:
#include <stdio.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
typedef struct NumArray
{
int size;
double values[1];
} NumArray;
// 检查传递的数组是否合法,并返回数组的地址。
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;
}
// 检查传递的参数是否合法,并返回数组中指定元素的地址。
static double *getelem(lua_State *L)
{
NumArray *a = checkarray(L);
int index = luaL_checkinteger(L, 2);
luaL_argcheck(L, 1 <= index && index <= a->size, 2,
"index out of range");
return &a->values[index - 1];
}
static int newarray(lua_State *L)
{
int n = luaL_checkinteger(L, 1);
size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double);
NumArray *a = (NumArray *)lua_newuserdata(L, nbytes);
luaL_getmetatable(L, "LuaBook.array");
// 注意,这里使用的不是"luaL_setmetatable",两个函数的功能不同。
lua_setmetatable(L, -2);
a->size = n;
return 1;
}
static int setarray(lua_State *L)
{
double value = luaL_checknumber(L, 3);
*getelem(L) = value;
return 0;
}
static int getarray(lua_State *L)
{
lua_pushnumber(L, *getelem(L));
return 1;
}
static int getsize(lua_State *L)
{
NumArray *a = checkarray(L);
lua_pushnumber(L, a->size);
return 1;
}
static const struct luaL_Reg arraylib[] = {
{"new", newarray},
{"set", setarray},
{"get", getarray},
{"size", getsize},
{NULL, NULL}
};
extern int luaopen_mylib(lua_State* L)
{
luaL_newmetatable(L, "LuaBook.array");
luaL_newlib(L, arraylib);
return 1;
}
“a.lua”中的代码保持不变,执行后会得到相同的结果。但是此时,如果你再向array.set
传入io.stdin
或是其他非法的”userdata”,将会得到明确的报错,
userdata: 0x1c960a8
1000.0
lua: a.lua:8: bad argument #1 to 'set' (LuaBook.array expected, got FILE*)
stack traceback:
[C]: in function 'mylib.set'
a.lua:8: in main chunk
[C]: in ?
附加:
1、尽管”full userdata”和”light userdata”在名字上给人感觉他们在占用资源上有很大差异。但实际上,”full userdata”也并不“昂贵”。
2、一般情况下,Lua中并不需要外部的数组,因为哈希表的一部分功能很好的实现了数组。但是对于非常大的数组而言,哈希表可能导致内存的大量浪费。
哈系表可以使用整数索引、字符串索引、”table”索引等等。但是数组只需要整数索引,而哈系表依旧分配了额外的内存,用于提供所有的功能。
在C中使用原生的数组,将比哈希表的实现方式节省50%的内存空间。