歡迎轉載,請保留出處:http://www.cnblogs.com/wellbye/
二、跨語言交互的實質
跨語言交互,也就是多語言混合編程,其實也是理解lua與c++交互的一個關鍵。
首先,是理解為什么要多語言混合使用,只用c++不行嗎?答案是因為腳本語言語法糖多使用方便、沙盒式安全機制使系統更穩定、整體概念簡單易學降低開發成本,等等……那么,只用腳本不行嗎?那也是不行的,因為與系統api的接口、計算密集性模塊的性能要求等是腳本語言不擅長的,這一部份仍然需要c/c++來完成。因此,為了綜合各自的優勢,就出現了混合編程的需要。
其次,是理解混編程序的大致運行流程,即在一個程序的生命周期里,哪些部份是c++寫的,哪些部份是lua寫的?哪里是交互接口的地方?以一個事件驅動型程序來說,程序啟動、創建窗口及渲染器、事件循環分發等,都是在c++里做的,各種窗口消息、網絡事件的接收分發也是c++做的,但是消息和事件的處理器有不少是在lua里的寫的,這些腳本處理器會根據傳入的參數及當前狀態做出反應,包括改變腳本自身環境內的變量(這就影響了所謂的“當前狀態”)、調用綁定的c函數從而修改了c/c++端的變量(從而影響了c++端的邏輯行為)。c++部分和lua部份的代碼就這樣交替往復的運行着,而彼此調用對方、傳遞信息的時候,也就是所謂的交互接口。
再次,是理解跨語言交互的大體技術。在一個語言里,怎么使用另一個語言的變量?調用另一個語言里的函數?這涉及到腳本語言的實現機制,一般來說都會有一個編譯模塊、一個虛擬機(執行)模塊、一套類型實現及數據管理模塊,通常還會有一個供外部操作的接口,如lua c api,這個接口讓嵌入方得以操作腳本狀態(如訪問變量、調用函數、管理內存),所以在c/c++程序里,會通過這些c api將一些重要函數導出到腳本里供其調用,並在適當的時候觸發調用腳本函數,交互也就由此而生了。
最后,總結一下,所有程序的本質功能都是處理數據,所有程序最終都是以機器碼的形式被硬件CPU執行,從這兩個角度去看,不同語言的代碼並沒有本質區別(腳本代碼只是腳本虛擬機c模塊的‘數據’而已),大家都是在處理數據,只是在不同時機處理數據的不同的部份,而所謂交互,就是在處理共享數據。
三、c++對象模型
說了半天腳本,現在該說另一半,c++了。但是還得先說一下更基礎的c。相對於探討c++與lua的交互來說,c與lua的交互則少有提及,因為那個實在是太簡單了:c能夠導出給lua的只有函數和常量,而導出函數恰恰是lua c api的標准功能,只要提供一個形式如下的c函數:
typedef int (*lua_CFunction) (lua_State *L);
就可以將其注冊到腳本里供lua調用,調用的參數可用各種lua c api從L中提取。但是這有一個問題,lua c api支持的外部c函數只能是具有以上格式(返回值及參數類型定義,又稱函數簽名)的函數,但我們通常寫的(或者早就已經寫好的)c函數都不是這樣子,因為寫成這樣子就只能被lua調了,其它c代碼就無法使用——雖然有不少綁定框架就是這么做的——但這不是我們的目的,我希望核心業務模塊是腳本獨立的,即純粹按照c/c++使用的方式來編寫,而額外的可選的腳本模塊以一種侵入性最小的方式來將其粘合進其它語言。回到這個問題,由於我們正常的業務邏輯c函數,都不符合lua的要求,怎么辦呢,那就是為每一個目標函數做一個包裝函數,包裝函數的簽名符合lua_CFunction的格式,在每個包裝函數內,從L中提取相應的參數再去調用目標函數,再將返回值送進L。舉例來說,下面這個簡單的c函數:
int add(int x,int y) { return x+y; }
需要一個這樣的包裝函數:
int C_add(lua_State* L) { int x = lua_tonumber(L,1); int y = lua_tonumber(L,2); int ret = add(x,y); lua_pushnumber(L,ret); return 1; }
為每一個c函數手工寫這樣的包裝函數,實在太繁瑣了。我們需要一種數據驅動的方法,將所有目標函數的信息填表,然后只手工寫一個總控包裝函數,在這個函數里,根據腳本調用的函數名,找到真正的目標函數指針,並從腳本中提取它需要的參數,拿它們去調用這個函數指針。關於函數信息表的生成和函數指針的動態調用,將在后文中介紹,在此先說明這個概念,即:實際稍大規模的工程項目中,完全手寫包裝是不可能的事(低效、易出錯、缺乏變動一致性),需要借助函數指針這個有力工具,來完成高度概括、以不變應萬變的自動綁定機制。
現在回到c++與lua的交互。一到c++里問題就復雜了,因為多了一個類和對象的概念,大部份時候,我們使用的是對象指針,調用的是其成員函數。這就出現了我之前提到的在群上屢見不鮮的問題——“我把一個c++對象導到lua里,怎么調用它的函數啊?” -_-#!
這屬於還沒有認真思考過c++的對象模型,不知道c++對象和它的成員函數之間是什么關系。可能誤以為對象是個大籮筐,所有函數都裝在它身上了,只要把它導出到腳本,腳本就可以像c++一樣使用其所有功能。實際上c++對象只是個普通結構體,包含了所有數據成員,那函數存在哪呢?其實所有函數本身都存在代碼段,跟對象是毫無關聯的,因為對象作為數據,都是在數據段。但是在對象身上,會有一些指針字段,輾轉鏈接指向與對象相關的特殊函數。所謂的特殊函數,就是虛函數,因為在引用函數時都是通過函數名,而在子類重載過父類虛函數時,就會出現多個相同名字的函數,因此必須通過對象身上的特殊指針來索引,才能取到正確的函數。普通函數因為不可能有重名,所以不需要在對象上做特殊關聯,直接通過名字就可以引用到惟一版本。簡單概括一下:對象是一個存在於數據段上的結構體,其字段除成員變量外,還有一些隱含的特殊指針字段,指向其類型所對應的虛函數表,表里的每一項是一個函數指針,指向代碼段里的函數實現。值得注意的是,多重繼承時,就會有多個虛表指針,它們存在對象這個結構體的不同位置。舉例來說,一個簡單的繼承鏈:
class base1{ virtual void b1_func1(); virtual void b1_func2(); } class base2{ virtual void b2_func1(); virtual void b2_func2(); } class derived : b1,b2 { int a; }
其內存布局大致如下圖所示:
this--> ---------------
| vtable1_ptr |--> vtable1 [ b1_func1 ]-----------------------> b1_func1 impl code
base2-> --------------- [ b1_func2 ]-----------------------> b1_func2 impl code
| vtable2_ptr |-----------------------> vtable2[ b2_func1 ]---> b2_func1 impl code
--------------- [ b2_func2 ]---> b2_func2 impl code
| int a |
---------------
這里vtable是屬於每個類的,該類的對象都會有一個指向此類vtable的指針。當在一個對象指針上調用其虛函數時:
derived* obj = new derived(); obj->b2_func1();
derived* obj = malloc(sizeof(derived)); call_derived_ctor(obj); //c++ new operator的兩個步驟 offset = offsetof(base2,derived); //因為調用的b2_func1屬於base2,所以需找到base2與derived的偏移 base2* b2_obj = (base2*)(((char*)obj)+offset); //得到obj的base2部位 fp_b2func1 = (void**)(*b2_obj)[indexof(b2_func1)]; //通過虛表及虛函數索引得到真正的函數地址 fp_b2func1(b2_obj); //調用函數,將base2部位地址當作this傳入
注意第4行,根據目標函數期望的類型來調整對象指針,這是由c++編譯器自動完成的,但是在與lua的交互過程中,c++對象指針在一次來回傳遞后類型信息已喪失,這時需要我們自己補上該操作。
至此應該很清楚了,所謂注冊c++類到lua,包括兩部份:一是把類成員函數的指針包裝注冊到lua,在腳本中拿到以userdata表示的對象指針后,其自身並沒有“主動技能”,而是作為保存和傳遞的媒介,把它作為第一個參數,去調用包裝后的成員函數;二是這個userdata的創建與銷毀,對應着c++構造、析構函數的調用,它們的處理在本質上與一般成員函數並無差別,只是又多一些特殊部驟,如lua端對象表格的設置、userdata上掛gc函數等。
基本的概念都介紹完了,從下一章開始進入實踐階段,以我寫的那個腳本粘合庫為例,說明實現c++與lua交互的所有細節。