【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) 的作用是一样的。 它分为两步操作:

  1. 取得 obj.method 函数(设为 fn
  2. 以参数 (obj,1,2,3) 调用此 fn

仔细一看,第二步操作的形式正好与调用约定为 __stdcall 的成员函数的手段一致(参考:C/C++/动态链接库DLL中函数的调用约定)。拿前面 MyObjectmf 方法来说事,它正好是 __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

实验证明:这种办法确实可行

虽然我做了几个假设,不能通用。但是这几种假设却可以通过一些手段给检测出来的。 后续我应该会再写一篇文章来做没有假设的实现。

可能有人会担心效率问题。不过我不太这样认为,因为相比原代码而言,只是在执行之前多了一个是否是成员函数的判断,但这个判断几乎没有效率问题。如果是成员函数,则又进行了几个简单极其简单的操作后,我仍然觉得这不会对效率产生影响,因为那代码真的是简单到无可挑剔。

发表于:2017年3月18日,阅读量:389,标签:lua · C++