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