[LUA] 在 C++ 中快速创建 LUA 中带元表的自定义数据类型变量

陪她去流浪 桃子 2017年03月11日 编辑 阅读次数:4008

前言

LUA 为开发者和用户提供一种叫作自定义数据(UserData)的类型,这种类型允许开发者将自己的 C/C++ 语言中的任意数据类型放进 LUA 环境中,并让用户可以以面向对象的方式来访问这种类型。 而这种机制则是基于为每种自定义数据类型创建其独有的元表(metatable)来实现的(元表与普通表并没有什么区别)。

原理

面向对象形式的写法 obj:method(1,2,3) 在 LUA 中只是一种语法糖,它与 obj.method(obj,1,2,3) 执行起来的作用是等价的(但生成的字节码不一样)。因此需要索引 objmethod 键并调用,但 obj 不是 table 类型哪来的键呢? 这个时候,obj 的元表就出场了。在 LUA 中,索引一个自定义数据类型的变量等于调用其元表的 __index 方法。于是元表暴露了所有能操作该自定义数据类型变量的方法集。因此,有必要为每个自定义数据类型变量设置该自定义数据类型特有的元表

实现过程

由于同一种自定义数据类型的所有变量共享相同的操作方法(而方法由元表提供),所以这个元表应该被保存在全局,以备后续使用。放在全局的哪个地方好呢?当然是注册表咯,即索引号为伪索引 LUA_REGISTRYINDEX 的一个全局、全状态可用的表。

创建自定义数据类型全局元表

这个创建过程只需要执行一次,要么在程序初始化时,要么在第一次创建该自定义数据类型的变量时。

  1. 创建一个普通表

    lua_createtable(L, ...);
    
  2. 设置元表拥有的方法

    const luaL_Reg __methods__[] = 
    {
       {"fun1", fun1},
       {"fun2", fun2},
       {nullptr, nullptr}
    };
    
    luaL_setfuncs(L, __method__, 0);
    
  3. 暴露元表方法查询接口

    lua_pushvalue(L, -1);
    lua_setfield(L, -2, "__index");
    
  4. 将其保存到全局

    lua_pushvalue(L, -1);
    lua_setfield(L, LUA_REGISTRYINDEX, '名字');
    

其中,第一、四步操作可由 LUA 的辅助函数 luaL_newmetatable() 一次性完成。

创建自定义类型并为其设置元表

  1. 创建自定义类型变量

    void* p = lua_newuserdata(L, sizeof(类型));
    

    这里返回的是原始内存,可以强制转换成普通数据指针或构造非普通数据类型对象即可使用。

  2. 拿到该类型的全局元表

    luaL_getmetatable(L, '名字');
    
  3. 设置元表到自定义类型

    lua_setmetatable(L, -2)
    

自此,一个带有元表的自定义数据类型变量就全部创建完成了。

我推荐的写法(C++11)

优点

我推荐的这种写法至少有以下几个优点:

  • 函数模板
  • 变参类模板
  • 完美转发
  • 宏定义
  • 延迟元表创建
  • Placement new

经这样一写,代码变得非常简洁。

包装过程

void SetObjectMetatable(lua_State* L, const char* name, const luaL_Reg* fns)
{
      if(luaL_getmetatable(L, name) == LUA_TNIL) {
        lua_pop(L, 1);
        luaL_newmetatable(L, name);
        luaL_setfuncs(L, fns, 0);
        lua_pushvalue(L, -1);
        lua_setfield(L, -2, "__index");
    }

    lua_setmetatable(L, -2);
}

template<typename T, typename... Args>
T* PushObject(lua_State* L, Args... args)
{
    void* m = lua_newuserdata(L, sizeof(T));

    auto p = new (m) T(std::forward<Args>(args)...);

    SetObjectMetatable(L, T::__name__(), T::__apis__());

    return p;
}

其中出现了几个有特别名字的静态函数:__name____apis__,它们分别用来取得该自定义数据类型的名字和方法集。也因此多了几个特别的宏:

#define LUAAPI(name)                static int name(lua_State* L)

#define DECL_OBJECT(T)                class T

#define DECL_THIS                   auto& O = *__this__(L)

#define _BEG_OBJ_API_(N, T) \
                                    static T* __this__(lua_State* L) {return reinterpret_cast<T*>(luaL_checkudata(L, 1, __name__()));}\
                                    static const char* const __name__() { return "" #N "::" #T; }\
                                    static const wchar_t* const __namew__() { return L"" #N  L"::" #T; }\
                                    static const luaL_Reg* const __apis__() { static luaL_Reg s_apis[] = {

#define BEG_OBJ_API(N, T)           LUAAPI(__gc)\
                                    {\
                                        DECL_THIS;\
                                        O.~T();\
                                        return 0;\
                                    }\
                                    _BEG_OBJ_API_(N, T)\
                                    OBJAPI(__gc)

#define OBJAPI(name)                {#name, name},

#define END_OBJ_API()               {nullptr, nullptr} }; return s_apis; }

自定义数据类型定义方法

DECL_OBJECT(MyObject)
{
public:
    MyObject(/* args */)
    {
    }

    BEG_OBJ_API(::, MyObject)
        OBJAPI(fun1)
        OBJAPI(fun2)
    END_OBJ_API()

protected:
    LUAAPI(fun1)
    {
        DECL_THIS;
        return 0;
    }

    LUAAPI(fun2)
    {
        DECL_THIS;
        return 0;
    }

private:
     // members 
};

自定义数据类型变量创建方法

PushObject<MyObject>(L, /* args */);

一步到位,非常简单。

完整代码

运行结果

MyObject::MyObject: x: 123, s: str
MyObject::fun1: 123
MyObject::~MyObject

标签:C++ · lua