看了2天userdata, 写了百来行代码, 才对userdata略知一二.

userdata这东西, 可以理解为用户自定义数据. 它是数据, 不是类型, 其实说白了, 就是一片内存. 通过一个简单的API, 我们就能获取一个userdata:

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

这个API一目了然, 创建好的userdata会被妥善安置在lua stack的顶部.

这里有一个很有趣的地方, 就是我们能够申请一段由lua管理的内存, 我听说lua的gc还是蛮不错的, 如果我可以把许多内存管理的工作扔给lua, 那真是太好了.  另一方面, 我觉得lua实在是不行, 还是自己管理内存比较靠谱, 但是我又需要让lua能比较直接的操作我写的C模块所申请的一片内存. 面对2种不同的需求, lua提供的机制都能够让我们一一应对.

1. 申请一片比较大的内存, 将实例放在这片内存里.

2. 申请小段内存, 在这片内存中保存实例地址, 将实例放在C/C++模块申请的内存中.

在情况1中, 一旦lua的gc回收内存, C/C++实例就被销毁. 第2种情况下, C/C++实例可以继续存在.

这两种解决方案都有可能被用来解决实际问题, 而第2种情况非常值得写一个完整的例子来研究. 不过今天只看一下第1种情况吧.

为了研究第1种情况, 我打算做一个简单的float数组. 下面是数组在C中的定义以及一些lua接口:

struct LuaArray
{
	int size;
	float data[1];	// 为了简便, 我就这么做了.
};
void InitArray(lua_State* pState);		// 这个函数不是向lua提供的接口. 只是用作初始化.
int NewArray(lua_State* pState);
int ReleaseArray(lua_State* pState);
int GetArrayValue(lua_State* pState);
int SetArrayValue(lua_State* pState);
int GetArrayLength(lua_State* pState);
int SumArray(lua_State* pState);

下面是InitArray函数的代码:

static const char* LuaArrayTableName = "LuaArray";
static const luaL_Reg ArrayFunction[] = 
{
	{"__newindex",	SetArrayValue},
	{"__len",		GetArrayLength},
	{"__gc",		ReleaseArray},
	{"get",			GetArrayValue},
	{"sum",			SumArray},
	{"new",			NewArray},
	{NULL, NULL}
};

void InitArray(lua_State* pState)
{
	luaL_register(pState, LuaArrayTableName, ArrayFunction);
	lua_pushvalue(pState, -1);
	lua_setfield(pState, -2, "__index");
	lua_pop(pState, 1);
};

我们可以采用类似MFC中消息映射的一些宏来简化LuaArrayTableName和ArrayFunction. 

这里, 我创建了一个lua table, 并让这张表本身作为一张元表, 存储于lua_State的context中.

一旦我们创建了这张元表, 就能让我们的userdata和这张元表绑定. 这样, 我们就能在lua中, 对userdata进行元表规定的操作.

先来看一下创建的代码:

int NewArray(lua_State* pState)
{
	int elemCount = luaL_checkint(pState, 1);
	int memSize = sizeof(LuaArray) + elemCount * sizeof(float);
	LuaArray* pUData = (LuaArray*)lua_newuserdata(pState, memSize);
	pUData->size = elemCount;
	pUData->data[0] = 0.0f;
	for (int i = 1; i <= elemCount; ++i)
		pUData->data[i] = 0.0f;
		
	lua_getglobal(pState, LuaArrayTableName);
	lua_setmetatable(pState, -2);
	
	// ----------------------------------------------------------------------------
	// 在gc时使用, 没有特别的意义.
	float* pExData = new float[10];
	memcpy_s((void*)pUData->data, sizeof(float), (void*)&pExData, sizeof(float*));
	// ----------------------------------------------------------------------------
	
	return 1;
}

第5行创建了userdata, 并在前端存储LuaArray结构.

在lua中, 我们用这样的代码就能创建一个LuaArray:

arr = LuaArray.new(10)	-- 创建10个元素的LuaArray

设置LuaArray中的值, 获取LuaArray中的值(省去所有检测):

int SetArrayValue( lua_State* pState )
{
	LuaArray* pUData = (LuaArray*)lua_touserdata(pState, 1);	// 这里可以做一些检测
	int idx = luaL_checkint(pState, 2);
	float val = (float)luaL_checknumber(pState, 3);
	pUData->data[idx] = val;									
	return 0;
}
int GetArrayValue( lua_State* pState )
{
	LuaArray* pUData = (LuaArray*)lua_touserdata(pState, 1);
	int idx = luaL_checkint(pState, 2);
	lua_pushnumber(pState, (lua_Number)pUData->data[idx]);
	return 1;
}

lua中设置和获取值的代码如下:

arr[1] = 100

print(arr:get(1)) // 没有arr[1]的原因在于元表中的__index属性被用来指向元表本身.

其他函数大同小异. 有意思的是__gc事件.

在lua中的变量都是引用, 当一个对象没有任何变量引用的时候, 就会被lua的gc回收.

在lua中这样写, 就会让代码回收:

arr = nil

在相应__gc事件的C/C++函数中, 我们就能对刚才申请的内存进行释放:

int ReleaseArray( lua_State* pState )
{
	LuaArray* pUData = (LuaArray*)lua_touserdata(pState, 1);
	float* pExData;
	memcpy_s((void*)&pExData, sizeof(float*), (void*)pUData->data, sizeof(float));

	delete [] pExData;

	return 0;
}

userdata+metatable的机制, 让我们能从C/C++的角度为lua提供数据和类型的扩展. 本文中对这套机制的使用方法仅仅是一个简陋的实验方法, 具体项目中可以加入许多改进以应对不同需求.

作者: glshader 发表于 2011-07-28 10:33 原文链接

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架