Q:如何使用”userdata”的”metamethods”?

A:我们继续来修改上一节中的例子,这次我们的目标是使用面向对象的方式调用”userdata”的方法。这个目标既可以在Lua中实现,也可以在C库中实现,我们先来看一个比较简单的方式,在Lua中实现。”mylib.c”中代码无需更改,只需要修改”a.lua”中的代码,

local array = require "mylib"

--[[ 这里创建一个大小为1的数组,仅仅是为了获取其"metatable"。
     虽然Lua代码无法更改"userdata"的"metatable",
     但是获取"metatable"以及修改"metamethods"是不受影响的。
     因为所有通过"array.new"创建的数组,
     均使用存储在"registry"中的同一个"metatable"。
     所以这里修改了此"metatable"的"metamethods",
     下面所创建的数组也就拥有了这些"metamethods"。]]
local metaarray = getmetatable(array.new(1))
metaarray.__index = metaarray
metaarray.set = array.set
metaarray.get = array.get
metaarray.size = array.size

a = array.new(1000)
print(a)    --> userdata: 0x240f268
print(a:size())    --> 1000
for i = 1, 1000 do 
    a:set(i, 1 / i)
end
print(a:get(10))    --> 0.1

在Lua中实现的方式虽然方便,但在实际应用中却并不推荐。因为提供C库的意义就在于让使用者方便的调用提供的函数,而基于这种实现方式下,难道要在发布的C库的”Readme.txt”中说明在使用时需要增加的代码,以及暴露自己C库中定义的函数名称?
所以为了避免这些问题,我们接下来将在C库中实现”userdata”的”metamethods”。

/* 将"l"中所有的函数注册到从栈顶开始"nup"个"upvalues"下的"table"中,
 * 被注册的函数共享"nup"个"upvalues"。
 * "nup"个"upvalues"在函数执行完成后会被出栈。
 */
void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup);

“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 int array2string(lua_State *L)
{
    NumArray *a = checkarray(L);
    lua_pushfstring(L, "array(%d)", a->size);

    return 1;
}

/* 现在C库中只需要向外部提供创建数组的方法,
 * 那些需要通过对象调用的方法都以"metamethods"的形式放在了"metatable"中。
 */
static const struct luaL_Reg arraylib_f[] = {
    {"new", newarray}, 
    {NULL, NULL}
};

/* "metamethods"。
 * 因为"userdata"根本就没有"keys",所以每当调用C库中未向外部提供的方法时,
 * 都会来寻找对应的"metamethods"。
static const struct luaL_Reg arraylib_m[] = {
    // 将对象转换为字符串时,默认会调用的"metamethod"。
    {"__tostring", array2string}, 
    {"set", setarray}, 
    {"get", getarray}, 
    {"size", getsize}, 
    {NULL, NULL}
};

extern int luaopen_mylib(lua_State* L)
{
    luaL_newmetatable(L, "LuaBook.array");    // 新创建的"metatable"会入栈。

    lua_pushstring(L, "__index");
    lua_pushvalue(L, -2);    // 复制一份"metatable"再次入栈。
    lua_settable(L, -3);    // "metatable.__index = metatable"
    // 将"metamethods"都存入"metatable"中。
    luaL_setfuncs(L, arraylib_m, 0);

    luaL_newlib(L, arraylib_f);

    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"的参数会自动调用对象的"__tostring"方法,
     C库中提供了此"metamethod",所以这里不再打印"userdata: ***",
     而是打印指定格式的信息。]]
print(a)    --> array(1000)
print(a:size())    --> 1000
for i = 1, 1000 do
    a:set(i, 1 / i)
end
print(a:get(10))    --> 0.1

附加:

1、我们还可以使用常规数组的访问方式访问我们在C库中定义的数组,只需要将metaarray.__index指向getarraymetaarray.__newindex指向setarray。由于这种实现方式,metaarray.__index只能指向单一的函数,getsizearray2string这类函数还是需要定义在luaL_newlib接收的数组中。
在Lua中实现:
“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 int array2string(lua_State *L)
{
    NumArray *a = checkarray(L);
    lua_pushfstring(L, "array(%d)", a->size);

    return 1;
}

static const struct luaL_Reg arraylib[] = {
    {"new", newarray},
    {"size", getsize},
    {"set", setarray},
    {"get", getarray},
    {"tostring", array2string},
    {NULL, NULL}
};

extern int luaopen_mylib(lua_State* L)
{
    luaL_newmetatable(L, "LuaBook.array");
    luaL_newlib(L, arraylib);

    return 1;
}

“a.lua”文件中:

local array = require "mylib"

local metaarray = getmetatable(array.new(1))
metaarray.__index = array.get
metaarray.__newindex = array.set

a = array.new(1000)
print(array.tostring(a))    --> array(1000)
print(array.size(a))    --> 1000
for i = 1, 1000 do
    a[i] = (1 / i)
end
print(a[10])    --> 0.1

在C库中实现:
“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");
    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 int array2string(lua_State *L)
{
    NumArray *a = checkarray(L);
    lua_pushfstring(L, "array(%d)", a->size);

    return 1;
}

static const struct luaL_Reg arraylib[] = {
    {"new", newarray},
    {"size", getsize},
    // 数组转换为字符串的函数不再作为"metamethod",而是显式的提供给外部。
    {"tostring", array2string}, 
    {NULL, NULL}
};

extern int luaopen_mylib(lua_State* L)
{
    luaL_newmetatable(L, "LuaBook.array");

    lua_pushstring(L, "__index");
    lua_pushcfunction(L, getarray);
    lua_settable(L, -3);    // metatable.__index = getarray

    lua_pushstring(L, "__newindex");
    lua_pushcfunction(L, setarray);
    lua_settable(L, -3);    // metatable.__newindex = setarray

    luaL_newlib(L, arraylib);

    return 1;
}

“a.lua”文件中:

local array = require "mylib"

a = array.new(1000)
print(array.tostring(a))    --> array(1000)
print(array.size(a))    --> 1000
for i = 1, 1000 do
    a[i] = (1 / i)    -- 常规数组访问方式。
end
print(a[10])    --> 0.1 -- 常规数组访问方式。