Lua的C++綁定庫(二)
下面的這個例子是用於展示,對於一個沒有向LuaBus註冊的類(本例中為類GCTest2),綁定庫依然會在對象生命周期結束後,調用其析構函數。其實這算不上一個特性,只是luatinker未能正確處理這種情況,我需要證明LuaBus是不存在這種錯誤的。如果一個lua的綁定庫沒有處理這種情況,它將可能會導致嚴重的資源泄漏問題。
1 #include "lua/LuaFunc.h" 2 3 struct GCTest1 { 4 GCTest1() { printf("GCTest1()\n"); } 5 GCTest1(const GCTest1&) { printf("GCTest(const GCTest1&)\n"); } 6 ~GCTest1() { printf("~GCTest1()\n"); } 7 }; 8 GCTest1 getGCTest1() { return GCTest1(); } 9 10 struct GCTest2 { 11 GCTest2() { printf("GCTest2()\n"); } 12 GCTest2(const GCTest2&) { printf("GCTest(const GCTest2&)\n"); } 13 ~GCTest2() { printf("~GCTest2()\n"); } 14 }; 15 GCTest2 getGCTest2() { return GCTest2(); } 16 17 const char *lua_str = R"( 18 a1 = getGCTest1() 19 print(a1) 20 a1 = nil 21 a2 = getGCTest2() 22 print(a2) 23 a2 = nil 24 collectgarbage() 25 )"; 26 27 int main(int argc, char **argv)28 { 29 lua_State *L = lua::open(); 30 31 lua::class_add<GCTest1>(L, "GCTest1"); 32 33 lua::def(L, "getGCTest1", getGCTest1); 34 lua::def(L, "getGCTest2", getGCTest2); 35 lua::dostring(L, lua_str); 36 37 lua::close(L); 38 return 0; 39 }
程序輸出:
GCTest1() GCTest(const GCTest1&) GCTest(const GCTest1&) GCTest(const GCTest1&) ~GCTest1() ~GCTest1() ~GCTest1() GCTest1: 00000000004BF218 GCTest2() GCTest(const GCTest2&) GCTest(const GCTest2&) GCTest(const GCTest2&) ~GCTest2() ~GCTest2() ~GCTest2() (_GC_META_): 00000000004BF318 ~GCTest2() ~GCTest1()
下面的例子用於演示c++的多繼承,因為效率的原因,LuaBus並不直接支持多繼承,但是提供了一種方式讓腳本能夠訪問多繼承的父類。
1 #include "lua/LuaFunc.h" 2 3 struct BaseA { 4 void testA() { printf("testA()\n"); } 5 }; 6 struct BaseB { 7 void testB() { printf("testB()\n"); } 8 }; 9 struct SubAB : public BaseA, public BaseB { 10 void testAB() { printf("testAB()\n"); } 11 }; 12 13 const char *lua_str = R"( 14 ab = SubAB() 15 ab:testAB() --調用本對象的方法 16 ab:testA() -- 調用父類的方法 17 b = ab:castToBaseB() --獲取本對象的其它父類 18 b:testB() --調用其它父類的方法 19 )"; 20 21 int main(int argc, char **argv) 22 { 23 lua_State *L = lua::open(); 24 25 lua::class_add<BaseA>(L, "BaseA"); 26 lua::class_def<BaseA>(L, "testA", &BaseA::testA); 27 lua::class_add<BaseB>(L, "BaseB"); 28 lua::class_def<BaseB>(L, "testB", &BaseB::testB); 29 lua::class_add<SubAB>(L, "SubAB"); 30 lua::class_con<SubAB>(L, lua::constructor<SubAB>); 31 lua::class_inh<SubAB, BaseA>(L); //向腳本註冊父類 32 lua::class_cast<SubAB, BaseB>(L); //向腳本註冊其它父類 33 lua::class_def<SubAB>(L, "testAB", &SubAB::testAB); 34 lua::dostring(L, lua_str); 35 36 lua::close(L); 37 return 0; 38 }
程序輸出:
testAB() testA() testB()
對於上面的例子,LuaBus要求向腳本註冊的父類必須為子類的第一個父類,否則腳本調用回到c++層後,父類的指針將會是錯誤的。LuaBus提供了一種方式可以繞開這個限制,讓需要被註冊為父類的類繼承於lua::tracker。這個特性可能在重構祖傳代碼時會用到,如果全新設計的代碼也需要這個特性,請讓我表示無語了。
1 #include "lua/LuaFunc.h" 2 3 struct BaseA { 4 void testA() { printf("testA(%p)\n", this); } 5 }; 6 struct BaseB : public lua::tracer { //這個類可以在腳本中被註冊為SubAB的父類,而且也只能是這個類被註冊為SubAB的父類。 7 void testB() { printf("testB(%p)\n", this); } 8 }; 9 struct SubAB : public BaseA, public BaseB { 10 void testAB() { printf("testAB(%p)\n", this); } 11 }; 12 13 const char *lua_str = R"( 14 ab:testB() --驗證c++層能夠獲得正確的this指針 15 )"; 16 17 int main(int argc, char **argv) 18 { 19 lua_State *L = lua::open(); 20 21 auto ab = SubAB(); 22 ab.testAB(); 23 ab.testA(); 24 ab.testB(); 25 26 lua::class_add<BaseA>(L, "BaseA"); 27 lua::class_def<BaseA>(L, "testA", &BaseA::testA); 28 lua::class_add<BaseB>(L, "BaseB"); 29 lua::class_def<BaseB>(L, "testB", &BaseB::testB); 30 lua::class_add<SubAB>(L, "SubAB"); 31 lua::class_inh<SubAB, BaseB>(L); 32 lua::class_def<SubAB>(L, "testAB", &SubAB::testAB); 33 lua::set(L, "ab", &ab); 34 lua::dostring(L, lua_str); 35 36 lua::close(L); 37 return 0; 38 }
程序輸出:
testAB(00000000002CF7F4) testA(00000000002CF7F4) testB(00000000002CF7F5) testB(00000000002CF7F5)
LuaBus的核心特性,處理野指針問題。讓需要被註冊的類繼承於lua::binder即可,這樣我們就可以在腳本層用is_object_alive()接口來判斷當前指針是否有效,即便是在腳本層直接操縱了野指針,錯誤也只會發生在腳本層,而不會拋出到C++層,繼而導致程序崩潰。
#include "lua/LuaBus.h" struct PtrTest : lua::binder { void test() { printf("test(%p)\n", this); } }; const char *lua_str = R"( print(pt1:is_object_alive()) --檢查指針的有效性 print(pt1, pt2, pt1 == pt2) --指針的相等性測試 pt1:test() --驗證對象的可用性,如果對象是野指針,腳本層就會拋出錯誤,但是綁定庫會捕獲它。 print(‘ok‘) --如果執行了這行代碼,就表明腳本層沒有發生任何錯誤。 )"; int main(int argc, char **argv) { lua_State *L = lua::open(); lua::class_add<PtrTest>(L, "PtrTest"); lua::class_def<PtrTest>(L, "test", &PtrTest::test); auto pt = new PtrTest(); lua::set(L, "pt1", pt); lua::set(L, "pt2", pt); lua::dostring(L, lua_str); // 這次是正確的,所以一切正常。 delete pt; // 這會讓腳本層產生野指針。 lua::dostring(L, lua_str); // 這次腳本層會因為野指針問題發生錯誤。 printf("all ok.\n"); // 驗證腳本層野指針錯誤不會導致程序崩潰。 lua::close(L); return 0; }
程序輸出:
true PtrTest: 00000000006BEEE8 PtrTest: 00000000006BEEE8 true test(00000000006B8150) ok false PtrTest: 00000000006BEEE8 PtrTest: 00000000006BEEE8 true [string "dobuffer()"]:4: userdata[00000000006BEEE8] is a nil value. <call stack> -> unknown : line -1 [[C] : line -1] test() : line -1 [[C] : line -1] unknown : line 4 [[string "dobuffer()"] : line 0] all ok.
當某個類繼承於lua::binder後,這也給我們帶來了另外一個收益,那就是我們能夠像寫C++代碼一樣直接用==運算符來判斷兩個對象是否相等(前提是在沒有重載==運算符的情況下),而且速度非常快。從底層來講,還有另外一個收益,那就是如果我們多次向腳本層傳入同一個對象,LuaBus不會多次重復創建userdata,它會重用第一次創建的userdata,由此帶來的結果自然是傳遞參數的速度更快了,內存占用也會更少。
最後,LuaBus還附帶了一些工具類,它們讓Lua與C++之間的交互盡量無縫化。小例子看不出來有啥用處,不過對於大型項目來說,這些功能幾乎是必備的。如果沒有這些工具類的加持,做項目時,可能就相當於不用lua綁定庫,直接基於lua的c接口擼代碼吧。
下面的例子簡單介紹了這個工具類的作用與用法,更多功能請自行查看相關類的接口。
#include "lua/LuaFunc.h" struct Test { Test() : a(0) {} Test(int a) : a(a) {} void test() { printf("a = %d\n", a); } int a; }; LuaRef tmp; void SaveTmp(LuaRef f) { tmp = std::move(f); } const char *lua_str = R"( v1, v2 = Test(), Test_int(3) t = {} t.print1 = function(arg) print(‘print1‘, t, arg) v1:test() end t.print2 = function(arg) print(‘print2‘, t, arg) v2:test() end f = function() print(‘all ok.‘) end r = function() print(‘ref ok.‘) end SaveTmp(r) -- 將變量保存到C++層 )"; int main(int argc, char **argv) { lua_State *L = lua::open(); // 給類註冊多個構造函數 lua::class_add<Test>(L, "Test"); lua::class_con<Test>(L, lua::constructor<Test>); // 1 lua::class_con(L, "Test_int", lua::constructor<Test, int>); // 2 lua::class_def<Test>(L, "test", &Test::test); lua::def(L, "SaveTmp", SaveTmp); lua::dostring(L, lua_str); // 給變量‘f‘創建一個引用,引用不會受到lua堆棧影響; // 我們後續會對變量‘f‘進行操作,但是它也不會影響到本引用的值。 LuaRef r(L, "f"); // 調用lua table的函數 LuaFuncs tfs(L, "t"); tfs.CallMethod<void>("print1"); // 會將本對象作為第一個參數傳入,類似於C++調用成員函數。 tfs.CallStaticMethod<void>("print2"); // 不會將本對象作為參數傳入,類似於C++調用靜態成員函數。 // 調用lua的全局函數 LuaFunc f(L, "f"); f.Call<void>(); // 將變量‘f‘置空,變量‘f‘不再有效。 lua::set(L, "f", nullptr); // 驗證變量‘f‘的狀態 LuaFunc f1(L, "f"); f1.Call<void>(); // 驗證引用的作用。 r.Get<LuaFunc>().Call<void>(); // 在實際應用中,我們可以把lua的臨時變量長久保存在C++層,被保存的變量不會被lua回收。 // 這可能也是LuaRef最多的用法。 tmp.Get<LuaFunc>().Call<void>(); lua::close(L); return 0; }
程序輸出:
print1 table: 00000000005BF100 table: 00000000005BF100 a = 0 print2 table: 00000000005BF100 nil a = 3 all ok. lua attempt to call global ‘f‘ (not a function) all ok. ref ok.
本例子運行完後有報錯,請無視,畢竟這只是一個功能驗證單元,我覺得它已經很好的完成了它的使命( ╯□╰ )
Lua的C++綁定庫(二)