1. 程式人生 > >Lua的C++綁定庫(二)

Lua的C++綁定庫(二)

簡單 span 輸出 func amp 運行 重構 rda 調用父類

下面的這個例子是用於展示,對於一個沒有向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++綁定庫(二)