浅析C++与Lua数据交互层
背景
C++作为服务器端开发的主流语言,相比其他语言有明显的性能优势,但是它的技术门槛比较高,不支持热更新,无法进行迅速的需求迭代和线上问题的响应。另外,C++内存管理的特点,技术人员对指针的使用不当,可能会造成宕机。因此,现在越来越多的技术团队会选择在C++中嵌入脚本语言,C++提供底层支持,脚本语言进行业务逻辑的开发。这样的好处,一方面将开发人员进行划分,C++代码的维护只由核心人员进行。业务层的脚本代码开发,相对比较简单,可以交由经验比较浅的新人,甚至外包给其他团队;另外一方面,脚本语言的灵活性,可热更新,又弥补了C++的不足。
Lua作为一门非常轻量的脚本语言,被大量的游戏团队使用。我们就来说一说,C++是如何与Lua进行数据交互的。
已有的类似系统有LuaBinder、LuaTinker、CppToLua等。
基本原理
Lua提供了虚拟栈的概念,C/C++可以通过push/pop从栈上逐一存取基本的数据类型,也可以通过pushcfunction/pushcclosure,将函数注册给Lua,供其调用。但对于C++来说,数据结构比较复杂,有类和对象的概念,类有成员变量和成员函数,类之间又有继承的关系。Lua的基本接口并无法支撑这么复杂的数据交互。我们要实现的数据交互层,就是为C++提供一套注册接口,方面将C++中的类、对象类型的变量、类的成员变量和成员函数注册到Lua中去,让Lua像访问原生数据一样访问它们。
如下,在C++中定义了如下类A,并利用数据交互层提供的接口,将A类的成员变量和成员函数注册给Lua。然后,将A类型的对象ca注册到Lua的全局变量中,为了进行区分,命名为la。在Lua代码里,就可以通过la.x访问对象ca.x的变量,通过la:funcA(p1,p2,…)调用ca.funcA的函数了。
Class A
{
public:
A();
~A();
<span class="n">TypeX</span> <span class="n">x</span><span class="p">;</span>
<span class="n">TypeY</span> <span class="n">funcA</span><span class="p">(</span><span class="n">Param1</span> <span class="n">p1</span><span class="p">,</span> <span class="n">Param2</span> <span class="n">p2</span><span class="p">,</span> <span class="p">...);</span>
};
…
registerClass<A>(L, "A"); // L 为lua_State*类型
registerClassFunction<A>(L, "funcA", A::funcA);
registerClassMemberVariable<A>(L, "x", &A::x);
A ca;
registerVariable(L, "la", &ca);
这背后的发生了什么?先来看一下regiserClass
的实现,在Lua的全局环境中注册了名为name的表,并定义几个元方法,我们称这种表为类表。
template<typename T>
registerClass(lua_State* L, const char* name)
{
ClassName<T>::name(name); //注册类名
lua_newtable(L);
<span class="n">lua_pushstring</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="s">"__name"</span><span class="p">);</span>
<span class="n">lua_pushstring</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="n">name</span><span class="p">);</span>
<span class="n">lua_rawset</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="o">-</span><span class="mi">3</span><span class="p">);</span>
<span class="n">lua_pushstring</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="s">"__index"</span><span class="p">);</span>
<span class="n">lua_pushcclosure</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="n">meta_get</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="n">lua_rawset</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="o">-</span><span class="mi">3</span><span class="p">);</span>
<span class="n">lua_pushstring</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="s">"__newindex"</span><span class="p">);</span>
<span class="n">lua_pushcclosure</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="n">meta_set</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="n">lua_rawset</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="o">-</span><span class="mi">3</span><span class="p">);</span>
<span class="n">lua_pushstring</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="s">"__gc"</span><span class="p">);</span>
<span class="n">lua_pushcclosure</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="n">destroyer</span><span class="o"><</span><span class="n">T</span><span class="o">></span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="n">lua_rawset</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="o">-</span><span class="mi">3</span><span class="p">);</span>
<span class="n">lua_setglobal</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="n">name</span><span class="p">);</span>
}
当访问la.x时,会把la当作table,寻找table中key为”x”的值。la.x当作左值时会触发 __newindex
元方法,当作右值时会触发 __index
元方法。当la中没有找到key为”x”的值,就会去la的元表中进行查找。因此,需要将la的元表设置为A的类表。
除了上面通过注册全局变量,Lua层还有另外两个途径访问C++类的对象:通过C++类的构造函数直接构造对象、通过其他对象的成员变量或者成员函数的返回值。三种途径的关键一步,都是将userdata的元表关联到注册给Lua的类表中。
一层数据封装
Lua中的基本数据类型有number、string、table、function、userdata、thread。la本质上就是userdata,指向内存数据的一个指针,简单理解,可以认为是对象实例ca的地址,实际上,进行了一层封装。分别定义了ValueToUser
struct UserBase
{
UserBase(void* ptr): m_p(ptr) {}
virtual ~UserBase();
void* m_p;
};
template<typename T>
struct ValueToUser: UserBase
{
template<typename … Args>
ValueToUser(Args… args) : UserBase(new T(std::forward<Args>(args)…)) {}
<span class="o">~</span><span class="n">ValueToUser</span><span class="p">()</span> <span class="p">{</span> <span class="k">delete</span> <span class="p">((</span><span class="n">T</span><span class="o">*</span><span class="p">)</span><span class="n">m_p</span><span class="p">);}</span>
};
template<typename T>
struct PtrToUser: UserBase
{
PtrToUser(T t): UserBase((void)t) {}
};
template<typename T>
struct RefToUser: UserBase
{
RefToUser(T& t): UserBase(&t) {}
};
注意到registerClass中将destroyer
template<typame T>
int destroyer(lua_State* L)
{
((UserBase*)lua_touserdata(L, -1))->~UserBase();
return 0; //告诉Lua该函数没有返回值
}
两个抽象操作
定义两个操作:pushToLuaStack
pushToLuaStack<T>
|
| EnumToLua<T> ---> 将number类型压入栈
| ObjectToLua<T> ---> | ValueToLua<T> ---> 将ValueToUser<T>类型入栈
| PtrToLua<T> ---> 将PtrToUser<T>类型入栈
| RefToLua<T> ---> 将RefToUser<T>类型入栈
& 将T的类表作为刚压入栈的userdata的元表
getFromLuaStack<T>
|
| LuaToEnum<T> ---> 从栈上取出number类型变量
| LuaToObject<T> ----> & 1. user = LuaUserDataToType<UserBase*>::convert()
& 2. VoidToType<T>(user->m_p)
|
| T如果是指针类型,VoidToPtr
| T如果是引用类型,VoidToRef
| T如果是值类型, VoidToValue
注册成员变量
registerClassMemberVariable<A>(L, "x", &A::x)
在类表中注册了key为”x”的键值对,值是一个void* 类型的userdata,实际是一个VariableBase类型的指针,利用运行时多态和模板,是可以达到反射的目的的。把任意类的任意成员变量,都抽象成VariableBase类,它只有两个虚函数接口,get用来把成员变量的值压入栈中,set用来取出栈上的值并赋给成员变量。VariableBase根据T和V派生出MemberVariable<T, V>类。MemberVariable<T, V>记录了成员变量在类中的偏移地址,同时具现了get和set这两个接口,因为有类型信息,可以知道从栈上取出是什么类型的值。
struct VariableBase
{
virtual void get(lua_State* L) = 0;
virtual void set(lua_State* L) = 0;
};
template<typename T, typename V>
MemberVariable: VariableBase
{
V T::*_var;
void get(lua_State* L)
{
pushToLuaStack<V>(L, getFromLuaStack<T*>(L, 1)->*(_var)); //从栈底取出对象地址偏移到成员变量所在地址,将成员变量值压入栈中
}
void set(lua_State* L)
{
getFromLuaStack<T*>(L, 1)->*(_var) = getFromLuaStack<V>(L, 3);
}
};
int meta_get(lua_State* L)
{
lua_getmetatable(L, 1); // 将栈底的元表压栈,例子中就是la的类表A
lua_pusvalue(L, 2); // 将栈底上面的元素,就是key复制压入栈;例子中就是"x"
lua_rawget(L, -2); // 得到A[x]的值
if(lua_isuserdata(L, -1))
{
LuaUserDataToType<VariableBase*>::convert(L, -1)->get(L); //将栈底的userdata转
lua_remove(L, -2); // 将类表移出栈
}
...
}
int meta_set(lua_State* L)
{
lua_getmetatable(L, 1); // 将栈底的元表压栈,例子中就是la的类表A
lua_pusvalue(L, 2); // 将栈底上面的元素,就是key复制压入栈;例子中就是"x"
lua_rawget(L, -2); // 得到A[x]的值
if(lua_isuserdata(L, -1))
{
LuaUserDataToType<VariableBase*>::convert(L, -1)->set(L);
}
...
lua_settop(L, 3);
}
注册成员函数
registerClassFunction<A>(L, "funcA", A::funcA)
做了什么?在类表中注册了key为”funcA”的键值对,值是一个闭包。lua_pushcclosure(L,func, n)
函数允许将一个函数func压入栈的同时,指定它之前栈上的n个元素作为upvalue,这就是闭包的概念。这里的闭包是模版类MemberFunctionDelegate<Ret, T, …Args>的静态函数——call函数,upvalue是A::funcA的函数指针。模板类通过模板参数,将所属类T,返回值类型Ret,以及函数的参数列表的类型都记录下来。当la:funcA调用时,实际调用的是MemberFunctionDelegate<Ret, T, …Args>::call
函数,Lua会将la(指向ca的指针)和函数的参数依次压入栈中。call的实现,首先从upvalue中取出成员函数指针,再从栈低到栈顶依次取出,ca的地址和A::funcA的各个参数,有了这些,就能真正地调用ca:funcA(p1,p2,…),并将函数的返回值压入栈。
template<typename RVal, typename T, typename ... Args>
struct MemberFunctionDelegate
{
static int call(lua_State* L)
{
typedef RVal(T::*MemFunc)(Args ...);
MemFunc fun = getUpValue<MemFunc>(L); // 从upvalue中取出成员函数指针
int index = 1;
T* t = getFromLuaStack<T*>(L, index++); //从栈底取出对象地址
pushToLuaStack(L, (t->*fun)(getFromLuaStack<Args>(L, index++)...));
return 1;
}
};
template<typename RVal, typename T, typename ... Args>
pushFunctionDelegate(lua_State* L, RVal(T::*func)(Args...))
{
lua_pushcclosure(L, MemberFunctionDelegate<RVal, T, Args...>::call, 1);
}
template<typename T, typename F>
void registerClassFunction(lua_State* L, const char* name, F func)
{
push_meta(L, ClassName<T>::name()); // 将T的类表压入栈
if(lua_istable(L, -1))
{
lua_pushstring(L, name);
new(lua_newuserdata(L, sizeof(F))) F(func); //成员函数指针作为upvalue
pushFunctionDelegate(L, func);
lua_rawset(L, -3);
}
lua_pop(L, 1);
}
注册构造函数
当我们在Lua中将表当作构造表达式使用时,例如t = T(…),就会调用T的__call元方法。通过下面代码可以看到,注册的构造函数,其实创建了一个ValueToUser
template<typename T, typename ... Args>
int constructor(lua_State* L)
{
int index = 2;
new(lua_newuserdata(L, sizeof(ValueToUser<T>))) ValueToUser<T>(getFromLuaStack<Args>(L, index++)...);
push_meta(L, ClassName<typename ClassType<T>::Type>::name());
lua_setmetatable(L, -2);
return 1;
}
...
template<typename T, typename F>
void addClassConstructor(lua_State* L, F func)
{
push_meta(L, ClassName<T>::name());
if(lua_istable(L, -1))
{
lua_newtable(L);
lua_pushstring(L, "__call");
lua_pushcclosure(L, func, 0);
lua_rawset(L, -3);
lua_setmetatable(L, -2);
}
lua_pop(L, 1);
}
// 注册构造函数示例
addClassConstrutor<A>(L, constructor<A, int>);
// Lua中创建A对象示例
la = A(10);
注册继承关系
C++中定义如下两个类A和B,B继承自A。
Class A
{
public:
A(int a): a(a);
~A();
<span class="kt">int</span> <span class="n">a</span><span class="p">;</span>
<span class="n">TypeY</span> <span class="n">funcA</span><span class="p">(</span><span class="n">Param1</span> <span class="n">p1</span><span class="p">,</span> <span class="n">Param2</span> <span class="n">p2</span><span class="p">,</span> <span class="p">...);</span>
};
Class B : public A
{
public:
B(int b) : A(b+1);
~B();
<span class="kt">int</span> <span class="n">b</span><span class="p">;</span>
<span class="n">TypeW</span> <span class="n">funcB</span><span class="p">(</span><span class="n">Param</span> <span class="n">p1</span><span class="p">,</span> <span class="n">Param2</span> <span class="n">p2</span><span class="p">,</span> <span class="p">...);</span>
};
通过数据交互层提供的接口,不仅可以注册类的成员变量和成员函数,还可以将继承关系继承到Lua中。
registerClass<A>(L, "A"); // L 为lua_State*类型
registerClassFunction<A>(L, "funcA", A::funcA);
registerClassMemberVariable<A>(L, "x", &A::x);
registerClass<B>(L, "B");
registerClassFunction<B>(L, "funcB", B::funcB);
registerClassMemberVariable<B>(L, "z", &B::z);
inherientClass<B, A>(L);
A ca;
registerVariable(L, "la", &ca);
于是在Lua脚本中,通过实例lb不仅可以访问类B的成员变量和函数,可以访问其父类的。
lb = B(3);
print(lb.b);
lb:funcB(…);
print(lb.a);
lb:funcA(…);
这种继承关系,是如何注册到Lua中去的?
看看inherientClass函数的实现:本质上就是将一个类表的__parent指向了父类表。
template<typename T, typename P>
void inherientClass(lua_State* L)
{
push_meta(L, ClassName<T>::name());
if(lua_istable(L, -1))
{
lua_pushstring(L, "__parent"); // 通过__parent元方法,奖励两个类表之间的“继承”关系
push_meta(L, ClassName<P>::name());
lua_rawset(L, -3);
}
lua_pop(L, 1);
}
我们再来看一下完整的meta_get和meta_set函数实现,当在当前类表中查找不到键值对时,就会调用invoke_parent函数,递归地从继承关系链中,向上搜寻父类表或者祖先类表中是否存在键值对。
static void invoke_parent(lua_State* L)
{
lua_pushstring(L, "__parent"); // 取出继承关系中的父类表
lua_rawget(L, -2);
<span class="k">if</span><span class="p">(</span><span class="n">lua_istable</span><span class="p">(</span><span class="n">L</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">))</span> <span class="c1">// 如果存在父类表
{
lua_pushvalue(L, 2); //将栈底上面的元素,即要找的key复制入栈
lua_rawget(L, -2); // 尝试从父类表中找到key对应的value是否存在
if(!lua_isnil(L, -1))
{
lua_remove(L, -2); //如果找到,将父类表移除
}
else
{
lua_remove(L, -1); //将栈顶的nil移除
invoke_parent(L); //递归调用在父类的父类中查找
lua_remove(L, -2); //将父类表移除
}
}
}
int meta_get(lua_State* L)
{
lua_getmetatable(L, 1); //将栈底元素的元表入栈
lua_pushvalue(L, 2); // 复制key并压入栈
lua_rawget(L, -3); // 获取元表[key]的值
if</spa