基于CClosure实现lua访问C++对象成员

在软件实现中,一个对象通常有一个handle,在C/C++语言中最为常见的指针也可以看做是一个handle。在和操作系统交互时,这个handle就可能是一个文件的描述符。当C++和lua交互时,同样需要一个handle作为某个对象(object)的标识。lua支持的基本类型中,直观上比较合适的就是lightuserdata和userdata,而且lightuserdata更直观,因为它包含一个指针,非常适合用来存储C++对象的地址。

但是当我们真正使用这个类型的时候就会发现,这个“light”相对于完整的“userdata”有一个很大的限制:这个限制不是在于light不支持复杂结构,而是在于“light不支持metatable”(这一点是我自己尝试使用lua完成对C++面向对象时遇到的第一个问题)。这也就相当于lua的设计对这个字段本身认为是一个比较简单的字段,它不包括方法(或者类似C语言中的struct类型——没有成员函数,只是数据的聚合),而userdata是lua从语言层面提供的支持C++对象的“官方机制”。这一点其实也比较容易理解,lua语言的流行最早就是在游戏中开始流行(具体来说,就是在暴雪的魔兽世界游戏的推动下开始流行),而游戏的前后台大部分又都是使用C++语言开发(游戏的实时性要求),所以lua对于C++语言提供内置原生策略也是自然的事情。

那么为什么支持metatable这个功能对于数据结构这么重要呢?因为当访问一个用户数据的时候,只有通过metatable才能回调到用户自定义的__index接口,而这个是lua和C++对象之间交互的一个一级入口。有了metatable的支持,那么我们就可以尝试实现一个三叶虫级别的原始实现版本。

二、实现数据成员(data member)的访问

这个例子中,在创建一个对象的时候,我们通过userdata以及它支持的metatable机制,在userdata中存储了C++创建对象的地址,并且设置它的metatable中的__index为我们定义的Index函数。在Index函数中通过查表找到这些C++结构字段的值(因为只有C++编译器知道内存的布局)。对lua来说,C++只是一个接口,给出变量返回数值即可。

tsecer@harry :cat main.cpp

extern "C"

{

#include <lua.h>

#include <lualib.h>

#include <lauxlib.h>

}

#include <stdio.h>

#include <string.h> //strcmp

struct AA

{

AA(int x, int y):

f1(x), f2(y)

{}

int f1, f2;

int fun1(int x, int y)

{

return x + y;

}

};

struct AAProp

{

virtual int GetFiled(AA *pObj, lua_State *L)

{

return 0;

}

};

struct AAField: public AAProp

{

int AA::*pint;

AAField(decltype(pint) disp):

pint(disp)

{}

virtual int GetFiled(AA *pObj, lua_State *L) override

{

lua_pushnumber(L, pObj->*pint);

return 1;

}

};

using StructFunc = int (AA::*)(int, int);

struct AAFunc:public AAProp

{

AAFunc(StructFunc func):

m_func(func)

{}

StructFunc m_func;

virtual int GetFiled(AA *pobj, lua_State *L) override

{

return 0;

}

};

struct AAPropDesc

{

const char *szName;

AAProp *pProp;

};

AAPropDesc m_Props[] =

{

{"f1", new AAField(&AA::f1)},

{"f2", new AAField(&AA::f2)},

{"func1", new AAFunc(&AA::fun1)}

};

AAPropDesc *GetDescByName(const char *szName)

{

for (auto &prop : m_Props)

{

if (strcmp(prop.szName, szName) == 0)

{

return &prop;

}

}

return nullptr;

}

int Index(lua_State *L)

{

//第一个参数为usrdata

AA *pObj = *(AA**)lua_touserdata(L, 1);

//第二个参数为key

const char * szKey = lua_tostring(L, 2);

printf("objptr %p, key %s\n", pObj, szKey);

AAPropDesc *pDesc = GetDescByName(szKey);

if (pDesc == nullptr)

{

printf("cannot find field %s\n", szKey);

return 0;

}

return pDesc->pProp->GetFiled(pObj, L);

}

int NewIndex(lua_State *L)

{

return 0;

}

int CreateObject(lua_State *L)

{

//函数的第一个参数为字符串名称的类名称

const char * pClassName = lua_tostring(L, 1);

if (strcmp(pClassName, "AA"))

{

printf("invalid pClassName %s\n", pClassName);

return 0;

}

//创建C++中对象

AA *pObj = new AA(1111, 2222);

printf("create object %p\n", pObj);

//将对象地址保存到创建用户数据中

*((AA**)lua_newuserdata(L, sizeof(pObj))) = pObj;

//这里就是我们使用userdata的原因:就是为了使用该接口支持metatable的功能

//所以在创建对象之后,还是要为对象设置一个metatable结构

//由于lua中的metatable也是一个简单的table,所以这个地方首先直接创建一个普通的table

lua_createtable(L, 0, 0);

//创建metatable中的__index方法

lua_pushstring(L, "__index");

//创建该方法对应的C函数

lua_pushcfunction(L, Index);

//将新生成的metatable的__index设置为新创建的cfunction

lua_rawset(L, -3);

//将生成函数设置为新创建对象的metatable

lua_setmetatable(L, -2);

//告诉lua函数有一个返回值

return 1;

}

int main(int argc, char ** argv) {

lua_State *L = luaL_newstate();

luaL_openlibs(L);

lua_register(L, "CreateObject", CreateObject);

const char *luascript = "pobj = CreateObject(\"AA\");"

"print(pobj.f1, pobj.f2);";

// "luasquare = function(x) return x * x, x * 2; end";

if (luaL_loadstring(L, luascript) == LUA_OK) {

if (lua_pcall(L, 0, 0, 0) == LUA_OK) {

lua_pop(L, lua_gettop(L));

}

}

else

{

printf("load failed\n");

return -1;

}

lua_close(L);

return 0;

}

tsecer@harry :make

make: “a.out”是最新的。

tsecer@harry :make -B

g++ -I /home/tsecer/src/ main.cpp -L /home/tsecer/src/ -llua -ldl -g

tsecer@harry :./a.out

create object 0x659900

objptr 0x659900, key f1

objptr 0x659900, key f2

1111.0 2222.0

tsecer@harry :

三、实现函数成员(method member)的访问

如果使用上面的方法来完成对于类的成员函数调用,前面的实现看起来就有些单薄了:因为lua中定义的cfunction只有一个函数地址,而我们调用一个类成员函数的时候还要知道它的对象。也就是我们调用pobj->func1的时候,其实是编译器隐式的传入了一个this指针。当使用lua的时候,这个隐含的指针就暴露出来、需要自己实现了。好在除了内置closure和标准CFunction之外,lua还提供了CClosure类型。顾名思义,就是closure提供了“闭包”的功能。之前已经提到过,这个所谓的闭包就是通过upvalue实现,也就是说,lua中的CClosure就是在C函数的基础上可以定义额外的、自定义的upvalue,这个其实就相当于为函数绑定了变量,而这其实也是C++面向对象的实质。

回到这里的问题,我们可以将对象指针作为cfunction的upvalue,从而让一个函数本身包含了函数指针。

在下面的实现中,我们在为func1生成cclosure函数自带了两个upvalue,这里再次强调:当一个函数带了变量之后,它拥有了C++中类的成员函数的功能了。就像“妖要是有了人性,就不再是妖了一样”,当一个函数有了自带变量之后,一个函数就不再是函数了,而是闭包(closure)。

当然下面的实现方法远远算不上通用,更谈不上优雅,这里只是展示lua为了适配C++的面向对象机制而适配的机制,使用这些机制加上C++的变参模板,实现一个通用的C++绑定lua脚本的功能应该是可以实现的。

tsecer@harry :cat main.cpp

extern "C"

{

#include <lua.h>

#include <lualib.h>

#include <lauxlib.h>

}

#include <stdio.h>

#include <string.h> //strcmp

//测试类,尝试在lua中访问这个结构的各个成员

struct AA

{

AA(int x, int y):

f1(x), f2(y)

{}

int f1, f2;

int fun1(int x, int y)

{

return f1 * x + f2 * y;

}

};

//AA类一个属性的描述类,定义为基类,从而可以将变量和函数放在同一个数组中

struct AAProp

{

virtual int GetField(AA *pObj, lua_State *L)

{

return 0;

}

};

//AA类的一个数据成员(只支持int类型)

struct AAField: public AAProp

{

int AA::*pint;

AAField(decltype(pint) disp):

pint(disp)

{}

//成员变量的访问比较简单,直接返回变量内容即可

virtual int GetField(AA *pObj, lua_State *L) override

{

lua_pushnumber(L, pObj->*pint);

return 1;

}

};

//AA类的一个函数成员(只支持 int(*)(int, int)类型)

using StructFunc = int (AA::*)(int, int);

struct AAFunc:public AAProp

{

AAFunc(StructFunc func):

m_func(func)

{}

StructFunc m_func;

//将成员函数转换为静态函数,从而可以和lua的cfunction绑定

static int luaGetField(lua_State *L)

{

//从upvalue中获得对象指针

AA *pObj = (AA*)lua_touserdata(L, lua_upvalueindex(1));

AAFunc * pSelf = (AAFunc*)lua_touserdata(L, lua_upvalueindex(2));

//函数的第一个和第二个参数表示调用参数

lua_pushinteger(L, (pObj->*pSelf->m_func)(lua_tointeger(L, 1), lua_tointeger(L, 2)));

return 1;

}

//完成lua中查找成员功能,成员函数的访问比成员变量访问更复杂一些,因为返回之后这个函数还要被再次调用

virtual int GetField(AA *pobj, lua_State *L) override

{

lua_pushlightuserdata(L, pobj);

lua_pushlightuserdata(L, this);

lua_pushcclosure(L, luaGetField, 2);

return 1;

}

};

//AA成员属性(数据和函数)的描述表,从字符串到结构的映射

struct AAPropDesc

{

const char *szName;

AAProp *pProp;

};

//AA所有成员的从字符串到结构的映射表

AAPropDesc m_Props[] =

{

{"f1", new AAField(&AA::f1)},

{"f2", new AAField(&AA::f2)},

{"func1", new AAFunc(&AA::fun1)}

};

//从字符串找到描述对象,也就是最为基本的“反射”功能

AAPropDesc *GetDescByName(const char *szName)

{

for (auto &prop : m_Props)

{

if (strcmp(prop.szName, szName) == 0)

{

return &prop;

}

}

return nullptr;

}

//注册个lua的__index方法,当lua需要查找一个结构当前不存在的filed时调用

int Index(lua_State *L)

{

//第一个参数为usrdata

AA *pObj = *(AA**)lua_touserdata(L, 1);

//第二个参数为key

const char * szKey = lua_tostring(L, 2);

printf("objptr %p, key %s\n", pObj, szKey);

AAPropDesc *pDesc = GetDescByName(szKey);

if (pDesc == nullptr)

{

printf("cannot find field %s\n", szKey);

return 0;

}

//转发给具体类型(数据或者函数)对象完成字段查找

return pDesc->pProp->GetField(pObj, L);

}

int NewIndex(lua_State *L)

{

return 0;

}

//注册给lua的全局变量,第一个参数为创建的类型名称

//这里没有实现从类名到类描述符的转换,假设只能创建AA类型对象

int CreateObject(lua_State *L)

{

//函数的第一个参数为字符串名称的类名称

const char * pClassName = lua_tostring(L, 1);

if (strcmp(pClassName, "AA"))

{

printf("invalid pClassName %s\n", pClassName);

return 0;

}

//创建C++中对象

AA *pObj = new AA(1111, 2222);

printf("create object %p\n", pObj);

//将对象地址保存到创建用户数据中

*((AA**)lua_newuserdata(L, sizeof(pObj))) = pObj;

//这里就是我们使用userdata的原因:就是为了使用该接口支持metatable的功能

//所以在创建对象之后,还是要为对象设置一个metatable结构

//由于lua中的metatable也是一个简单的table,所以这个地方首先直接创建一个普通的table

lua_createtable(L, 0, 0);

//创建metatable中的__index方法

lua_pushstring(L, "__index");

//创建该方法对应的C函数

lua_pushcfunction(L, Index);

//将新生成的metatable的__index设置为新创建的cfunction

lua_rawset(L, -3);

//将生成函数设置为新创建对象的metatable

lua_setmetatable(L, -2);

//告诉lua函数有一个返回值

return 1;

}

int main(int argc, char ** argv) {

lua_State *L = luaL_newstate();

luaL_openlibs(L);

lua_register(L, "CreateObject", CreateObject);

const char *luascript = "pobj = CreateObject(\"AA\");"

"print(pobj.f1, pobj.f2);"

"print(pobj.func1(100000, 1))"

;

// "luasquare = function(x) return x * x, x * 2; end";

if (luaL_loadstring(L, luascript) == LUA_OK) {

if (lua_pcall(L, 0, 0, 0) == LUA_OK) {

lua_pop(L, lua_gettop(L));

}

}

else

{

printf("load failed\n");

return -1;

}

lua_close(L);

return 0;

}

tsecer@harry :make -B

g++ -I /home/tsecer/src/ main.cpp -L /home/tsecer/src/ -llua -ldl -g

tsecer@harry :./a.out

create object 0x6544e0

objptr 0x6544e0, key f1

objptr 0x6544e0, key f2

1111.0 2222.0

objptr 0x6544e0, key func1

111102222

tsecer@harry :

四、__index/__newindex的直写机制

直观上看,当执行一次new(甚至一次查找类的__index)操作,那么按照hash表或者C++的map容器的范式,那么lua应该把这个值缓存起来,从而下次调用的时候就不用再调用__index/__newindex这种耗时的外部调用了。这样带来的问题就是当lua执行index/__index之后,后续的操作都无法反应到C++代码中,这明显不是我们希望的效果。

这里再次强调:lua的设计就是为了更好的和C++互相调用。这也意味着,你不是发现lua“恰好”有某个特性可以满足你的需求,而是lua添加的这个功能即使为了满足你的这个C++需求。

再看下lua中对于成员访问的方法。这里注意到的是,luaV_fastget/luaV_fastset接口,判断如果传入的对象不是一个lua内置的Table(lua唯一支持的复杂数据结构),直接转发(透传)到基于metatable的机制完成,并且两个函数中都没有所谓的“对结果进行缓存”的操作。也就是这个操作是一次性,通过lua直达到metatable的。对于我们自定义的metatable,则会调用到对应的__index/__newindex接口

/*

** copy of 'luaV_gettable', but protecting the call to potential

** metamethod (which can reallocate the stack)

*/

#define gettableProtected(L,t,k,v) { const TValue *slot; \

if (luaV_fastget(L,t,k,slot,luaH_get)) { setobj2s(L, v, slot); } \

else Protect(luaV_finishget(L,t,k,v,slot)); }

/* same for 'luaV_settable' */

#define settableProtected(L,t,k,v) { const TValue *slot; \

if (!luaV_fastset(L,t,k,slot,luaH_get,v)) \

Protect(luaV_finishset(L,t,k,v,slot)); }

/*

** fast track for 'gettable': if 't' is a table and 't[k]' is not nil,

** return 1 with 'slot' pointing to 't[k]' (final result). Otherwise,

** return 0 (meaning it will have to check metamethod) with 'slot'

** pointing to a nil 't[k]' (if 't' is a table) or NULL (otherwise).

** 'f' is the raw get function to use.

*/

#define luaV_fastget(L,t,k,slot,f) \

(!ttistable(t) \

? (slot = NULL, 0) /* not a table; 'slot' is NULL and result is 0 */ \

: (slot = f(hvalue(t), k), /* else, do raw access */ \

!ttisnil(slot))) /* result not nil? */

/*

** Fast track for set table. If 't' is a table and 't[k]' is not nil,

** call GC barrier, do a raw 't[k]=v', and return true; otherwise,

** return false with 'slot' equal to NULL (if 't' is not a table) or

** 'nil'. (This is needed by 'luaV_finishget'.) Note that, if the macro

** returns true, there is no need to 'invalidateTMcache', because the

** call is not creating a new entry.

*/

#define luaV_fastset(L,t,k,slot,f,v) \

(!ttistable(t) \

? (slot = NULL, 0) \

: (slot = f(hvalue(t), k), \

ttisnil(slot) ? 0 \

: (luaC_barrierback(L, hvalue(t), v), \

setobj2t(L, cast(TValue *,slot), v), \

1)))

下面是测试的例子,从中可以看到,每次get/set调用都是透传到了外部定义的函数实现

tsecer@harry :cat main.cpp

extern "C"

{

#include <lua.h>

#include <lualib.h>

#include <lauxlib.h>

}

#include <stdio.h>

#include <string.h> //strcmp

//测试类,尝试在lua中访问这个结构的各个成员

struct AA

{

AA(int x, int y):

f1(x), f2(y)

{}

int f1, f2;

int fun1(int x, int y)

{

return f1 * x + f2 * y;

}

};

//AA类一个属性的描述类,定义为基类,从而可以将变量和函数放在同一个数组中

struct AAProp

{

virtual int GetField(AA *pObj, lua_State *L)

{

return 0;

}

virtual int SetField(AA *pObj, lua_State *L)

{

return 0;

}

};

//AA类的一个数据成员(只支持int类型)

struct AAField: public AAProp

{

int AA::*pint;

AAField(decltype(pint) disp):

pint(disp)

{}

//成员变量的访问比较简单,直接返回变量内容即可

virtual int GetField(AA *pObj, lua_State *L) override

{

const char * szKey = lua_tostring(L, 2);

printf("get field key %s val %d\n", szKey, pObj->*pint);

lua_pushnumber(L, pObj->*pint);

return 1;

}

virtual int SetField(AA *pObj, lua_State *L) override

{

//第三个参数表示设置的值(第一个为table,第二个为key,第三个为val)

int val = lua_tointeger(L, 3);

const char * szKey = lua_tostring(L, 2);

printf("set field key %s value %d\n", szKey, val);

pObj->*pint = val;

return 0;

}

};

//AA类的一个函数成员(只支持 int(*)(int, int)类型)

using StructFunc = int (AA::*)(int, int);

struct AAFunc:public AAProp

{

AAFunc(StructFunc func):

m_func(func)

{}

StructFunc m_func;

//将成员函数转换为静态函数,从而可以和lua的cfunction绑定

static int luaGetField(lua_State *L)

{

//从upvalue中获得对象指针

AA *pObj = (AA*)lua_touserdata(L, lua_upvalueindex(1));

AAFunc * pSelf = (AAFunc*)lua_touserdata(L, lua_upvalueindex(2));

//函数的第一个和第二个参数表示调用参数

lua_pushinteger(L, (pObj->*pSelf->m_func)(lua_tointeger(L, 1), lua_tointeger(L, 2)));

return 1;

}

//完成lua中查找成员功能,成员函数的访问比成员变量访问更复杂一些,因为返回之后这个函数还要被再次调用

virtual int GetField(AA *pobj, lua_State *L) override

{

lua_pushlightuserdata(L, pobj);

lua_pushlightuserdata(L, this);

lua_pushcclosure(L, luaGetField, 2);

return 1;

}

};

//AA成员属性(数据和函数)的描述表,从字符串到结构的映射

struct AAPropDesc

{

const char *szName;

AAProp *pProp;

};

//AA所有成员的从字符串到结构的映射表

AAPropDesc m_Props[] =

{

{"f1", new AAField(&AA::f1)},

{"f2", new AAField(&AA::f2)},

{"func1", new AAFunc(&AA::fun1)}

};

//从字符串找到描述对象,也就是最为基本的“反射”功能

AAPropDesc *GetDescByName(const char *szName)

{

for (auto &prop : m_Props)

{

if (strcmp(prop.szName, szName) == 0)

{

return &prop;

}

}

return nullptr;

}

//注册个lua的__index方法,当lua需要查找一个结构当前不存在的filed时调用

int Index(lua_State *L)

{

//第一个参数为usrdata

AA *pObj = *(AA**)lua_touserdata(L, 1);

//第二个参数为key

const char * szKey = lua_tostring(L, 2);

printf("objptr %p, key %s\n", pObj, szKey);

AAPropDesc *pDesc = GetDescByName(szKey);

if (pDesc == nullptr)

{

printf("cannot find field %s\n", szKey);

return 0;

}

//转发给具体类型(数据或者函数)对象完成字段查找

return pDesc->pProp->GetField(pObj, L);

}

int NewIndex(lua_State *L)

{

//第一个参数为usrdata

AA *pObj = *(AA**)lua_touserdata(L, 1);

//第二个参数为key

const char * szKey = lua_tostring(L, 2);

printf("objptr %p, key %s\n", pObj, szKey);

AAPropDesc *pDesc = GetDescByName(szKey);

if (pDesc == nullptr)

{

printf("cannot find field %s\n", szKey);

return 0;

}

//转发给具体类型(数据或者函数)对象完成字段查找

return pDesc->pProp->SetField(pObj, L);

return 0;

}

//注册给lua的全局变量,第一个参数为创建的类型名称

//这里没有实现从类名到类描述符的转换,假设只能创建AA类型对象

int CreateObject(lua_State *L)

{

//函数的第一个参数为字符串名称的类名称

const char * pClassName = lua_tostring(L, 1);

if (strcmp(pClassName, "AA"))

{

printf("invalid pClassName %s\n", pClassName);

return 0;

}

//创建C++中对象

AA *pObj = new AA(1111, 2222);

printf("create object %p\n", pObj);

//将对象地址保存到创建用户数据中

*((AA**)lua_newuserdata(L, sizeof(pObj))) = pObj;

//这里就是我们使用userdata的原因:就是为了使用该接口支持metatable的功能

//所以在创建对象之后,还是要为对象设置一个metatable结构

//由于lua中的metatable也是一个简单的table,所以这个地方首先直接创建一个普通的table

lua_createtable(L, 0, 0);

//创建metatable中的__index方法

lua_pushstring(L, "__index");

//创建该方法对应的C函数

lua_pushcfunction(L, Index);

//将新生成的metatable的__index设置为新创建的cfunction

lua_rawset(L, -3);

//设置New字段

lua_pushstring(L, "__newindex");

lua_pushcfunction(L, NewIndex);

lua_rawset(L, -3);

//将生成函数设置为新创建对象的metatable

lua_setmetatable(L, -2);

//告诉lua函数有一个返回值

return 1;

}

int main(int argc, char ** argv) {

lua_State *L = luaL_newstate();

luaL_openlibs(L);

lua_register(L, "CreateObject", CreateObject);

const char *luascript = "pobj = CreateObject(\"AA\");"

"print(pobj.f1, pobj.f2);"

"print(pobj.func1(100000, 1));"

"pobj.f1 = 10;"

"print(pobj.f1);"

"pobj.f1 = 20;"

;

// "luasquare = function(x) return x * x, x * 2; end";

if (luaL_loadstring(L, luascript) == LUA_OK) {

if (lua_pcall(L, 0, 0, 0) == LUA_OK) {

lua_pop(L, lua_gettop(L));

}

}

else

{

printf("load failed\n");

return -1;

}

lua_close(L);

return 0;

}

tsecer@harry :make -B

g++ -I /home/tsecer/lua-5.3.4/src/ main.cpp -L /home/tsecer/lua-5.3.4/src/ -llua -ldl -g

tsecer@harry :./a.out

create object 0x659db0

objptr 0x659db0, key f1

get field key f1 val 1111

objptr 0x659db0, key f2

get field key f2 val 2222

1111.0 2222.0

objptr 0x659db0, key func1

111102222

objptr 0x659db0, key f1

set field key f1 value 10

objptr 0x659db0, key f1

get field key f1 val 10

10.0

objptr 0x659db0, key f1

set field key f1 value 20

tsecer@harry :