型別安全的 C++/Lua 任意參數互調用

來源:互聯網
上載者:User

在 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。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.