(一)通过一个简单的样例来看一下userdata的使用方法:

写一个C的Lua库,让Lua可以訪问C的数组。借助userdata来实现。

(1)VS中新建一个DLLproject,设置好lua库的包括文件夹、链接库;

(2)新建一个源文件main.cpp,代码例如以下:

#include <stdio.h>
#include <string.h>

extern "C"
{
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}

typedef struct NumArray
{
int size;
double values[1];
}NumArray;

// lua语句:newarray(size)
extern "C" 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; // 新建的userdata会压栈
}

// lua语句:setarray(userdata, index, value)
extern "C" 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 excepted");
luaL_argcheck(L, index >= 1 && index <= a->size, 2, "index out of range");

a->values[index - 1] = value;
return 0;
}

// lua语句:getarray(userdata, index)
extern "C" 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 excepted");
luaL_argcheck(L, index >= 1 && index <= a->size, 2, "index out of range");

lua_pushnumber(L, a->values[index - 1]);
return 1;
}

// lua语句:getsize(userdata)
extern "C" int getsize(lua_State* L)
{
NumArray* a = (NumArray*)lua_touserdata(L, 1);
luaL_argcheck(L, a != NULL, 1, "array excepted");
lua_pushnumber(L, a->size);
return 1;
}

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

extern "C" __declspec(dllexport)
int luaopen_array(lua_State* L)
{
const char* libName = "array";
luaL_register(L, libName, arraylib);
return 1;
}


(3)编译生成名为array.dll的文件。并将array.dll放在luaforwindows的clibs子文件夹下,该文件夹下都是为lua写的c库,或者将其放到本地注冊的Lua环境变量的某个文件夹下;

(4)lua測试:


require "array"

a = array.new(100)
print(array.size(a))

for i = 1, 100 do
array.set(a, i, i)
end

print(array.get(a, 10))


上面代码中的关键函数:

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

  新建full userdata。

  (1)分配一块指定大小的内存;

  (2)将该full userdata压栈;

  (3)返回该内存块的地址给主机程序,主机程序可以任意使用这块内存。

  void luaL_argcheck (lua_State *L,int cond,int arg,const char *extramsg);

  检查条件是否满足。

(二)利用metatable标识userdata来添加代码的安全性

  上面的C库是有缺陷的,比方我们怎么确保样例中setarray的第一个參数就是我们想要的数组userdata,而不是别的不相关的userdata呢?userdata是一种lua类型,它能够用来表示宿主语言中的各种自己定义类型对象,为了区分特定类型,我们使用的方法是:

  我们单独为该数组创建一个metatable,每次创建数组userdata时,我们设置其和metatable的关联。每次我们訪问数组的时候,都检查一下其是否有一个正确的metatable就可以。也就是利用不同的metatable来标记不同类型的userdata。

由于Lua代码不可以改变userdata的metatable。所以Lua不会伪造我们的代码。

  所以我们对上面的样例进行一些改进,给数组userdata加入一个类型标识。C库代码例如以下:

#include <stdio.h>
#include <string.h>

extern "C"
{
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}

typedef struct NumArray
{
int size;
double values[1];
}NumArray;

// lua语句:newarray(size)
extern "C" 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);

// 获取预先创建好的metatable,并设置给新建的userdata
luaL_getmetatable(L, "LuaBook.array");
lua_setmetatable(L, -2);

a->size = n;
return 1; // 新建的userdata会压栈
}

// 辅助函数,检查数组userdata的metatable是否为LuaBook.array(可理解为是否是LuaBook.array类型的userdat)
static NumArray* checkarray(lua_State* L)
{
void* ud = luaL_checkudata(L, 1, "LuaBook.array");
luaL_argcheck(L, ud != NULL, 1, "array expcected");
return (NumArray*)ud;
}

// 辅助函数,获取索引处的指针
static double* getelem(lua_State* L)
{
NumArray* a = checkarray(L);
int index = luaL_checkint(L, 2);
luaL_argcheck(L, index >= 1 && index <= a->size, 2, "index out of range");
return &a->values[index - 1];
}

// lua语句:setarray(userdata, index, value)
extern "C" int setarray(lua_State* L)
{
double newvalue = luaL_checknumber(L, 3);
*getelem(L) = newvalue;
return 0;
}

// lua语句:getarray(userdata, index)
extern "C" int getarray(lua_State* L)
{
lua_pushnumber(L, *getelem(L));
return 1;
}

// lua语句:getsize(userdata)
extern "C" 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 "C" __declspec(dllexport)
int luaopen_array(lua_State* L)
{
// 创建数组userdata将要用到的metatable
luaL_newmetatable(L, "LuaBook.array");

const char* libName = "array";
luaL_register(L, libName, arraylib);
return 1;
}


上面代码中的关键函数:

  void luaL_newmetatable (lua_State *L, const char *tname);

  创建userdata可用的metatable。

  假设registry已经有tnme键值。则函数返回0;

  否则,创建一个[tname, metatable]。并放入registry。并返回1。

  两种情况下。都会讲tname相应的值入栈。

  堆栈+1

  void *luaL_checkudata (lua_State *L, int index, const char *tname);

  检查在栈中指定位置的对象是否为带有给定名字的metatable(registry中键tname相应的值)的usertata。是则返回userdata地址,否则返回NULL。

  void luaL_getmetatable (lua_State *L, const char *tname);

  获取registry中的tname相应的metatable,并入栈。注意区分lua_getmetatable函数。

  void luaL_setmetatable (lua_State *L, const char *tname);

  将栈顶对象的metatable设置为registry表中键tname相应的值。注意区分lua_setmetatable函数。

  int lua_getmetatable (lua_State *L, int index);

  获取index相应的table的metatable,并入栈。假设该table没有metatable,则返回0,且堆栈不变。

  void lua_setmetatable (lua_State *L, int index);

  将栈顶的table出栈并设置给index处的值作为metatable。

  堆栈-1

 (三)将上面的代码改造成面向对象的方式

  类型为对象的userdata,能够使用例如以下的语法来操作对象的实例:

require "array"

a = array.new(100)

print(getmetatable(a))

print(a:size())

for i = 1, 100 do
a:set(i, i)
end

print(a:get(10))


思路大致例如以下:

  (1)array表仅仅包括一个方法,也就是用来生成数组对象的new方法;

  (2)数组userdata带有metatable用于类型识别;

  (3)userdata的metatable定义__index,那么,每当訪问数组的方法时。都会触发__index这个metamethod(对于userdata来讲。每次被訪问的时候元方法都会被调用,由于userdata根本就没有不论什么key)。

  (4)将metatable.__index设为该表metatable本身(__index能够为函数或者表,这里使用后者)。

  (5)metatable包括其余全部的数组操作函数。

  那么每当调用userdata的某个方法时。比方a:size(),它等同于a.size(a)。这时会触发userdata的名为__index的metamethod,metatable的__index就是它本身,而metatable表中有size域,所以调用metatable的size(a)函数。就ok了。

#include <stdio.h>
#include <string.h>

extern "C"
{
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}

typedef struct NumArray
{
int size;
double values[1];
}NumArray;

// lua语句:newarray(size)
extern "C" 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);

// 获取预先创建好的metatable,并设置给新建的userdata
luaL_getmetatable(L, "LuaBook.array");
lua_setmetatable(L, -2);

a->size = n;
return 1; // 新建的userdata会压栈
}

// 辅助函数,检查数组userdata的metatable是否为LuaBook.array(可理解为是否是LuaBook.array类型的userdat)
static NumArray* checkarray(lua_State* L)
{
void* ud = luaL_checkudata(L, 1, "LuaBook.array");
luaL_argcheck(L, ud != NULL, 1, "array expcected");
return (NumArray*)ud;
}

// 辅助函数。获取索引处的指针
static double* getelem(lua_State* L)
{
NumArray* a = checkarray(L);
int index = luaL_checkint(L, 2);
luaL_argcheck(L, index >= 1 && index <= a->size, 2, "index out of range");
return &a->values[index - 1];
}

// lua语句:setarray(userdata, index, value)
extern "C" int setarray(lua_State* L)
{
double newvalue = luaL_checknumber(L, 3);
*getelem(L) = newvalue;
return 0;
}

// lua语句:getarray(userdata, index)
extern "C" int getarray(lua_State* L)
{
lua_pushnumber(L, *getelem(L));
return 1;
}

// lua语句:getsize(userdata)
extern "C" int getsize(lua_State* L)
{
NumArray* a = checkarray(L);
lua_pushnumber(L, a->size);
return 1;
}

// metatable的tostring函数
int array2string(lua_State* L)
{
NumArray* a = checkarray(L);
lua_pushfstring(L, "array(%d)", a->size);
return 1;
}

// 表本身仅仅包括一个new方法
static const struct luaL_reg arraylib_f[] =
{
{"new", newarray},
{NULL, NULL}
};

// 这些方法注冊在metatable里面
static const struct luaL_reg arraylib_m[] =
{
{"__tostring", array2string},
{"set", setarray},
{"get", getarray},
{"size", getsize},
{NULL, NULL}
};

extern "C" __declspec(dllexport)
int luaopen_array(lua_State* L)
{
// 创建数组userdata将要用到的metatable
luaL_newmetatable(L, "LuaBook.array");

// 设置metatable的__index为metatable本身
lua_pushstring(L, "__index");
lua_pushvalue(L, -2);
lua_settable(L, -3);
// 注冊metatable的函数
luaL_register(L, NULL, arraylib_m);

// 创建array表。仅仅有一个new函数
luaL_register(L, "array", arraylib_f);
return 1;
}


(四)以数组下标的形式訪问

  如何实现支持下表操作的语法来訪问userdata呢,就像以下一样:


require "array"

a = array.new(100)
a[10] = 3
print(a[10])


  能够直接在lua中通过下面代码实现:


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



  相应到C中的实现方式例如以下:


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

extern "C" __declspec(dllexport)
int luaopen_array(lua_State* L)
{
// 创建数组userdata将要用到的metatable
luaL_newmetatable(L, "LuaBook.array");
luaL_register(L,"array",arraylib);

// 那么如今metatable在栈底,array表在其上的位置
// metatable.__index = array.get
lua_pushliteral(L, "__index");
lua_pushliteral(L, "get");
lua_gettable(L, 2);
lua_settable(L, 1);

// metatable.__index = array.set
lua_pushliteral(L, "__newindex");
lua_pushliteral(L, "set");
lua_gettable(L, 2);
lua_settable(L, 1);

return 0;
}



  将metatable的__index设为array的get方法,__newindex设为set方法就可以。在读取a[i]的时候会触发__index,并将对象本身和參数同一时候传递给__index相应的函数,写a[i]的时候原理一致。

(五)light userdata

  light userdata不同于full userdata。它有例如以下特点:

  (1)full userdata代表Lua中的C对象,light userdata代表一个C指针的值(也就是一个void *类型的值)。因为它是一个值,我们不能创建他们(相同的,我们也不能创建一个数字)。

  (2)不过一个指针。像数字一样。没有metatables,light userdata不须要垃圾收集器来管理她。

  (3)能够用于表示不同类型的对象。我们在Lua中使用light userdata表示C对象。

  由于它是一个值。不论什么指向同一个C地址的light userdata都相等。

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

  将一个light userdata入栈。

 (六)userdata相关的资源释放

  Lua以__gc元方法的方式提供了finalizers。这个元方法仅仅对userdata类型的值有效。当一个userdata将被收集的时候,而且userdata有一个__gc域,Lua会调用这个域的值(应该是一个函数):以userdata作为这个函数的參数调用。这个函数负责释放与userdata相关的全部资源。比方说文件描写叙述符、窗体句柄等。