Lua的C++綁定庫(一)


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是沒有這些功能的,敬請期待。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM