浅析C++绑定到Lua的方法
来源:互联网 发布:天分玻璃优化软件下载 编辑:程序博客网 时间:2024/06/16 16:15
注:原文也在公司内部论坛上发了
概述
虽然将C++对象绑定到Lua已经有tolua++(Cocos2d-x 3.0用的就是这个)、LuaBridge(我们游戏客户端对这个库进行了改进)和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的测试代码,如下:
001
extern
"C"
{
002
#include "lua.h"
003
#include "lauxlib.h"
004
}
005
006
#define LUNAR_DECLARE_METHOD(Class, Name) {#Name, &Class::Name}
007
008
template
<
typename
T>
class
Lunar {
009
public
:
010
011
//C++可以向Lua注册的函数类型
012
typedef
int
(T::*mfp)(lua_State *L);
013
014
//向Lua中注册的函数名字以及对应的函数地址
015
typedef
struct
{
const
char
*name; mfp mfunc; } RegType;
016
017
//用来注册C++定义的类供Lua使用
018
static
void
Register(lua_State *L) {
019
//创建method table,该table key为函数名,
020
//value为函数地址(函数可以是C++中定义类的方法,也可以在Lua中定义函数)
021
//在Lua中,以table的key为函数名,调用相应的方法。
022
lua_newtable(L);
023
int
methods = lua_gettop(L);
024
025
//创建userdata的元表
026
luaL_newmetatable(L, T::className);
027
int
metatable = lua_gettop(L);
028
029
//把method table注册到全局表中,这样在Lua中可以直接使用该table,
030
//这样可以在这个table中增加Lua实现的函数
031
lua_pushvalue(L, methods);
032
set(L, LUA_GLOBALSINDEX, T::className);
033
034
//隐藏userdata的实质的元表,也就是说在Lua中
035
//调用getmetatable(userdata)得到的是methods table,而不是metatable table
036
lua_pushvalue(L, methods);
037
set(L, metatable,
"__metatable"
);
038
039
//设置metatable table的元方法
040
lua_pushvalue(L, methods);
041
set(L, metatable,
"__index"
);
042
043
lua_pushcfunction(L, tostring_T);
044
set(L, metatable,
"__tostring"
);
045
046
//设置__gc元方法,这样方便在Lua回收userdata时,
047
//可以做一些其他操作,比如释放其相应的对象
048
lua_pushcfunction(L, gc_T);
049
set(L, metatable,
"__gc"
);
050
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
057
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);
064
}
065
066
lua_pop(L, 2);
//弹出methods和metatable table,保证Register调用后,栈的空间大小不变
067
}
068
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)
074
{
075
int
base = lua_gettop(L) - nargs;
//获取userdata在栈中的索引
076
if
(!luaL_checkudata(L, base, T::className)) {
077
//如果用错误的类型调用相应的方法,则从栈中弹出userdata和参数
078
//并且压入相应的错误信息
079
lua_settop(L, base-1);
080
lua_pushfstring(L,
"not a valid %s userdata"
, T::className);
081
return
-1;
082
}
083
084
lua_pushstring(L, method);
//压入方法名,通过该名字在userdata method table中获取实质要调用的c closure
085
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);
092
return
-1;
093
}
094
lua_insert(L, base);
// 把方法移到userdata和args下面
095
096
int
status = lua_pcall(L, 1+nargs, nresults, errfunc);
// 调用方法
097
if
(status) {
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
103
return
-1;
104
}
105
return
lua_gettop(L) - base + 1;
// 调用的方法,返回值的个数
106
}
107
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);
116
117
//设置mt["userdata"] = lookup,并向栈顶压入lookup,lookup是一个mode为"v"的weak table,保存所有类对象对应的userdata
118
//key是对象地址,value是userdata
119
subtable(L, mt,
"userdata"
,
"v"
);
120
userdataType *ud =
121
static_cast
<userdataType*>(pushuserdata(L, obj,
sizeof
(userdataType)));
//向栈顶压入一个userdata
122
if
(ud) {
123
ud->pT = obj;
//把对象的地址obj保存到userdata中
124
lua_pushvalue(L, mt);
//压入注册表中类名对应的table mt
125
lua_setmetatable(L, -2);
//设置userdata的元表
126
if
(gc ==
false
) {
127
//gc为false,Lua在回收userdata时,不会主动是释放obj对应的对象,此时应用程序负责相应对象释放
128
lua_checkstack(L, 3);
129
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从栈中弹出
137
}
138
}
139
lua_replace(L, mt);
//把索引mt出元表值替换为userdata
140
lua_settop(L, mt);
//设置栈的大小,即通过调用push()调用,栈顶元素为userdata,该userdata包含指向对象的指针
141
return
mt;
//返回userdata在栈中的索引
142
}
143
144
//检测索引narg处的值是否为相应的userdata,若是则返回一个指针,该指针指向类型T的对象
145
static
T *check(lua_State *L,
int
narg) {
146
userdataType *ud =
147
static_cast
<userdataType*>(luaL_checkudata(L, narg, T::className));
148
if
(!ud) {
149
luaL_typerror(L, narg, T::className);
150
return
NULL;
151
}
152
return
ud->pT;
153
}
154
155
private
:
156
157
typedef
struct
{ T *pT; } userdataType;
158
159
Lunar();
//隐藏默认的构造函数
160
161
//Lua中调用类的成员函数,都是通过调用该函数,然后使用userdataType的upvalue来调用实质的成员函数
162
static
int
thunk(lua_State *L) {
163
//此时栈中元素是userdata和参数
164
T *obj = check(L, 1);
//检测是否是相应的userdata,若是,返回指向T对象的指针
165
lua_remove(L, 1);
//从栈中删除userdata,以便成员函数的参数的索引从1开始
166
//利用upvalue获取相应的成员函数
167
RegType *l =
static_cast
<RegType*>(lua_touserdata(L, lua_upvalueindex(1)));
168
return
(obj->*(l->mfunc))(L);
//调用实质的成员函数
169
}
170
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时,相应的对象也会删除
177
return
1;
178
}
179
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中,不删除相应的对象
188
}
189
userdataType *ud =
static_cast
<userdataType*>(lua_touserdata(L, 1));
190
T *obj = ud->pT;
191
if
(obj)
delete
obj;
//删除相应的对象
192
return
0;
193
}
194
195
//在Lua中调用tostring(object)时,会调用该函数
196
static
int
tostring_T (lua_State *L) {
197
char
buff[32];
198
userdataType *ud =
static_cast
<userdataType*>(lua_touserdata(L, 1));
199
T *obj = ud->pT;
200
sprintf
(buff,
"%p"
, (
void
*)obj);
201
lua_pushfstring(L,
"%s (%s)"
, T::className, buff);
202
203
return
1;
204
}
205
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);
211
}
212
213
//在栈顶压入一个模式为mode的weak table
214
static
void
weaktable(lua_State *L,
const
char
*mode) {
215
lua_newtable(L);
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
221
}
222
223
//该函数向栈中压入值t[name],t是给定索引的tindex的值,
224
//若原来t[name]值不存在,则创建一个模式为mode的weak table wt,并且赋值t[name] = wt
225
//最后栈顶中压入这个weak table
226
static
void
subtable(lua_State *L,
int
tindex,
const
char
*name,
const
char
*mode) {
227
lua_pushstring(L, name);
228
lua_gettable(L, tindex);
229
if
(lua_isnil(L, -1)) {
230
lua_pop(L, 1);
//弹出nil
231
lua_checkstack(L, 3);
//检测栈的空间,是否足够
232
weaktable(L, mode);
//栈顶压入一个指定模式的weak table
233
lua_pushstring(L, name);
234
lua_pushvalue(L, -2);
//再次压入创建的weak table
235
lua_settable(L, tindex);
//t[name] = wt
236
}
237
}
238
239
//向栈顶压入lookup[key],lookup是一个weak table,保存所有类对象对应的userdata
240
//key是对象地址,value是userdata,若不存在则创建一个userdata,并赋值lookup[key] = userdata
241
//这样使得在脚本中引用过的对象,就不会创建新的userdata了
242
static
void
*pushuserdata(lua_State *L,
void
*key,
size_t
sz) {
243
void
*ud = 0;
244
lua_pushlightuserdata(L, key);
//创建一个light userdata
245
lua_gettable(L, -2);
// 查找lookup[key]是否存在
246
if
(lua_isnil(L, -1)) {
247
lua_pop(L, 1);
//弹出nil
248
lua_checkstack(L, 3);
//检测栈的空间
249
ud = lua_newuserdata(L, sz);
//创建一个userdata
250
lua_pushlightuserdata(L, key);
251
lua_pushvalue(L, -2);
//再次压入userdata
252
lua_settable(L, -4);
//lookup[key] = userdata
253
}
254
return
ud;
255
}
256
};
总结
根据实际项目需要,可能还需要其他功能,比如在脚本中获取和设置对象的成员变量、处理类的继承等需求。只要理解C++对象绑定到Lua方法,就可以方便按需要扩展,或者快速修改第三方库以满足需求。
参考资料
http://lua-users.org/wiki/CppBindingWithLunar
http://lua-users.org/wiki/LunaFive
http://lua-users.org/wiki/BindingCodeToLua
https://github.com/vinniefalco/LuaBridge
http://www.codenix.com/~tolua/
1 0
- 浅析C++绑定到Lua的方法
- 备忘[cocos lua 绑定c方法] 3.10
- cocos2dx 关于lua 绑定的环境配置官方文档翻译与 将自定义c++方法绑定到lua的的方法
- C++绑定到Lua
- 绑定HGE到Lua中
- C++对象绑定到Lua
- 绑定自定义类到lua
- C++对象绑定到Lua
- cocos2dx3.0 自动绑定自定义的C++到lua
- cocos2dx的lua绑定
- cocos2dx的lua绑定
- cocos2dx的lua绑定
- cocos2dx的lua绑定
- 一种Lua到C的封装
- lua源码到c的简单转换
- 绑定Enum到DropDownList控件的方法
- 绑定Enum到DropDownList控件的方法
- WPF 绑定StaticResource到控件的方法
- 堆排序
- 【noip模拟赛】数字对
- 运用定义函数来输出 ABCD递增型图像
- ExecutorService生命周期
- AN GridView 横向分页,适合TV等有按键需求的android系统
- 浅析C++绑定到Lua的方法
- Active Contour Models 主动轮廓模型
- Myeclipse+maven的简单使用说明
- js字符串格式化时间
- Linux进程间的通信(二)
- Java 多线程
- java回调
- [Erlang危机](1.2)第三方Erlang代码库
- daychooser