ffluaC++ 封裝的 Lua 擴(kuò)展庫
在使用C++做服務(wù)器開發(fā)中,經(jīng)常會(huì)使用到腳本技術(shù),Lua是最優(yōu)秀的嵌入式腳本之一。Lua的輕量、小巧、概念之簡(jiǎn)單,都使他變得越來越受歡迎。本人也使用過python做嵌入式腳本,二者各有特點(diǎn),今天主要講Lua相關(guān)的開發(fā)技術(shù)。Lua具有如下特點(diǎn):
-
Lua 擁有虛擬機(jī)的概念,而其全部用標(biāo)準(zhǔn)C實(shí)現(xiàn),不依賴任何庫即可編譯安裝,更令人欣喜的是,整個(gè)Lua 的實(shí)現(xiàn)代碼并不算多,可以直接繼承到項(xiàng)目中,并且對(duì)項(xiàng)目的編譯時(shí)間幾乎沒有什么影響
-
Lua的虛擬機(jī)是線程安全的,這里講的線程安全級(jí)別指得是STL的線程安全級(jí)別,即一個(gè)lua虛擬機(jī)被一個(gè)線程訪問是安全的,多個(gè)lua虛擬機(jī)被多個(gè)線程分別訪問也是安全的,一個(gè)lua虛擬機(jī)被多個(gè)線程訪問是不安全的。
-
Lua的概念非常少,數(shù)據(jù)結(jié)構(gòu)只有table,這樣當(dāng)使用Lua作為項(xiàng)目的配置文件時(shí),即使沒有編程功底的策劃也可以很快上手編寫。
-
Lua沒有原生的對(duì)象,沒有class的關(guān)鍵字,這也保障了其概念簡(jiǎn)單,但是仍然是可以使用Lua面向?qū)ο缶幊痰摹?/p>
-
Lua盡管小巧,卻支持比較先進(jìn)的編程范式,lua 中的匿名函數(shù)和閉包會(huì)讓代碼寫起來更加 優(yōu)雅和高效,如果某人使用的C++ 編譯器還比較老套,不支持C++11,那么可以盡快感受一下lua的匿名函數(shù)和閉包。
-
Lua是最高效的嵌入式腳本之一(如果不能說最的話,目前證據(jù)顯示是最)。
-
Lua的垃圾回收也可以讓C++程序收益匪淺,這也是C++結(jié)合腳本技術(shù)的重要優(yōu)勢(shì)之一。
-
Lua 的嵌入非常的容易,CAPI 相對(duì)比較簡(jiǎn)潔,而且文檔清晰,當(dāng)然Lua的Capi需要掌握Lua中獨(dú)特的堆棧的概念,但仍然足夠簡(jiǎn)單。
-
Lua的擴(kuò)展也非常的容易,將C++是對(duì)象、函數(shù)導(dǎo)入到lua中會(huì)涉及到一些技巧,如果純粹使用lua CAPI會(huì)稍顯繁雜,幸運(yùn)的是一些第三方庫簡(jiǎn)化了這些操作,而FFLUA絕對(duì)是最好用的之一。
嵌入Lua:
嵌入lua腳本,必須要把lua腳本載入lua虛擬機(jī),lua中的概念稱之為dofile,F(xiàn)FLUA中封裝了dofile的操作,由于lua文件可能集中放在某個(gè)目錄,F(xiàn)FLUA中也提供了設(shè)置lua腳本目錄的接口:
int add_package_path(const string& str_) int load_file(const string& file_name_) throw (lua_exception_t)
load_file就是執(zhí)行dofile操作,若出錯(cuò),則throw異常對(duì)象,可以使用exception引用目標(biāo)對(duì)象使用what接口輸出代碼出錯(cuò)的traceback。
當(dāng)嵌入lua時(shí),最簡(jiǎn)單的情況是把lua腳本當(dāng)成配置使用,那么需要獲取lua腳本中的變量和設(shè)置lua變量,F(xiàn)FLUA封裝了兩個(gè)接口用于此操作。lua是動(dòng)態(tài)語言,變量可以被賦值為任何lua支持的類型,但C++是強(qiáng)類型的,所以兩個(gè)接口都是范型的:
template<typenameT> int get_global_variable(conststring& field_name_, T& ret_); template<typenameT> int get_global_variable(constchar* field_name_, T& ret_);
有時(shí)需要直接執(zhí)行一些lua語句,lua中有dostring的概念,F(xiàn)FLUA中封裝了單獨(dú)的接口run_string:
void run_string(constchar* str_) throw (lua_exception_t)
嵌入lua時(shí)最一般的情況是調(diào)用lua中的函數(shù),lua的函數(shù)比C++更靈活,可以支持任意多個(gè)參數(shù),若未賦值,自動(dòng)設(shè)置為nil,并且可以返回多個(gè)返回值。無論如何,從C++角度講,當(dāng)你嵌入lua調(diào)用lua函數(shù)時(shí),你總希望lua的使用方式跟C++越像越好,你不希望繁復(fù)的處理調(diào)用函數(shù)的參數(shù)問題,比如C++數(shù)據(jù)轉(zhuǎn)換成lua能處理的數(shù)據(jù),即無趣又容易出錯(cuò)。正也正是FFLUA需要做到,封裝調(diào)用lua函數(shù)的操作,把賦值參數(shù),調(diào)用函數(shù),接收返回值的細(xì)節(jié)做到透明,C++調(diào)用者就像調(diào)用普通的C++函數(shù)一樣。使用FFLUA中調(diào)用lua函數(shù)使用call接口:
void call(constchar* func_name_) throw (lua_exception_t)
當(dāng)調(diào)用出錯(cuò)時(shí),異常信息記錄了traceback。
實(shí)際上,F(xiàn)FLUA重載了9個(gè)call函數(shù),以來自動(dòng)適配調(diào)用9個(gè)參數(shù)的lua函數(shù)。
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接口的參數(shù)是范型的,自動(dòng)會(huì)使用范型traits機(jī)制轉(zhuǎn)換成lua類型,并push到lua堆棧中
-
call接口的返回值也是范式的,這就要求使用call時(shí)必須提供返回值的類型,如果lua函數(shù)不返回值會(huì)怎樣?lua中有個(gè)特性,只有nil和false的布爾值為false,所以當(dāng)lua函數(shù)返回空時(shí),你仍然可以使用bool類型接收參數(shù),只是調(diào)用者忽略其返回值就行了。
-
call只支持一個(gè)返回值,雖然lua可以返回多個(gè)值,但是call會(huì)忽略其他返回值,這也是為了盡可能像是調(diào)用C++函數(shù),若要返回多個(gè)值,完全可以用table返回。
擴(kuò)展LUA:
這也是非常重要的操作,嵌入lua總是和擴(kuò)展lua相伴相行。lua若要操作C++中的對(duì)象或函數(shù),那么必須先把C++對(duì)應(yīng)的接口注冊(cè)都lua中。Lua CAPI提供了一系列的接口擁有完成此操作,但是關(guān)于堆棧的操作總是會(huì)稍顯頭疼,fflua極大的簡(jiǎn)化了注冊(cè)C++對(duì)象和接口的操作,可以說是最簡(jiǎn)單的注冊(cè)方式之一(如果不準(zhǔn)說最的話)。首先我們整理一下需要哪些注冊(cè)操作:
-
C++ 靜態(tài)函數(shù)注冊(cè)為lua中的全局函數(shù),這樣在lua中調(diào)用C++函數(shù)就像是調(diào)用C++全局函數(shù)
-
C++對(duì)象注冊(cè)成Lua中的對(duì)象,可以通過new接口在lua中創(chuàng)建C++對(duì)象
-
C++類中的屬性注冊(cè)到lua,lua訪問對(duì)象的屬性就像是訪問table中的屬性一樣。
-
C++類中的函數(shù)注冊(cè)到lua中,lua調(diào)用其接口就像是調(diào)用talbe中的接口一樣。
FFLUA中提供了一個(gè)范型接口,適配于注冊(cè)C++相關(guān)數(shù)據(jù):
template<typename T> void fflua_t::reg(T a)
{
a(this->get_lua_state());
}
這樣,若要對(duì)lua進(jìn)行注冊(cè)操作,只需要提供一個(gè)仿函數(shù)即可,這樣可以批量在注冊(cè)所有的C++數(shù)據(jù),當(dāng)然FFLUA中提供了工具類用于生成仿函數(shù)中應(yīng)該完成的注冊(cè)操作:
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中的所有注冊(cè)操作,都可以使用def操作完成。 示例如下:
//! 注冊(cè)子類,ctor(int) 為構(gòu)造函數(shù), foo_t為類型名稱, //base_t為繼承的基類名稱 fflua_register_t<foo_t, ctor(int)>(ls, "foo_t", "base_t") .def(&foo_t::print, "print") //! 子類的函數(shù) .def(&foo_t::a, "a"); //! 子類的字段
尤其特別的是,C++中的繼承可以在注冊(cè)到lua中被保持這樣注冊(cè)過基類的接口,子類就不需要重復(fù)注冊(cè)。
高級(jí)特性:
通過以上的介紹,也許你已經(jīng)了解了FFLUA的設(shè)計(jì)原則,即:當(dāng)在編寫C++代碼時(shí),希望使用LUA就像使用C++本地的代碼一樣,而在lua中操作C++的數(shù)據(jù)和接口的時(shí)候,又希望C++用起來完全跟table一個(gè)樣。這樣可以大大減輕程序開發(fā)的工作,從而把精力更多放大設(shè)計(jì)和邏輯上。那么做到如何lua才算像C++,C++做到如何才算像lua呢?我們知道二者畢竟相差甚遠(yuǎn),我們只需要把常見的操作封裝成一直即可,不常見操作則特殊處理。常見操作有:
-
C++ 調(diào)用lua函數(shù),F(xiàn)FLUA已經(jīng)封裝了call函數(shù),保障了調(diào)用lua函數(shù)就像調(diào)用本地C++函數(shù)一樣方便
-
C++注冊(cè)接口和對(duì)象到lua中,lua中操作對(duì)象就像操作table一樣直接。
-
C++中除了自定義對(duì)象,STL是用的最多的了,C++希望lua中能夠接收STL的參數(shù),或者能夠返回STL數(shù)據(jù)結(jié)構(gòu)
-
Lua中只有table數(shù)據(jù)結(jié)構(gòu),Lua希望C++的參數(shù)的數(shù)據(jù)結(jié)構(gòu)支持table,并且lua可以直接把table作為返回值。
-
C++的指針需要傳遞到lua中,同時(shí)也希望某些操作,lua可以把C++對(duì)象指針作為返回值
以上前兩者已經(jīng)介紹了,而后三者FFLUA也是給予 完美支持。通過范型的C++封裝,可以將C++ STL完美的轉(zhuǎn)換成luatable,同時(shí)在lua返回table的時(shí)候,自動(dòng)根據(jù)返回值類型將lua的table轉(zhuǎn)換成C++ STL。FFLUA中只要被注冊(cè)過的C++對(duì)象,都可以把其指針作為參數(shù)賦值給lua,甚至在lua中保存。當(dāng)我講述以上特性的時(shí)候,都是在保證類型安全的前提下。重要的類型檢查有:
-
STL轉(zhuǎn)成Luatable時(shí),STL中的類型必須是lua支持的,包括基本類型和已經(jīng)注冊(cè)過的C++對(duì)象指針。并且STL可以嵌套使用,如vector<list<int> >, 不要驚訝,這是支持的,不管嵌套多少層,都是支持的,使用C++模板的遞歸機(jī)制,該特性得到了完美支持。vector、list、set都會(huì)轉(zhuǎn)換成table的數(shù)組模式,key從1開始累加。而map類型自動(dòng)適配為table字典。
-
LUA中的table可以被當(dāng)成返回值轉(zhuǎn)換成C++ STL,轉(zhuǎn)換跟上邊剛好是對(duì)應(yīng)的,當(dāng)然有一個(gè)限制,由于C++的STL類型必須是唯一的,如vector<int>的返回值就要求lua中的table所有值都是int。否則FFLUA會(huì)返回出錯(cuò),并提示類型轉(zhuǎn)換失敗
-
無論死調(diào)用lua中使用C++對(duì)象指針,還是LuA中返回C++對(duì)象指針,該對(duì)象必須是lua可以識(shí)別的,即已經(jīng)被注冊(cè)的,否則FFLUA會(huì)提示轉(zhuǎn)換類型失敗。
關(guān)于重載:
關(guān)于重載LUA 可以使用lua中內(nèi)部自己的reload,也可以將fflua對(duì)象銷毀后,重先創(chuàng)建一個(gè),創(chuàng)建fflua對(duì)象的開銷和創(chuàng)建lua虛擬機(jī)的開銷一直,不會(huì)有附加開銷。
總結(jié):
-
FFLUA是簡(jiǎn)化C++嵌入綁定lua腳本的類庫
-
FFLUA只有三個(gè)頭文件,不依賴除lua之外的任何的類庫,開發(fā)者可以非常容易的使用FFLUA
-
FFLUA 對(duì)于常用的STL數(shù)據(jù)結(jié)構(gòu)進(jìn)行了支持
-
FFLUA 即使擁有了這么多特性,仍然保持了輕量,只要用過C++,只要用過lua,F(xiàn)FLUA的代碼就可以非常清晰的看清其實(shí)現(xiàn),當(dāng)你了解其內(nèi)部實(shí)現(xiàn)時(shí),你會(huì)發(fā)現(xiàn)FFLUA已經(jīng)做到了極簡(jiǎn),范型模板展開后的代碼就跟你自己原生LUA CAPI 編寫的一樣直接。
完整的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:%d\n", __FUNCTION__, v);
}
int v;
};
class foo_t: public base_t
{
public:
foo_t(int b):a(b)
{
printf("in %s b:%d this=%p\n", __FUNCTION__, b, this);
}
~foo_t()
{
printf("in %s\n", __FUNCTION__);
}
void print(int64_t a, base_t* p) const
{
printf("in foo_t::print a:%ld p:%p\n", (long)a, p);
}
static void dumy()
{
printf("in %s\n", __FUNCTION__);
}
int a;
};
//! lua talbe 可以自動(dòng)轉(zhuǎn)換為stl 對(duì)象
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__);
}
class clazz{
public:
static void static_func(){
printf("in clazz::%s end ------------\n", __FUNCTION__);
}
};
static void lua_reg(lua_State* ls)
{
//! 注冊(cè)基類函數(shù), ctor() 為構(gòu)造函數(shù)的類型
fflua_register_t<base_t, ctor()>(ls, "base_t") //! 注冊(cè)構(gòu)造函數(shù)
.def(&base_t::dump, "dump") //! 注冊(cè)基類的函數(shù)
.def(&base_t::v, "v"); //! 注冊(cè)基類的屬性
//! 注冊(cè)子類,ctor(int) 為構(gòu)造函數(shù), foo_t為類型名稱, base_t為繼承的基類名稱
fflua_register_t<foo_t, ctor(int)>(ls, "foo_t", "base_t")
.def(&foo_t::print, "print") //! 子類的函數(shù)
.def(&foo_t::a, "a"); //! 子類的字段
fflua_register_t<>(ls)
.def(&dumy, "dumy"); //! 注冊(cè)靜態(tài)函數(shù)
fflua_register_t<clazz, ctor()>(ls, "clazz")
.def(&clazz::static_func, "static_func");
}
int main(int argc, char* argv[])
{
fflua_t fflua;
try
{
//! 注冊(cè)C++ 對(duì)象到lua中
fflua.reg(lua_reg);
//! 載入lua文件
fflua.add_package_path("./");
#ifdef _WIN32
fflua.load_file("../test.lua");
#else
fflua.load_file("test.lua");
#endif
//! 獲取全局變量
int var = 0;
assert(0 == fflua.get_global_variable("test_var", var));
//! 設(shè)置全局變量
assert(0 == fflua.set_global_variable("test_var", ++var));
//! 執(zhí)行l(wèi)ua 語句
fflua.run_string("print(\"exe run_string!!\")");
//! 調(diào)用lua函數(shù), 基本類型作為參數(shù)
int32_t arg1 = 1;
float arg2 = 2;
double arg3 = 3;
string arg4 = "4";
fflua.call<bool>("test_func", arg1, arg2, arg3, arg4);
//! 調(diào)用lua函數(shù),stl類型作為參數(shù), 自動(dòng)轉(zhuǎn)換為lua talbe
vector<int> vec; vec.push_back(100);
list<float> lt; lt.push_back((float)99.99);
set<string> st; st.insert("OhNIce");
map<string, int> mp; mp["key"] = 200;
fflua.call<string>("test_stl", vec, lt, st, mp);
//! 調(diào)用lua 函數(shù)返回 talbe,自動(dòng)轉(zhuǎn)換為stl結(jié)構(gòu)
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");
//! 調(diào)用lua函數(shù),c++ 對(duì)象作為參數(shù), foo_t 必須被注冊(cè)過
foo_t* foo_ptr = new foo_t(456);
fflua.call<void>("test_object", foo_ptr);
//! 調(diào)用lua函數(shù),c++ 對(duì)象作為返回值, foo_t 必須被注冊(cè)過
assert(foo_ptr == fflua.call<foo_t*>("test_ret_object", foo_ptr));
//! 調(diào)用lua函數(shù),c++ 對(duì)象作為返回值, 自動(dòng)轉(zhuǎn)換為基類
base_t* base_ptr = fflua.call<base_t*>("test_ret_base_object", foo_ptr);
assert(base_ptr == foo_ptr);
}
catch (exception& e)
{
printf("exception:%s\n", e.what());
}
#ifdef _WIN32
system("pause");
#endif
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
-- 測(cè)試調(diào)用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參數(shù)
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 參數(shù)
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è)試接受C++對(duì)象
function test_object(foo_obj)
--測(cè)試構(gòu)造
base = base_t:new()
-- 每個(gè)對(duì)象都有一個(gè)get_pointer獲取指針
print("base ptr:", base:get_pointer())
-- 測(cè)試C++對(duì)象函數(shù)
foo_obj:print(12333, base)
base:delete()
--基類的函數(shù)
foo_obj:dump()
-- 測(cè)試C++ 對(duì)象屬性
print("foo property", foo_obj.a)
print("base property", foo_obj.v)
end
-- 測(cè)試返回C++對(duì)象
function test_ret_object(foo_obj)
return foo_obj
end
-- 測(cè)試返回C++對(duì)象
function test_ret_base_object(foo_obj)
return foo_obj
end
clazz:static_func()