Q:什么是”userdata”?

A:”userdata”分为两类,”full userdata”和”light userdata”。Lua使用他们来表示C中一些特殊的类型。前面的章节中,我们看到了如何通过C编写新的函数来扩展Lua;使用”userdata”,我们将可以通过C编写新的类新来扩展Lua。

Q:两种”userdata”的区别?

A:

\

“full userdata”

“light userdata”

本质

一段在被创建时可以指定大小的内存区域,通常用来表示C中的结构体。

一小段固定的内存区域,通常用来表示C中的指针(void *)

使用

需要显式的创建一块儿内存,该段内存由Lua的垃圾回收器管理,使用者无需关心。

无需创建内存,它就相当于一个值(就像Lua中的数值一样),它所使用的内存空间不由Lua的垃圾回收器管理,所以使用者需要关心其内存使用。

创建

void *lua_newuserdata(lua_State *L, size_t size);

void lua_pushlightuserdata(lua_State *L, void *p);

其他

可以指定其”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%的内存空间。