IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    类型安全的 C++/Lua 任意参数互调用

    Gu Lu\'s Blog发表于 2016-05-19 14:42:00
    love 0

    在 C++ 和 Lua 协作时,双方的互调用是一个绕不开的话题。通常情况下,我们直接使用 Lua/C API 就可以完成普通的参数传递过程。但在代码中直接操作 lua stack,容易写出繁冗和重复的代码。这时我们往往会借助 tolua++ 之类的库,把参数传递的工作自动化,降低负担。

    进一步讲,由于 Lua 的参数传递在个数和类型上非常灵活(任何一个函数可以传递任意个数和类型的参数),有时我们会希望在与 C++ 的互操作时保留这种灵活性,比如 C++ 向 Lua 发一个消息时,如果可以是一个消息 ID 带上任意数量和类型的参数,就会很方便(反过来也一样)。由于 C++ 能通过可变参的模板函数实现类型安全的参数传递,与 Lua 的动态参数列表相结合后,我们就能在一个接口上实现更大的跨语言自由度。


    有不少第三方库能够简化 C++ 和 Lua 之间的互调用,这次我们使用 LuaBridge 来完成工作。开始前我们先介绍一下普通的互调用怎么做。

    首先,从 C++ 调 Lua 的函数:

    -- lua side
    function foo(str, i, f)
        return string.format("%s, %d, %f", str, i, f)
    end
    
    // C side
    luabridge::LuaRef foo = luabridge::getGlobal(L, 'foo');
    auto retString = foo("bar", 12, 0.25f);   // 这里先忽略错误处理
    

    接着是 Lua 调 C++:

    // C side
    int CallMe(const std::string& arg1, const std::string& arg2)
    {
        return std::stoi(arg1) + std::stoi(arg2); // 同样先不管错误处理
    }
    
    luabridge::getGlobalNamespace(L)
        .beginNamespace("native")
        .addFunction("call_me", CallMe)
        .endNamespace();
    
    -- lua side
    sum = native.call_me("15", "20")    -- sum = 35
    

    嗯,可以看到,在 LuaBridge 的帮助下,双方互调用的参数和返回值符合各自的习惯,不用写任何额外的代码。


    好了,热身完毕。现在我们看一下 C++ 调用 Lua 的可变参接口。

    -- lua side
    function g_post(msgID, ...)
        _queue:appendMsg({id=msgID, args={...}})
    end
    

    我们在可作为 functor 使用的 luabridge::LuaRef 上做一个简单的封装,如下:

    template<class TRet, class... U>
    TRet PostMessage(U&&... u) 
    {
        // 获取对应的函数
        auto refFunc = GetGlobal("g_post");
        if (!refFunc.isFunction())
            return luabridge::LuaRef(m_state);
    
        // 生成携带所有参数的 functor
        auto func = std::bind(refFunc, std::forward<U>(u)...);
    
        // implCallGlobal() 实现略, 使用 try/catch 处理错误,并把返回值转回需要的类型
        return implCallGlobal(name, func);
    }
    

    有了这样的接口,就可以在 C++ 这边用下面的方式去调:

    // C side
    PostMessage(MsgType_A, "foo", "bar");
    PostMessage(MsgType_B, 100, 0.25f, std::string("std::string goes as well.");
    // 任意的参数组合...
    

    而在 Lua 端的队列里,就可以得到

    -- lua side
    { id=MsgType_A, args={"foo", "bar" } }
    { id=MsgType_B, args={100, 0.25f, "std::string goes as well." } }
    -- args 表内可以容纳传过来的任意参数 
    

    对于特定的消息类型,Lua 只需检测自己关心的参数是否匹配即可。
    这样从某种程度上把动态语言的灵活性延伸到了宿主语言。


    而反过来 Lua 以任意参数化的方式调 C++ 就稍麻烦一点,因为 C++ 本质上是静态的,函数的参数类型需要在编译时完全确定。

    我们可以这么做:

    -- 在 Lua 端简单封装一下
    function g_post_native(msgID, ...)
        native.post(msgID, {...}})
    end
    
    // C side
    int Post(int msgID, luabridge::LuaRef args)
    {
        switch (msgID)
        {
            case MsgA:
            {
                auto t = tuple_cast<std::string, std::string>(args);
                return ProcessA(std::get<0>(t), std::get<1>(t));
            }
            case MsgB:
                auto t = tuple_cast<int, float, float>(args);
                return ProcessB(std::get<0>(t), std::get<1>(t), std::get<2>(t));
            }
        }
    
        return FAILED_BAD_ID;
    }
    

    这里使用 tuple_cast 的好处是把所有的类型转换重复代码收拢到一处,对自定义类型的扩展也很容易。 tuple_cast() 函数本质上是把一个 LuaRef 根据期望类型(由模板参数指定)展开成一个 std::tuple,对于任何一组给定的类型,递归地在编译期完成展开。具体的技术在之前的 blog 中有提到,这里不再赘述。

    好了,现在可以在 Lua 端这样调了:

    -- lua side
    g_post_native(MsgType_A, "foo", "bar");
    g_post_native(MsgType_B, 100, 0.1f, 12.5f);
    

    然后在 C++ 端直接定义接受明确参数列表的函数

    // C side
    int ProcessA(const std::string& s1, const std::string& s2);
    int ProcessB(int arg1, float arg2, float arg3);
    

    这样的最大好处是,不管是写脚本的脚本程序员,还是写宿主语言的工程师,都可以以各自语言习惯的方式去写,尤其是 C++ 端程序员,总是可以用 tuple_cast 转成自己期望的参数列表,让所有的接口函数做到 self-documenting。

    [完]
    Gu Lu
    [2016-05-19]



沪ICP备19023445号-2号
友情链接