在 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(L);

    // 生成携带所有参数的 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.25, "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 可以用 template <int N> 来避免分支处理,并消除每一个 case 内重复的代码。具体实现暂略,这里为了清晰直接手写
    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.1, 12.5);

然后在 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

Comments
Write a Comment

Tags

游戏开发   随笔   Programming   C/C++   优化   Unity   C++   知乎   游戏设计   比特币   Unity3D   区块链   软件开发   引擎设计   系统架构   Production   idtech   中国文化   加密货币   项目管理   Bitcoin   游戏评论   资源管理   资源流水线   效率   道德经   网络   方法论   模板编程   Lua   Oculus   GDC   渲染   VR   PerfAssist   Unity MemoryProfiler   BCH   经济学   信息过载   行业报告   字体   Productivity   图形   网络编程   Dice   协程   EMC   Premake   测试   中间件   Game Engine   新手引导   Methodology   CI   命令行解析   goroutine   ndk   Ethereum   nanomsg   自动化   Scripting   摘录   Debugging   同步技术   cppcon   C++模板   DOOM3   技术评估   Unity GC   C++11   学习方法   Surface Pro 3   Engine Evaluation   CRT   文化   Blockchain   笔记   golang   图形编程   多线程   ETH   Blockchain Computing   Bitcoin Cash   cppcon14   Bitcoin SV   Visual Studio   Unity Coroutine   跨语言可变参数列表   团队协作   货币   Deployment   Visual Assist   工程改进   Michael Abrash   exp   开放世界   域名   虚拟现实   系统重构   slua   遮挡剔除   完美转发   协作式调度   Modern C++   类型推导   Memory Debugging   个人成长   小故事   BTC   暴雪   产品   历史   错误处理   Unity Profiler   MOD  

知识共享许可协议
本作品由Gu Lu创作,采用知识共享Attribution-NonCommercial-NoDerivatives 4.0 国际许可协议进行许可。