[LUA] 让 LUA 原生支持 C++ 成员函数绑定的一次尝试
本文标题的意思是:直接绑定 C++ 的特定原型的非静态成员函数到 LUA 中,这样在被调用的时候就直接处于 this 环境中。
而不是通过写成 static
的静态成员函数形式,然后从第 1 个参数中取出对象指针,然后访问其数据的面向过程形式的方法。
虽然刚刚才发表过一篇关于快速封装 C++ 中的对象到 LUA 中的一种方法(【LUA】在 C++ 中快速创建 LUA 中带元表的自定义数据类型变量),但是仍然觉得这个过程偏复杂,我指的是对象中数据成员的访问。
直接来段代码吧,这是当前常用的封装 C++ 对象到 LUA 时类定义的写法(未包装):
class MyObject
{
public:
MyObject()
: n(8)
{}
public:
// 从第 1 个参数中取出对象指针
static MyObject* check_this(lua_State* L)
{
return (MyObject*)luaL_checkudata(L, 1, "MyObject");
}
public:
// 静态成员函数
static int sf(lua_State* L)
{
MyObject* self = check_this(L);
// 注意这里对数据成员 n 的访问的写法
std::cout << __FUNCTION__ << ": " << self->n << std::endl;
std::cout << " argc: " << lua_gettop(L) << ": "
<< luaL_checknumber(L, 2) << ","
<< luaL_checkstring(L, 3)
<< std::endl;
return 0;
}
// 一个非静态标准调用方法
int __stdcall mf(lua_State* L)
{
std::cout << __FUNCTION__ << ": " << n << std::endl;
// 注意这里对数据成员 n 的访问的写法
std::cout << " argc: " << lua_gettop(L) << ": "
<< luaL_checknumber(L, 2) << ","
<< luaL_checkstring(L, 3)
<< std::endl;
return 0;
}
protected:
int n;
};
由于向 LUA 注册的 C++ 函数的原型必须是一个全局函数的形式,所以,要想成员函数能够被注册到 LUA 中,该成员函数也必须为全局函数,即:得加上 static
修饰,就像上面的 sf
一样。但是一旦加上了 static
,该就不能直接访问数据成员了。这在写法上带来了诸多不便。
于是想寻找一个优雅的写法。
考虑 LUA 中对象方法的调用语法:obj:method(1,2,3)
,它其实是一个语法糖,它与 obj.method(obj,1,2,3)
的作用是一样的。
它分为两步操作:
- 取得 obj.method 函数(设为
fn
) - 以参数
(obj,1,2,3)
调用此fn
仔细一看,第二步操作的形式正好与调用约定为 __stdcall
的成员函数的手段一致(参考:C/C++/动态链接库DLL中函数的调用约定)。拿前面 MyObject
的 mf
方法来说事,它正好是 __stdcall
调用约定。当有如下代码:
MyObject obj;
obj.mf(L); // L 为 lua_State* 变量
时,编译器为我们生成的第 2 行的代码却是 mf(&obj,L)
这种形式。这正好与 LUA 的调用形式一致。
于是,我打算把一个调用约定为 __stdcall
的成员函数强制转换为 LUA 要求的函数形式注册进 LUA,然后调用时把第一个参数作为对象指针并调用的方法来运作一下,看看会不会有奇迹产生。
我把这种形式的函数原型定义成:typedef int (__stdcall* lua_CPPFunction) (void* this_, lua_State* L);
开干!
首先得把成员函数指针强制转换为全局形式,我使用了一个模板函数来转换:
template<typename T>
lua_CFunction tolcf(T t)
{
union
{
T t;
lua_CFunction f;
} u;
u.t = t;
return u.f;
}
但是,怎么识别这是一个成员函数,而非普通的全局函数呢?为了测试的方便,我限制了一些测试环境:假定系统平台为 Win32 平台, 在这个环境下,用户地址空间的函数指针为 4 个字节大小,并且最高位始终为 0。所以,我特地地把传递给 LUA 的(成员)函数指针的最高位置为 1 以标识这是一个成员函数。于是得改写一下上面的代码片段:
template<typename T>
lua_CFunction tolcf(T t)
{
union
{
T t;
lua_CFunction f;
} u;
u.t = t;
*(int*)&u.f |= 0x80000000;
return u.f;
}
然后来到 LUA 调用我们注册的全局(现在来说并非全局都是)函数的地方:luaD_precall @ ldo.c
,
其中有一行代码是:n = (*f)(L); /* do the actual call */
。f
即是我们注册的函数。现在我们需要在这里动一点手脚:
需要检测一下这到底是一个真正的全局函数,还是我通过强制转换而来的一个成员函数,改成下面这样:
if((int)f & 0x80000000) {
// 这是一个成员函数
// ** 特殊处理 **
// 执行成员函数的标准调用
// 第 1 个参数为对象指针
StkId udp; // 第一个参数地址
lua_CPPFunction pf; // 成员函数指针
void* this; // 对象指针
udp = func + 1; // 取得第一个参数
// 参数校验:第一个参数必须为 UserData
if(L->top - udp < 1 || ttnov(udp) != LUA_TUSERDATA)
luaL_argerror(L, 1, "C++ object(as userdatum) expected");
// 转换回成员函数指针
pf = (lua_CPPFunction)((int)f & 0x7FFFFFFF);
// 拿到对象指针
this = getudatamem(uvalue(udp));
n = (*pf)(this, L);
}
else {
// 这是一个真正的全局函数
n = (*f)(L);
}
上面的代码,我做了一个假设:假设第一个参数(UserData)确实为该成员函数对应的类的对象指针,也就是假设用户不会写像是
obj.mf(another_object)
这种故意搞乱的写法。
完整的测试代码我提交到了 GitHub 上:一次漂亮的尝试 · movsb/luacpp@016f444。
以下是测试输出结果:
MyObject: 000E7FB0
table: 000E6298
function: 008D6C3A
MyObject::sf: 8
argc: 3: 123,456
MyObject::sf: 8
argc: 3: 123,456
function: 808D6C44
MyObject::mf: 8
argc: 3: 123,456
MyObject::mf: 8
argc: 3: 123,456
实验证明:这种办法确实可行。
虽然我做了几个假设,不能通用。但是这几种假设却可以通过一些手段给检测出来的。 后续我应该会再写一篇文章来做没有假设的实现。
可能有人会担心效率问题。不过我不太这样认为,因为相比原代码而言,只是在执行之前多了一个是否是成员函数的判断,但这个判断几乎没有效率问题。如果是成员函数,则又进行了几个简单极其简单的操作后,我仍然觉得这不会对效率产生影响,因为那代码真的是简单到无可挑剔。