1. 程式人生 > 其它 >FFLIB之FFLUA——C++嵌入Lua&擴充套件Lua利器

FFLIB之FFLUA——C++嵌入Lua&擴充套件Lua利器

摘要:

在使用C++做伺服器開發中,經常會使用到指令碼技術,Lua是最優秀的嵌入式指令碼之一。Lua的輕量、小巧、概念之簡單,都使他變得越來越受歡迎。本人也使用過python做嵌入式指令碼,二者各有特點,關於python之後會寫相關的文章,python對於我而言更喜歡用來編寫工具,我前邊一些相關的演算法也是用python來實現的。今天主要講Lua相關的開發技術。Lua具有如下特點:

  • Lua 擁有虛擬機器的概念,而其全部用標準C實現,不依賴任何庫即可編譯安裝,更令人欣喜的是,整個Lua 的實現程式碼並不算多,可以直接繼承到專案中,並且對專案的編譯時間幾乎沒有什麼影響
  • Lua的虛擬機器是執行緒安全的,這裡講的執行緒安全級別指得是STL的執行緒安全級別,即一個lua虛擬機器被一個執行緒訪問是安全的,多個lua虛擬機器被多個執行緒分別訪問也是安全的,一個lua虛擬機器被多個執行緒訪問是不安全的。
  • Lua的概念非常少,資料結構只有table,這樣當使用Lua作為專案的配置檔案時,即使沒有程式設計功底的策劃也可以很快上手編寫。
  • Lua沒有原生的物件,沒有class的關鍵字,這也保障了其概念簡單,但是仍然是可以使用Lua面向物件程式設計的。
  • Lua儘管小巧,卻支援比較先進的程式設計正規化,lua 中的匿名函式和閉包會讓程式碼寫起來更加 優雅和高效,如果某人使用的C++ 編譯器還比較老套,不支援C++11,那麼可以儘快感受一下lua的匿名函式和閉包。
  • Lua是最高效的嵌入式指令碼之一(如果不能說最的話,目前證據顯示是最)。
  • Lua的垃圾回收也可以讓C++程式收益匪淺,這也是C++結合指令碼技術的重要優勢之一。
  • Lua 的嵌入非常的容易,CAPI 相對比較簡潔,而且文件清晰,當然Lua的Capi需要掌握Lua中獨特的堆疊的概念,但仍然足夠簡單。
  • Lua的擴充套件也非常的容易,將C++是物件、函式匯入到lua中會涉及到一些技巧,如果純粹使用lua CAPI會稍顯繁雜,幸運的是一些第三方庫簡化了這些操作,而FFLUA絕對是最好用的之一。

嵌入Lua:

嵌入lua指令碼,必須要把lua指令碼載入lua虛擬機器,lua中的概念稱之為dofile,FFLUA中封裝了dofile的操作,由於lua檔案可能集中放在某個目錄,FFLUA中也提供了設定lua指令碼目錄的介面:

int  add_package_path(const string& str_)
int  load_file(const string& file_name_) throw (lua_exception_t)

load_file就是執行dofile操作,若出錯,則throw異常物件,可以使用exception引用目標物件使用what介面輸出程式碼出錯的traceback。

當嵌入lua時,最簡單的情況是把lua腳本當成配置使用,那麼需要獲取lua指令碼中的變數和設定lua變數,FFLUA封裝了兩個介面用於此操作。lua是動態語言,變數可以被賦值為任何lua支援的型別,但C++是強型別的,所以兩個介面都是範型的:

    template<typenameT>
    int  get_global_variable(conststring& field_name_, T& ret_);
    template<typenameT>
    int  get_global_variable(constchar* field_name_, T& ret_);

有時需要直接執行一些lua語句,lua中有dostring的概念,FFLUA中封裝了單獨的介面run_string:

void run_string(constchar* str_) throw (lua_exception_t)

嵌入lua時最一般的情況是呼叫lua中的函式,lua的函式比C++更靈活,可以支援任意多個引數,若未賦值,自動設定為nil,並且可以返回多個返回值。無論如何,從C++角度講,當你嵌入lua呼叫lua函式時,你總希望lua的使用方式跟C++越像越好,你不希望繁複的處理呼叫函式的引數問題,比如C++資料轉換成lua能處理的資料,即無趣又容易出錯。正也正是FFLUA需要做到,封裝呼叫lua函式的操作,把賦值引數,呼叫函式,接收返回值的細節做到透明,C++呼叫者就像呼叫普通的C++函式一樣。使用FFLUA中呼叫lua函式使用call介面:

void call(constchar* func_name_) throw (lua_exception_t)

當調用出錯時,異常資訊記錄了traceback。

實際上,FFLUA過載了9個call函式,以來自動適配呼叫9個引數的lua函式。

template<typename RET>
     RET call(const char* func_name_) throw (lua_exception_t);
    ......
    template<typename RET, typename ARG1, typename ARG2, typename ARG3, typename ARG4,
             typename ARG5, typename ARG6, typename ARG7, typename ARG8, typename ARG9>
    RET call(const char* func_name_, ARG1 arg1_, ARG2 arg2_, ARG3 arg3_,
             ARG4 arg4_, ARG5 arg5_, ARG6 arg6_, ARG7 arg7_,
             ARG8 arg8_, ARG9 arg9_) throw (lua_exception_t);

需要註明的是:

  • call介面的引數是範型的,自動會使用範型traits機制轉換成lua型別,並push到lua堆疊中
  • call介面的返回值也是正規化的,這就要求使用call時必須提供返回值的型別,如果lua函式不返回值會怎樣?lua中有個特性,只有nil和false的布林值為false,所以當lua函式返回空時,你仍然可以使用bool型別接收引數,只是呼叫者忽略其返回值就行了。
  • call只支援一個返回值,雖然lua可以返回多個值,但是call會忽略其他返回值,這也是為了儘可能像是呼叫C++函式,若要返回多個值,完全可以用table返回。

擴充套件LUA:

這也是非常重要的操作,嵌入lua總是和擴充套件lua相伴相行。lua若要操作C++中的物件或函式,那麼必須先把C++對應的介面註冊都lua中。Lua CAPI提供了一系列的介面擁有完成此操作,但是關於堆疊的操作總是會稍顯頭疼,fflua極大的簡化了註冊C++物件和介面的操作,可以說是最簡單的註冊方式之一(如果不準說最的話)。首先我們整理一下需要哪些註冊操作:

  • C++ 靜態函式註冊為lua中的全域性函式,這樣在lua中呼叫C++函式就像是呼叫C++全域性函式
  • C++物件註冊成Lua中的物件,可以通過new介面在lua中建立C++物件
  • C++類中的屬性註冊到lua,lua訪問物件的屬性就像是訪問table中的屬性一樣。
  • C++類中的函式註冊到lua中,lua呼叫其介面就像是呼叫talbe中的介面一樣。

FFLUA中提供了一個範型介面,適配於註冊C++相關資料:

template<typename T>
void  fflua_t::reg(T a)
{
    a(this->get_lua_state());
}

這樣,若要對lua進行註冊操作,只需要提供一個仿函式即可,這樣可以批量在註冊所有的C++資料,當然FFLUA中提供了工具類用於生成仿函式中應該完成的註冊操作:

template<typename CLASS_TYPE = op_tool_t, typename CTOR_TYPE = void()>
class fflua_register_t
{
public:
    fflua_register_t(lua_State* ls_):m_ls(ls_){}
    fflua_register_t(lua_State* ls_, const string& class_name_, string inherit_name_ = "");


    template<typename FUNC_TYPE>
    fflua_register_t& def(FUNC_TYPE func, const string& s_)
    {
        fflua_register_router_t<FUNC_TYPE>::call(this, func, s_);
        return *this;
    }
};

剛才提到的像lua中的所有註冊操作,都可以使用def操作完成。 示例如下:

//! 註冊子類,ctor(int) 為建構函式, foo_t為型別名稱, base_t為繼承的基類名稱
    fflua_register_t<foo_t, ctor(int)>(ls, "foo_t", "base_t")
                .def(&foo_t::print, "print")        //! 子類的函式
                .def(&foo_t::a, "a");               //! 子類的欄位

尤其特別的是,C++中的繼承可以在註冊到lua中被保持這樣註冊過基類的介面,子類就不需要重複註冊。

高階特性:

通過以上的介紹,也許你已經瞭解了FFLUA的設計原則,即:當在編寫C++程式碼時,希望使用LUA就像使用C++本地的程式碼一樣,而在lua中操作C++的資料和介面的時候,又希望C++用起來完全跟table一個樣。這樣可以大大減輕程式開發的工作,從而把精力更多放大設計和邏輯上。那麼做到如何lua才算像C++,C++做到如何才算像lua呢?我們知道二者畢竟相差甚遠,我們只需要把常見的操作封裝成一直即可,不常見操作則特殊處理。常見操作有:

  • C++ 呼叫lua函式,FFLUA已經封裝了call函式,保障了呼叫lua函式就像呼叫本地C++函式一樣方便
  • C++註冊介面和物件到lua中,lua中操作物件就像操作table一樣直接。
  • C++中除了自定義物件,STL是用的最多的了,C++希望lua中能夠接收STL的引數,或者能夠返回STL資料結構
  • Lua中只有table資料結構,Lua希望C++的引數的資料結構支援table,並且lua可以直接把table作為返回值。
  • C++的指標需要傳遞到lua中,同時也希望某些操作,lua可以把C++物件指標作為返回值

以上前兩者已經介紹了,而後三者FFLUA也是給予 完美支援。通過範型的C++封裝,可以將C++ STL完美的轉換成luatable,同時在lua返回table的時候,自動根據返回值型別將lua的table轉換成C++ STL。FFLUA中只要被註冊過的C++物件,都可以把其指標作為引數賦值給lua,甚至在lua中儲存。當我講述以上特性的時候,都是在保證型別安全的前提下。重要的型別檢查有:

  • STL轉成Luatable時,STL中的型別必須是lua支援的,包括基本型別和已經註冊過的C++物件指標。並且STL可以巢狀使用,如vector<list<int> >,  不要驚訝,這是支援的,不管巢狀多少層,都是支援的,使用C++模板的遞迴機制,該特性得到了完美支援。vector、list、set都會轉換成table的陣列模式,key從1開始累加。而map型別自動適配為table字典。
  • LUA中的table可以被當成返回值轉換成C++ STL,轉換跟上邊剛好是對應的,當然有一個限制,由於C++的STL型別必須是唯一的,如vector<int>的返回值就要求lua中的table所有值都是int。否則FFLUA會返回出錯,並提示型別轉換失敗
  • 無論死呼叫lua中使用C++物件指標,還是LuA中返回C++物件指標,該物件必須是lua可以識別的,即已經被註冊的,否則FFLUA會提示轉換型別失敗。

關於過載:

  關於過載LUA 可以使用lua中內部自己的reload,也可以將fflua物件銷燬後,重先建立一個,建立fflua物件的開銷和建立lua虛擬機器的開銷一直,不會有附加開銷。

總結:

  • FFLUA是簡化C++嵌入繫結lua指令碼的類庫
  • FFLUA只有三個標頭檔案,不依賴除lua之外的任何的類庫,開發者可以非常容易的使用FFLUA
  • FFLUA 對於常用的STL資料結構進行了支援
  • FFLUA 即使擁有了這麼多特性,仍然保持了輕量,只要用過C++,只要用過lua,FFLUA的程式碼就可以非常清晰的看清其實現,當你瞭解其內部實現時,你會發現FFLUA已經做到了極簡,範型模板展開後的程式碼就跟你自己原生LUA CAPI 編寫的一樣直接。
  • FFLUA的開原始碼:https://github.com/fanchy/fflua

完整的C++示例程式碼:

#include <iostream>
#include <string>
#include <assert.h>
using namespace std;

#include "lua/fflua.h"

using namespace ff;

class base_t
{
public:
    base_t():v(789){}
    void dump()
    {
        printf("in %s a:%dn", __FUNCTION__, v);
    }
    int v;
};
class foo_t: public base_t
{
public:
    foo_t(int b):a(b)
    {
        printf("in %s b:%d this=%pn", __FUNCTION__, b, this);
    }
    ~foo_t()
    {
        printf("in %sn", __FUNCTION__);
    }
    void print(int64_t a, base_t* p) const
    {
        printf("in foo_t::print a:%ld p:%pn", (long)a, p);
    }

    static void dumy()
    {
        printf("in %sn", __FUNCTION__);
    }
    int a;
};

//! lua talbe 可以自動轉換為stl 物件
void dumy(map<string, string> ret, vector<int> a, list<string> b, set<int64_t> c)
{
    printf("in %s begin ------------n", __FUNCTION__);
    for (map<string, string>::iterator it =  ret.begin(); it != ret.end(); ++it)
    {
        printf("map:%s, val:%s:n", it->first.c_str(), it->second.c_str());
    }
    printf("in %s end ------------n", __FUNCTION__);
}

static void lua_reg(lua_State* ls)
{
    //! 註冊基類函式, ctor() 為建構函式的型別
    fflua_register_t<base_t, ctor()>(ls, "base_t")  //! 註冊建構函式
                    .def(&base_t::dump, "dump")     //! 註冊基類的函式
                    .def(&base_t::v, "v");          //! 註冊基類的屬性

    //! 註冊子類,ctor(int) 為建構函式, foo_t為型別名稱, base_t為繼承的基類名稱
    fflua_register_t<foo_t, ctor(int)>(ls, "foo_t", "base_t")
                .def(&foo_t::print, "print")        //! 子類的函式
                .def(&foo_t::a, "a");               //! 子類的欄位

    fflua_register_t<>(ls)
                .def(&dumy, "dumy");                //! 註冊靜態函式
}

int main(int argc, char* argv[])
{

    fflua_t fflua;
    try 
    {
        //! 註冊C++ 物件到lua中
        fflua.reg(lua_reg);
        
        //! 載入lua檔案
        fflua.add_package_path("./");
        fflua.load_file("test.lua");
        
        //! 獲取全域性變數
        int var = 0;
        assert(0 == fflua.get_global_variable("test_var", var));
        //! 設定全域性變數
        assert(0 == fflua.set_global_variable("test_var", ++var));

        //! 執行lua 語句
        fflua.run_string("print("exe run_string!!")");
        
        //! 呼叫lua函式, 基本型別作為引數
        int32_t arg1 = 1;
        float   arg2 = 2;
        double  arg3 = 3;
        string  arg4 = "4";
        fflua.call<bool>("test_func", arg1, arg2, arg3,  arg4);
        
        //! 呼叫lua函式,stl型別作為引數, 自動轉換為lua talbe
        vector<int> vec;        vec.push_back(100);
        list<float> lt;         lt.push_back(99.99);
        set<string> st;         st.insert("OhNIce");
        map<string, int> mp;    mp["key"] = 200;
        fflua.call<string>("test_stl", vec, lt, st,  mp);
        
        //! 呼叫lua 函式返回 talbe,自動轉換為stl結構
        vec = fflua.call<vector<int> >("test_return_stl_vector");
        lt  = fflua.call<list<float> >("test_return_stl_list");
        st  = fflua.call<set<string> >("test_return_stl_set");
        mp  = fflua.call<map<string, int> >("test_return_stl_map");
        
        //! 呼叫lua函式,c++ 物件作為引數, foo_t 必須被註冊過
        foo_t* foo_ptr = new foo_t(456);
        fflua.call<bool>("test_object", foo_ptr);
        
        //! 呼叫lua函式,c++ 物件作為返回值, foo_t 必須被註冊過 
        assert(foo_ptr == fflua.call<foo_t*>("test_ret_object", foo_ptr));
        //! 呼叫lua函式,c++ 物件作為返回值, 自動轉換為基類
        base_t* base_ptr = fflua.call<base_t*>("test_ret_base_object", foo_ptr);
        assert(base_ptr == foo_ptr);
 
    }
    catch (exception& e)
    {
        printf("exception:%sn", e.what());
    }
    return 0;
}

完整的LUA示例程式碼:

test_var = 99

function dump_table(tb, str)
    if nil == str then str = "" end
    for k, v in pairs(tb)
    do
        print(str, k, v)
    end
end

-- 測試呼叫lua
function test_func(arg1, arg2, arg3, arg4)
    print("in test_func:", arg1, arg2, arg3, arg4)
    mp = {["k"] = "v"}
    vc = {1,2,3}
    lt = {4,5,6}
    st = {7,8,9}
    dumy(mp, vc, lt, st)
end

-- 接受stl引數
function test_stl(vec, lt, st, mp)
    print("--------------dump_table begin ----------------")
    dump_table(vec, "vec")
    dump_table(lt, "lt")
    dump_table(st, "st")
    dump_table(mp, "mp")
    print("--------------dump_table end ----------------")
    return "ok"
end

-- 返回stl 引數
function test_return_stl_vector()
    return {1,2,3,4}
end
function test_return_stl_list()
    return {1,2,3,4}
end
function test_return_stl_set()
    return {1,2,3,4}
end
function test_return_stl_map()
    return {
        ["key"] = 124
    }
end
-- 測試接受C++物件
function test_object(foo_obj)
    --測試構造
    base = base_t:new()
    -- 每個物件都有一個get_pointer獲取指標
    print("base ptr:", base:get_pointer())
    -- 測試C++物件函式
    foo_obj:print(12333, base)
    base:delete()
    --基類的函式
    foo_obj:dump()
    -- 測試C++ 物件屬性
    print("foo property", foo_obj.a)
    print("base property", foo_obj.v)
end

-- 測試返回C++物件
function test_ret_object(foo_obj)
    return foo_obj
end

-- 測試返回C++物件
function test_ret_base_object(foo_obj)
    return foo_obj
end