注:原文也在公司内部论坛上发了
概述
尽管将C++对象绑定到Lua已经有
tolua++(Cocos2d-x 3.0用的就是这个)、
LuaBridge(我们游戏client对这个库进行了改进)和
luabind等各种库能够直接使用了(lua-users.org上有对各种语言绑定到lua库的
汇总),但弄清楚C++对象绑定到Lua的常见方法,不但有助于更深的了解Lua的机制,还能够方便改动第三方库以满足实际项目需求。
本文通过分析第三方库Lunar(我们游戏服务端用的是Luna,Lunar是Luna添加版。但仍然足够简洁)的实现,来理解C++对象绑定到Lua的通常方法。Lunar的測试代码放在我的github上。
Lunar实现的功能以及原理
Lunar实现很简洁,同一时候实现了C++绑定到Lua主要功能。使用Lunar至少能够做到下面几点:
1、在脚本中,能够使用注冊过的类创建C++对象,此类C++对象,由Lua控制何时释放。
2、在C++创建的对象,能够压入栈中,供脚本使用这个对象,而且提供一个可选參数。来决定这个对象是由C++控制释放,还是Lua控制释放。
3、脚本中的C++类对象。能够调用注冊过的类成员函数。
4、在C++中,能够获取在脚本中创建的对象。而且在C++中能够调用这个对象的成员函数。
5、能够在脚本中定义对象的成员函数,而且能在C++中调用这些用脚本实现的成员函数。
在脚本中创建C++对象,实质返回给脚本是一个userdata类型的值ud。ud中保存一个指针,该指针指向所创建的C++对象。这时候Lua控制对象何时释放,即在Lua在回收userdata时,同一时候利用userdata的元表__gc字段来回收动态分配的对象, 这时候就不须要C++释放内存了。注意这里返回给脚本不能是lightuserdata。由于lightuserdata实质上仅仅是一个指针。不受垃圾回收收集器的管理,而且它也没有元表。
在脚本中创建的对象是由Lua来维护对象的生命周期。
在Lunar中还能够使用Lunar<T>::push(lua_State *L, T *obj, bool gc=false)向栈中压入对象供脚本使用,当中第三个參数能够决定创建的对象是由C++控制释放。还是Lua控制释放。 其原理是在把要传递给Lua的对象obj压入栈时,它首先利用对象地址在lookup表中查找(lookup是一个mode为"v"的weak table。保存全部在脚本中使用的对象。该表的key是对象地址。value是对象相应的userdata),若不在lookup中。则会创建一个新的userdata。并把它保存在lookup中,若第三个參数为false,即由C++控制对象释放。还会把上面的userdata保存在一个nottrash表中,nottrash是一个mode为"k"的weak
table,保存全部不会随userdata回收其对应对象也释放的userdata,该表key为userdata。value为true。
这样处理后,在Lua回收userdata时,首先检測userdata是否在nottrash中。若不在。则删除userdata所指向对象,否则须要C++自己释放所创建的对象。
在调用Lunar<T>::Register(lua_State *L)向脚本注冊类时,会创建两个表,一个是methods表。该表的key为函数名,value为函数地址(函数能够是C++中定义类的方法,也能够在Lua中定义函数),在Lua中调用的对象方法。都保存在该表中;还有一个是metatable表,做为userdata的元表,该metatable表保存了上面的lookup表和nottrash表。而且设置了metatable.__metatable = methods,这样在脚本中就隐藏了metatable表,也就是说在Lua中,调用getmetatable(userdata)得到的是methods表,而不是metatable表,在脚本中,给对象加入成员方法也是会保存在methods表中。
Lunar源代码分析
以下逐行分析了Luanr的实现,在附件中是Lunar的測试代码。例如以下:
006
|
#define
LUNAR_DECLARE_METHOD(Class, Name) {#Name, &Class::Name}
|
008
|
template < typename T> class Lunar
{
|
012
|
typedef int (T::*mfp)(lua_State
*L);
|
014
|
//向Lua中注冊的函数名字以及相应的函数地址
|
015
|
typedef struct { const char *name;
mfp mfunc; } RegType;
|
018
|
static void Register(lua_State
*L) {
|
019
|
//创建method
table,该table key为函数名,
|
020
|
//value为函数地址(函数能够是C++中定义类的方法,也能够在Lua中定义函数)
|
021
|
//在Lua中,以table的key为函数名。调用对应的方法。
|
023
|
int methods
= lua_gettop(L);
|
026
|
luaL_newmetatable(L,
T::className);
|
027
|
int metatable
= lua_gettop(L);
|
029
|
//把method
table注冊到全局表中,这样在Lua中能够直接使用该table。
|
030
|
//这样能够在这个table中添加Lua实现的函数
|
031
|
lua_pushvalue(L,
methods);
|
032
|
set(L,
LUA_GLOBALSINDEX, T::className);
|
034
|
//隐藏userdata的实质的元表,也就是说在Lua中
|
035
|
//调用getmetatable(userdata)得到的是methods
table。而不是metatable table
|
036
|
lua_pushvalue(L,
methods);
|
037
|
set(L,
metatable, "__metatable" );
|
039
|
//设置metatable
table的元方法
|
040
|
lua_pushvalue(L,
methods);
|
041
|
set(L,
metatable, "__index" );
|
043
|
lua_pushcfunction(L,
tostring_T);
|
044
|
set(L,
metatable, "__tostring" );
|
046
|
//设置__gc元方法。这样方便在Lua回收userdata时,
|
047
|
//能够做一些其它操作。比方释放其对应的对象
|
048
|
lua_pushcfunction(L,
gc_T);
|
049
|
set(L,
metatable, "__gc" );
|
051
|
lua_newtable(L); //创建methods的元表mt
|
052
|
lua_pushcfunction(L,
new_T);
|
053
|
lua_pushvalue(L,
-1); //
把new_T再次压入栈顶
|
054
|
set(L,
methods, "new" ); //
把new_T函数增加到methods中。这样脚本可通过调用T:new()来创建C++对象
|
055
|
set(L,
-3, "__call" ); //
mt.__call = mt,这样脚本能够通过调用T()来创建C++对象
|
056
|
lua_setmetatable(L,
methods); //设置methods的元表为mt
|
058
|
//把类T中的方法保存到method
table中,供Lua脚本使用
|
059
|
for (RegType
*l = T::methods; l->name; l++) {
|
060
|
lua_pushstring(L,
l->name);
|
061
|
lua_pushlightuserdata(L,
( void *)l); //以注冊函数在数组的位置作为cclosure的upvalue
|
062
|
lua_pushcclosure(L,
thunk, 1); //在Lua调用的类方法,调用的都是c
closure thunk,thunk通过upvalue获取实质调用的函数地址
|
063
|
lua_settable(L,
methods);
|
066
|
lua_pop(L,
2); //弹出methods和metatable
table。保证Register调用后。栈的空间大小不变
|
069
|
//调用保存在method
table中的函数
|
070
|
//在调用call之前。须要向栈中压入userdata和參数。
|
071
|
//并把最后的调用结果压入栈中,參数method传入要调用的函数名
|
072
|
static int call(lua_State
*L, const char *method,
|
073
|
int nargs=0, int nresults=LUA_MULTRET, int errfunc=0)
|
075
|
int base
= lua_gettop(L) - nargs; //获取userdata在栈中的索引
|
076
|
if (!luaL_checkudata(L,
base, T::className)) {
|
077
|
//假设用错误的类型调用对应的方法。则从栈中弹出userdata和參数
|
079
|
lua_settop(L,
base-1);
|
080
|
lua_pushfstring(L, "not
a valid %s userdata" ,
T::className);
|
084
|
lua_pushstring(L,
method); //压入方法名,通过该名字在userdata
method table中获取实质要调用的c closure
|
086
|
//获取相应的函数地址。其流程是从userdata的元表metatable查找,
|
087
|
//而metatable.__index=methods,在methods中通过方法名,获取对应的方法
|
088
|
lua_gettable(L,
base);
|
089
|
if (lua_isnil(L,
-1)) { //若不存在对应的方法
|
090
|
lua_settop(L,
base-1);
|
091
|
lua_pushfstring(L, "%s
missing method '%s'" ,
T::className, method);
|
094
|
lua_insert(L,
base); //
把方法移到userdata和args以下
|
096
|
int status
= lua_pcall(L, 1+nargs, nresults, errfunc); //
调用方法
|
098
|
const char *msg
= lua_tostring(L, -1);
|
099
|
if (msg
== NULL) msg = "(error
with no message)" ;
|
100
|
lua_pushfstring(L, "%s:%s
status = %d\n%s" ,
|
101
|
T::className,
method, status, msg);
|
102
|
lua_remove(L,
base); //
remove old message
|
105
|
return lua_gettop(L)
- base + 1; //
调用的方法。返回值的个数
|
108
|
//向栈中压入userdata,该userdata包括一个指针,该指针指向一个类型为T的对象
|
109
|
//參数obj为指向对象的指针,參数gc默觉得false,即Lua在回收userdata时,不会主动是释放obj相应的对象,此时应用程序负责相应对象释放
|
110
|
//若为true,则Lua在回收userdata时。会释放对应的对象
|
111
|
static int push(lua_State
*L, T *obj, bool gc= false )
{
|
112
|
if (!obj)
{ lua_pushnil(L); return 0;
}
|
113
|
luaL_getmetatable(L,
T::className); //在注冊表中获取类名的相应的table
mt,以作为以下userdata的元表
|
114
|
if (lua_isnil(L,
-1)) luaL_error(L, "%s
missing metatable" ,
T::className);
|
115
|
int mt
= lua_gettop(L);
|
117
|
//设置mt["userdata"]
= lookup。并向栈顶压入lookup,lookup是一个mode为"v"的weak table,保存全部类对象相应的userdata
|
118
|
//key是对象地址,value是userdata
|
119
|
subtable(L,
mt, "userdata" , "v" );
|
121
|
static_cast <userdataType*>(pushuserdata(L,
obj, sizeof (userdataType))); //向栈顶压入一个userdata
|
123
|
ud->pT
= obj; //把对象的地址obj保存到userdata中
|
124
|
lua_pushvalue(L,
mt); //压入注冊表中类名相应的table
mt
|
125
|
lua_setmetatable(L,
-2); //设置userdata的元表
|
127
|
//gc为false。Lua在回收userdata时,不会主动是释放obj相应的对象,此时应用程序负责相应对象释放
|
128
|
lua_checkstack(L,
3);
|
130
|
//mt["do
not trash"] = nottrash。nottrash是一个mode为"k"的weak table,保存全部不会随userdata回收其对应对象也释放的userdata
|
131
|
//key是userdata,value是true。向栈顶压入nottrash
|
132
|
subtable(L,
mt, "do
not trash" , "k" );
|
133
|
lua_pushvalue(L,
-2); //再次压入userdata
|
134
|
lua_pushboolean(L,
1);
|
135
|
lua_settable(L,
-3); //nottrash[userdata]
= true
|
136
|
lua_pop(L,
1); //把nottrash从栈中弹出
|
139
|
lua_replace(L,
mt); //把索引mt出元表值替换为userdata
|
140
|
lua_settop(L,
mt); //设置栈的大小。即通过调用push()调用,栈顶元素为userdata,该userdata包括指向对象的指针
|
141
|
return mt; //返回userdata在栈中的索引
|
144
|
//检測索引narg处的值是否为对应的userdata,若是则返回一个指针。该指针指向类型T的对象
|
145
|
static T
*check(lua_State *L, int narg)
{
|
147
|
static_cast <userdataType*>(luaL_checkudata(L,
narg, T::className));
|
149
|
luaL_typerror(L,
narg, T::className);
|
157
|
typedef struct {
T *pT; } userdataType;
|
161
|
//Lua中调用类的成员函数,都是通过调用该函数,然后使用userdataType的upvalue来调用实质的成员函数
|
162
|
static int thunk(lua_State
*L) {
|
164
|
T
*obj = check(L, 1); //检測是否是对应的userdata,若是,返回指向T对象的指针
|
165
|
lua_remove(L,
1); //从栈中删除userdata。以便成员函数的參数的索引从1開始
|
167
|
RegType
*l = static_cast <RegType*>(lua_touserdata(L,
lua_upvalueindex(1)));
|
168
|
return (obj->*(l->mfunc))(L); //调用实质的成员函数
|
171
|
//创建一个新的对象T。在脚本中调用T()或T:new(),实质调用的都是该函数
|
172
|
//调用后,栈顶元素为userdata,该userdata包括指向对象的指针
|
173
|
static int new_T(lua_State
*L) {
|
174
|
lua_remove(L,
1); //
要求在脚本中使用T:new(),而不能是T.new()
|
175
|
T
*obj = new T(L); //
调用类T的构造函数
|
176
|
push(L,
obj, true ); //
传入true,表明Lua回收userdata时,对应的对象也会删除
|
180
|
//Lua在回收userdata时,对应的也会调用该函数
|
181
|
//依据userdata是否保存在nottrash(即mt["do
not trash"],mt为注冊表中T:classname相应的table)中来决定
|
182
|
//是否释放对应的对象,若在,则不释放对应的对象,须要应用程序自己删除,否则删除对应的对象
|
183
|
static int gc_T(lua_State
*L) {
|
184
|
if (luaL_getmetafield(L,
1, "do
not trash" ))
{
|
185
|
lua_pushvalue(L,
1); //再次压入userdata
|
186
|
lua_gettable(L,
-2); //向栈中压入nottrash[userdata]
|
187
|
if (!lua_isnil(L,
-1)) return 0; //在nottrash中,不删除对应的对象
|
189
|
userdataType
*ud = static_cast <userdataType*>(lua_touserdata(L,
1));
|
191
|
if (obj) delete obj; //删除对应的对象
|
195
|
//在Lua中调用tostring(object)时,会调用该函数
|
196
|
static int tostring_T
(lua_State *L) {
|
198
|
userdataType
*ud = static_cast <userdataType*>(lua_touserdata(L,
1));
|
200
|
sprintf (buff, "%p" ,
( void *)obj);
|
201
|
lua_pushfstring(L, "%s
(%s)" ,
T::className, buff);
|
206
|
//设置t[key]=value,t是索引为table_index相应的值,value为栈顶元素
|
207
|
static void set(lua_State
*L, int table_index, const char *key)
{
|
208
|
lua_pushstring(L,
key);
|
209
|
lua_insert(L,
-2); //交换key和value
|
210
|
lua_settable(L,
table_index);
|
213
|
//在栈顶压入一个模式为mode的weak
table
|
214
|
static void weaktable(lua_State
*L, const char *mode)
{
|
216
|
lua_pushvalue(L,
-1);
|
217
|
lua_setmetatable(L,
-2); //创建的weak
table以自身作为元表
|
218
|
lua_pushliteral(L, "__mode" );
|
219
|
lua_pushstring(L,
mode);
|
220
|
lua_settable(L,
-3); //
metatable.__mode = mode
|
223
|
//该函数向栈中压入值t[name]。t是给定索引的tindex的值,
|
224
|
//若原来t[name]值不存在,则创建一个模式为mode的weak
table wt,而且赋值t[name] = wt
|
225
|
//最后栈顶中压入这个weak
table
|
226
|
var allowComments=true,cb_blogId=348828,cb_entryId=7077574,cb_blogApp=currentBlogApp,cb_blogUserGuid='4ed8aa7c-ad21-e711-9fc1-ac853d9f53cc',cb_entryCreatedDate='2017/6/25 19:07:00';loadViewCount(cb_entryId);var cb_postType=1;var isMarkdown=false;
var commentManager = new blogCommentManager();commentManager.renderComments(0);
var googletag = googletag || {};
googletag.cmd = googletag.cmd || [];
googletag.cmd.push(function() {
googletag.defineSlot('/1090369/C1', [300, 250], 'div-gpt-ad-1546353474406-0').addService(googletag.pubads());
googletag.defineSlot('/1090369/C2', [468, 60], 'div-gpt-ad-1539008685004-0').addService(googletag.pubads());
googletag.pubads().enableSingleRequest();
googletag.enableServices();
});
if(enablePostBottom()) {
codeHighlight();
fixPostBody();
setTimeout(function () { incrementViewCount(cb_entryId); }, 50);
deliverT2();
deliverC1();
deliverC2();
loadNewsAndKb();
loadBlogSignature();
LoadPostInfoBlock(cb_blogId, cb_entryId, cb_blogApp, cb_blogUserGuid);
GetPrevNextPost(cb_entryId, cb_blogId, cb_entryCreatedDate, cb_postType);
loadOptUnderPost();
GetHistoryToday(cb_blogId, cb_blogApp, cb_entryCreatedDate);
}
|