C++是一門非常復雜的語言,然而更可怕的是,這門語言的各種奇葩特性還在繼續增加,很多C++的程序員都會覺得這完全是在給自己添堵嘛,都已經這么復雜了,何必還要使勁往里面塞東西呢,明明C++03或者說是C++98的標准就已經完全夠用了。我個人的看法呢,其實后續的標准還是很有必要的,因為這里面的很多新標准,對於一些寫底層庫的作者來說,真的是非常需要,換句話說,如果沒有type_traits、
右值語義、
可變參數模板這些特性,那我就不會重新造這個輪子,也就不會寫這篇文章了,正是因為C++11的標准里面引入了這些特性,以往一些庫的實現就顯得很笨重、很復雜,所以我決定重新造一個代碼輕量級但是功能卻不會有刪減的輪子。最后,友情提示那些反感C++新標准的小伙伴一下,其實這些新標准里面大部分的內容我們根本就不必關心,因為對於寫邏輯層的功能來說,這些功能我們根本就用不上,能用上的也就一些新增的語法糖,例如auto、foreach、lambda,其它的功能有個毛用啊,不過呢,這些語法糖還是建議了解一下,可以省很多事哦。
我個人最早接觸的lua綁定庫是ELuna,它是由我的一個同事引入項目中的,並且以此為基礎進行了大量魔改操作,然后當我接手這個代碼的時候,可讀性有點傷人。我后來又在網上搜了一下,感覺luatinker這個庫評價挺好的,於是我就想在項目里引入luatinker。可惜的是,luatinker是基於lua5.1寫的,而當時lua5.3已經出來了,大家都知道,lua5.3是原生支持64位整數的,而在這之前,所有的lua綁定庫為了支持64位整數,都得用一些奇葩手段才能簡單支持,最重要的是,我有把64位整數傳入腳本的需求,所以只好先做移植了,好不容易移植過去了,寫了幾個例子一跑,慘了,有bug啊。好吧,那就繼續改吧,等這些都做完了,靜下來想了想,好像對於這個luatinker自己已經完全掌握了,而且這個庫里面還有好多代碼是c++11已經原生支持了的,干脆我自己用c++11實現一份吧,這樣子代碼肯定簡潔的多,代碼寫得越少,bug也就寫得越少嘛。所以總結就一句話,以下這個庫的api設計跟luatinker幾乎完全一致,只要你用過luatinker,那么用這個庫,基本上是0成本,除非是luatinker缺失的功能。
好了,開始進入正題。我魔改過的luatinker:https://github.com/jallyx/LuaTinker,新輪子:https://gitee.com/jallyx/fusion/tree/master/src/lua。友情提示,std::string_view是c++17引入的,所以c++11編譯這個代碼需要先自己處理一下,方案1、刪掉這個功能,具體做法就是哪兒編譯出錯就刪哪兒,方案2、使用boost提供的boost::string_view或boost::string_ref,然后這樣包裝一下https://gitee.com/jallyx/fusion/blob/master/feature/string_view,方案3、使用c++17吧。
讓我們先從luatinker的例子走起。
1 #include "lua/LuaFunc.h" 2 3 int cpp_func(int arg1, int arg2) { 4 return arg1 + arg2; 5 } 6 7 const char *lua_str = R"( 8 print('cpp_func(1,2) = ' .. cpp_func(1, 2)) --調用c++全局函數 9 function lua_func(arg1, arg2) --定義lua全局函數 10 return arg1 + arg2 11 end 12 )"; 13 14 int main(int argc, char **argv) 15 { 16 lua_State *L = lua::open(); // 新建lua虛擬機,並做一些必要的初始化工作。 17 lua::def(L, "cpp_func", cpp_func); // 向lua注冊c++全局函數 18 lua::dostring(L, lua_str); // 執行lua腳本,如果是文件則需要調用lua::dofile。 19 printf("lua_func(3,4) = %d\n", LuaFunc(L, "lua_func").Call<int>(3, 4)); // 調用lua的全局函數 20 lua::close(L); // 關閉lua虛擬機,釋放資源 21 return 0; 22 }
程序輸出:
cpp_func(1,2) = 3 lua_func(3,4) = 7
似乎太簡單了,我都不知道需要解釋點啥,接下來進入第二個例子。
1 #include "lua/LuaBus.h" 2 3 static int cpp_int = 100; 4 5 const char *lua_str = R"( 6 print('cpp_int = ' .. cpp_int) -- 打印c++的全局變量 7 lua_int = 200 -- 定義lua的全局變量 8 )"; 9 10 int main(int argc, char **argv) 11 { 12 lua_State *L = lua::open(); // 新建lua虛擬機,並做一些必要的初始化工作。 13 lua::set(L, "cpp_int", cpp_int); // 向lua注冊c++全局變量 14 lua::dostring(L, lua_str); // 執行lua腳本,如果是文件則需要調用lua::dofile。 15 printf("lua_int = %d\n", lua::get<int>(L, "lua_int")); // 獲取lua的全局變量 16 lua::close(L); // 關閉lua虛擬機,釋放資源 17 return 0; 18 }
程序輸出:
cpp_int = 100 lua_int = 200
第三個例子,稍微有點復雜,主要是對c++類成員函數、成員變量、類繼承的支持。在最后的lua代碼中,對用到的c++類變量進行了一個內省輸出的操作,如果大家對自己的定位是這個庫的使用者,則完全沒必要關心。PS:我寫這段代碼也只是因為luatinker的測試代碼是這樣的而已。
1 #include "lua/LuaBus.h" 2 3 struct A { 4 A(int v) : value(v) {} 5 int value; 6 }; 7 8 struct base { 9 base() {} 10 const char* is_base() { return "this is base"; } 11 }; 12 13 class test : public base { 14 public: 15 test(int val) : _test(val) {} 16 ~test() {} 17 const char* is_test() { return "this is test"; } 18 void ret_void() {} 19 int ret_int() { return _test; } 20 int ret_mul(int m) const { return _test * m; } 21 A get() { return A(_test); } 22 void set(A a) { _test = a.value; } 23 int _test; 24 }; 25 26 test g_test(11); 27 28 const char *lua_str = R""( 29 print(g_test._test) --直接訪問c++類對象的成員變量 30 print(g_test:is_test()) --調用c++類對象的成員函數 31 print(g_test:ret_int()) --調用int返回值的成員函數 32 temp = test(4) --直接在腳本中創建c++類對象 33 print(temp._test) --打印由腳本創建的c++類對象變量的成員變量 34 a = g_test:get() --類成員函數返回一個c++類對象 35 temp:set(a) --類成員函數的參數為一個c++類對象 36 print(temp._test) --打印以上調用結果 37 print(temp:is_base()) --調用基類的成員函數 38 print(temp:is_test()) --調用自己的成員函數 39 ------------------------------------------------------------------------------- 40 function objinfo(obj) 41 local meta = getmetatable(obj) 42 if meta ~= nil then 43 metainfo(meta) 44 else 45 print("no object infomation !!") 46 end 47 end 48 function metainfo(meta) 49 if meta ~= nil then 50 local name = meta["__name"] 51 if name ~= nil then 52 metainfo(meta["__parent"]) 53 print("<"..name..">") 54 for key,value in pairs(meta) do 55 if not string.find(key, "__..") then 56 if type(value) == "function" then 57 print("\t[f] "..name..":"..key.."()") 58 elseif type(value) == "userdata" then 59 print("\t[v] "..name..":"..key) 60 end 61 end 62 end 63 end 64 end 65 end 66 ------------------------------------------------------------------------------ - 67 print("g_test -> ", g_test) 68 print("temp -> ", temp) 69 print("a -> ", a) 70 print("objinfo(g_test)") 71 objinfo(g_test) 72 print("objinfo(temp)") 73 objinfo(temp) 74 print("objinfo(a)") 75 objinfo(a) 76 )""; 77 78 int main(int argc, char **argv) 79 { 80 lua_State *L = lua::open(); // 新建lua虛擬機,並做一些必要的初始化工作。 81 lua::class_add<base>(L, "base"); // 注冊base類 82 lua::class_def<base>(L, "is_base", &base::is_base); // 注冊is_base成員函數 83 lua::class_add<test>(L, "test"); // 注冊test類 84 lua::class_inh<test, base>(L); // 定義c++類之間的繼承關系 85 lua::class_con<test>(L, lua::constructor<test, int>); // 注冊test構造函數 86 lua::class_def<test>(L, "is_test", &test::is_test); // 注冊is_test成員函數 87 lua::class_def<test>(L, "ret_void", &test::ret_void); // 無返回值的成員函數 88 lua::class_def<test>(L, "ret_int", &test::ret_int); // int返回值的成員函數 89 lua::class_def<test>(L, "ret_mul", &test::ret_mul); // const修飾的成員函數 90 lua::class_def<test>(L, "get", &test::get); // 類對象返回值的成員函數 91 lua::class_def<test>(L, "set", &test::set); // 參數為類對象的成員函數 92 lua::class_mem<test>(L, "_test", &test::_test); // 注冊_test成員變量 93 lua::set(L, "g_test", &g_test); // 向lua注冊c++類對象 94 lua::dostring(L, lua_str); // 執行lua腳本,如果是文件則需要調用lua::dofile。 95 lua::close(L); // 關閉lua虛擬機,釋放資源 96 return 0; 97 }
程序輸出:
11 this is test 11 4 11 this is base this is test g_test -> test: 00000000004D9DC8 temp -> test: 00000000004D9F08 a -> (_GC_META_): 00000000004D9F88 objinfo(g_test) <test> [f] test:downcast() [f] test:ret_mul() [f] test:is_object_alive() [f] test:get() [v] test:_test [f] test:reinterpret() [f] test:ret_int() [f] test:set() [f] test:ret_void() [f] test:is_test() objinfo(temp) <test> [f] test:downcast() [f] test:ret_mul() [f] test:is_object_alive() [f] test:get() [v] test:_test [f] test:reinterpret() [f] test:ret_int() [f] test:set() [f] test:ret_void() [f] test:is_test() objinfo(a) <(_GC_META_)>
第四個例子,是對lua table的支持。
1 #include "lua/LuaFunc.h" 2 3 const char *lua_str = R"( 4 print(haha) --打印全局table 5 print(haha.value) --打印全局table的鍵值 6 print(haha.inside) --打印全局table的鍵值,這個鍵值是一個table. 7 print(haha.inside.value) --嵌套table鍵值測試 8 haha.test = "input from lua" --添加鍵值,c++會獲取它。 9 function print_table(arg) --函數參數為lua table的測試例子 10 print("arg = ", arg) 11 print("arg.name = ", arg.name) 12 end 13 function return_table(arg) --函數返回一個lua table的測試例子 14 local ret = {} 15 ret.name = arg 16 return ret 17 end 18 )"; 19 20 int main(int argc, char **argv) 21 { 22 lua_State *L = lua::open(); // 新建lua虛擬機,並做一些必要的初始化工作。 23 LuaTable haha(L, "haha"); // 關聯一個全局lua table,如果沒有就新建一個全局的lua table. 24 haha.set("value", 1); // 給lua table添加一個鍵值對 25 haha.set("inside", LuaTable(L)); // 給lua table添加一個鍵值對,值是一個臨時表 26 LuaTable inside = haha.get<LuaTable>("inside"); // 獲取上一步加進去的臨時表 27 inside.set("value", 2); // 修改鍵值,lua腳本里面會打印它。 28 lua::dostring(L, lua_str); // 執行lua腳本,如果是文件則需要調用lua::dofile。 29 const char* test = haha.get<const char*>("test"); // 獲取在腳本中加進去的鍵值 30 printf("haha.test = %s\n", test); // 不要問我這在干嘛! 31 LuaTable temp(L); // 在棧上新建一個lua table 32 temp.set("name", "local table !!"); // 添加一個鍵值對 33 LuaFunc(L, "print_table").Call<void, const LuaTable&>(temp); // 調用lua函數,參數為lua table. 34 LuaFunc func = LuaFunc(L, "return_table"); // 獲取一個lua的全局函數,並存放在棧上,返回變量是對這個值的一個引用,我們后面就可以多次重用這個函數對象了。 35 LuaTable ret = func.Call<LuaTable>("give me a table !!"); // 可以對這個函數對象執行調用方法。 36 printf("ret.name =\t%s\n", ret.get<const char*>("name")); // 打印上一步返回的lua table的一個鍵值 37 ret = LuaFunc(L, "return_table").Call<LuaTable>("give me a table !!"); // 這兩行代碼執行下來絕對不會出現你期望的輸出結果,要想理解其具體原理可能需要多了解一點lua與c交互的知識才行,當然與此相關的c++特性你也需要十分了解。不過嘛,你也可以選擇記住一定不能這樣寫,或者代碼出錯了回過頭來看看,是不是有代碼這樣寫了。 38 printf("ret.name =\t%s\n", ret.get<const char*>("name")); 39 lua::close(L); // 關閉lua虛擬機,釋放資源 40 return 0; 41 }
程序輸出:
table: 00000000002EDD80 1 table: 00000000002EDDC0 2 haha.test = input from lua arg = table: 0000000000321F70 arg.name = local table !! ret.name = give me a table !! ret.name = (null)
第五個例子,其實這個例子的用法,在前面幾個例子中都有涉及,我不清楚為啥luatinker還要單獨設計這個例子,由於它的例子的注釋是韓語的,我也只好表示看不懂了。或許是用來測試調用一個不存在的函數吧?
1 #include "lua/LuaFunc.h" 2 3 void show_error(const char* error) { 4 printf("_ALERT -> %s\n", error); 5 } 6 7 const char *lua_str = R"( 8 function test_error() 9 print("test_error() called !!") 10 test_error_1() 11 end 12 function test_error_1() 13 print("test_error_1() called !!") 14 test_error_2() 15 end 16 function test_error_2() 17 print("test_error_2() called !!") 18 test_error_3() 19 end 20 )"; 21 22 int main(int argc, char **argv) 23 { 24 lua_State *L = lua::open(); // 新建lua虛擬機,並做一些必要的初始化工作。 25 printf("%s\n", "-------------------------- current stack"); 26 lua::enum_stack(L); 27 lua_pushnumber(L, 1); 28 printf("%s\n", "-------------------------- stack after push '1'"); 29 lua::enum_stack(L); 30 lua::dostring(L, lua_str); // 執行lua腳本,如果是文件則需要調用lua::dofile。 31 printf("%s\n", "-------------------------- calling test_error()"); 32 LuaFunc(L, "test_error").Call<void>(); 33 printf("%s\n", "-------------------------- calling test_error_3()"); 34 LuaFunc(L, "test_error_3").Call<void>(); 35 lua::def(L, "_ALERT", show_error); 36 LuaFunc(L, "_ALERT").Call<void>("test !!!"); 37 printf("%s\n", "-------------------------- calling test_error()"); 38 LuaFunc(L, "test_error").Call<void>(); 39 lua::close(L); // 關閉lua虛擬機,釋放資源 40 return 0; 41 }
程序輸出:
-------------------------- current stack stack: 0 -------------------------- stack after push '1' stack: 1 number 1 -------------------------- calling test_error() test_error() called !! test_error_1() called !! test_error_2() called !! [string "dobuffer()"]:12: attempt to call a nil value (global 'test_error_3') <call stack> -> test_error_3() : line -1 [[C] : line -1] test_error_2() : line 12 [[string "dobuffer()"] : line 10] test_error_1() : line 8 [[string "dobuffer()"] : line 6] unknown : line 4 [[string "dobuffer()"] : line 2] -------------------------- calling test_error_3() lua attempt to call global 'test_error_3' (not a function) _ALERT -> test !!! -------------------------- calling test_error() test_error() called !! test_error_1() called !! test_error_2() called !! [string "dobuffer()"]:12: attempt to call a nil value (global 'test_error_3') <call stack> -> test_error_3() : line -1 [[C] : line -1] test_error_2() : line 12 [[string "dobuffer()"] : line 10] test_error_1() : line 8 [[string "dobuffer()"] : line 6] unknown : line 4 [[string "dobuffer()"] : line 2]
由於例子比較簡單,我也就沒有添加啥注釋了。唯一需要注意的就是,當調用一個不存在的函數時(更准確的說,應該是當lua拋出異常時),底層綁定庫會自動打印當前的調用堆棧,以幫助調用者定位問題所在。
第六個例子是關於協程的,然而luatinker對協程並沒有提供任何的支持,當然了,我也沒有對協程提供支持,所以這個例子就不貼出來了。
以上這些例子所展示的功能,應該算是一個lua的c++綁定庫最基礎而且必須存在的功能,它們的作用有這么幾個,1、驗證綁定庫在功能上的正確性,2、用於幫助玩家快速入門,3、展示綁定庫給玩家帶來的便利性。關於這最后的一點嘛,大家可以在網上搜一下最原始版的lua與c交互的相關文章,看看c++給我們帶來了多少便利。例如百度最靠前的這篇文章:https://www.cnblogs.com/sevenyuan/p/4511808.html,我們可以看到,lua與c的交互是非常麻煩的,綁定庫只需要一行代碼就能完成,並且還簡潔易懂的代碼,用原始的交互方式,那是需要很多行代碼的,而且大家還得時時關心當前的堆棧狀態,不知道大家有沒有寫匯編的感覺?c++給我們帶來了很多的復雜性,然而也給我們帶來了很多的方便之處,c的綁定庫也有很多,然而跟c++相比,那是一個能打的都沒有。
以上有一個例子的寫法存在着一個bug,如果大家想要深入研究一下,不妨參考我上面提到的那篇文章,那絕對算得上一篇精品文章。
在接下來的系列文章里,我會開始介紹fusion/lua這個綁定庫的一些高級功能,不說這些功能都是特有的,但是至少我所接觸到的eluna、luatinker是沒有這些功能的,敬請期待。