實現自己的LUA綁定器-一個模板編程挑戰
實現LUA綁定器
author : Kevin Lynx
Preface
當LUA腳本調用我們注冊的C函數時,我們需要逐個地從LUA棧里取出調用參數,當函數返回時,又需要一個一個地往LUA
棧壓入返回值,并且我們注冊的函數只能是int()(lua_State*)類型。這很不方便,對于上層程序員來說更不方便。
因此我們要做的是,實現一個綁定器,可以把任意prototype的函數綁定到LUA腳本當中,并且封裝取參數和壓返回值時
的諸多細節。
確實,世界上已經有很多庫做了這件事情。但是,我們這里的需求很簡單,我們只需要綁定函數,而不需要綁定C++類之
類的東西,自己實現的才是輕量級的。
What we usually do
先看下我們平時是怎么做這些事的。首先注冊函數很簡單:



然后是func的具體處理:












這是個簡單的例子,目的是展示我之前說的局限性,以及,恩,丑陋性。
How
要讓事情變得優美,我們就得隱藏丑陋。
首先,我們看看如何改進to_string的處理,使其看起來干凈。最直接也是最通用的做法是,我們自己做一個粘合層,
充當LUA與應用層之間的粘合劑。也就是說,LUA直接回調的不再直接是應用層的函數,而是我們實現的這一層中的函數,
我們的函數整理調用參數,然后回調到上層函數,上層返回后,我們收集上層的返回值,然后整理給LUA,最后返回。
這就是思路,具體實現時更為有趣。
Implementing...
直覺告訴我,我需要使用C++模板來實現。模板和宏都是個好東西,因為它們是泛性的,它們給程序員帶來自動性。
另一個直覺告訴我,盡量不要讓上層保存任何東西。通過模板的實例化,編譯器已經為我們添加了很多東西,我也不
想讓上層理會我太多。
因為,我們至少需要保存上層的函數指針(我們暫時只考慮C式的函數),我們至少還需要一個粘合層函數用以被LUA
直接回調,所以,我得到了以下類模板:
















這樣,泛化了Prototype后,lua_binder可以保存任意原型的函數指針。例如:


借助于模板技術,即使上層只是這樣一個簡單的看似不會產生任何代碼的typedef,實際上也會產生出一個static的
函數指針變量:_func。
這個時候,我們也該考慮下注冊函數部分了。注意,事實上我們總共需要干兩件事:封裝粘合層函數、封裝注冊函數
部分。同樣,我們得到一個最直觀的注冊函數模板:







為什么模板參數是binder_type而不是Prototype?(最直接的想法可能會想到Prototype)因為我們需要獲取func_type
以及最重要的:設置_func的值!綜合起來,lua_bind函數主要作用就是接受用戶層函數指針,并相應的將粘合層函數注冊
到LUA中。注意,lua_pushcfunction注冊的是binder_type::adapter函數。
那么,理論上,我們現在可以這樣注冊一個函數:



(這個時候to_string為:const char* to_string( int ) )
處理函數參數的個數
事情遠沒有我們想象的那么簡單。adapter函數中毫無實現,重要的是,該如何去實現?我們面對的首個問題是:上層
函數參數個數不一樣,那么我們的adapter該調用多少次lua_to*去從LUA棧中獲取參數?
解決該問題的辦法是,恩,很笨,但是這可以工作:為不同參數個數的函數都實現一個對應的adapter。沒有參數的函數對
應一個adapter,一個參數的函數對應另一個adapter,依次類推。
(穿插一下:ttl(tiny template library)庫中使用了一個很強大的宏技術,可以自動生成這些代碼,但是具體原理
我不懂。所以只能使用這個笨辦法了。)
這樣,我們就需要區分不同參數個數的函數原型。很顯然,我們需要改進lua_binder。行之有效的技術是:模板偏特化。
改進后的lua_binder類似于:

























lua_binder主體已經是一個單純的聲明而已,它的諸多特化版本將分別對應0個參數,1個參數,2個參數等。例如以上
列舉的就是一個參數的偏特化版本。
Now, we can try ??
那么,我們現在是否可以寫下諸多的lua_to*函數去獲取參數了?你覺得可以嗎?假設現在要獲取棧頂第一個參數,你
該調用lua_tonumber還是lua_tostring?
問題就在于,我們并不知道該調用哪個函數。
解決辦法是:根據不同的參數類型,調用對應的lua_to*函數。
不同的類型擁有不同的行為,這一點讓你想起什么?那就是模板世界里的type traits,類型萃取。我想,完成本文的
綁定器,更多的是對你模板編程能力的考驗。
lua_to*系列函數是有限的,因此我們也只需要實現幾個類型的行為即可。我們這個時候的目的就是,根據不同的類型,
調用對應的lua_to*函數。例如,對于number(int, long, double, float, char等等),我們就調用lua_tonumber。
于是得到:





















param_traits主體處理所有的number(因為number類型太多,也許concept可以解決這個問題),其他特化版本處理其他
類型。這樣,在adapter里,就可以根據參數類型獲取到相應的參數了,例如:
P1 p1 = param_traits<P1>::get_param( L, -1);
到這個時候,我們的adapter函數變為:(以一個參數的函數舉例)









And how about the result ??
是的,我們還需要處理函數返回值。我們暫時假設所有的函數都只有一個返回值。這里面對的問題同取參數一樣,我
們需要根據不同的返回值類型,調用對應的lua_push*函數壓入返回值。
同樣的type traits技術,你應該自己寫得出來,例如:

















到這個時候,我們的adapter函數基本成型了:








The last 'return' ???
最礙眼的,是adapter最后一行的return。LUA手冊上告訴我們,lua_CFunction必須返回函數返回值的個數。我們已經
假設我們只支持一個返回值,那么,很好,直接返回1吧。
關鍵在于,C/C++的世界里還有個關鍵字:void。是的,它表示沒有返回值。在用戶層函數返回值為void類型時(原諒
這矛盾的說法),我們這里需要返回0。
你意識到了什么?是的,我們需要根據返回值類型是否是void來設置這個return的值:1或者0。又是個type traits的
小技術。我想你現在很熟悉了:

















于是,我們的adapter變為:











Is everything OK?
我很高興我能流暢地寫到這里,同樣我希望我不僅向你展示了某個應用的實現,而是展示了模板編程的思想。
但是,問題在于,當你要注冊一個返回值為void的函數時:



你可能會被編譯器告知:非法使用void類型。
是的,好好省視下你的代碼,當你的binder_type::result_type為void時,在adapter函數中,你基本上也就寫下了
void r = something 的代碼。這是個語法錯誤。
同樣的問題還有:當返回值是void時,我們也沒有必要調用return_traits的set_result函數。
我想你覺察出來,又一個type traits技術。我們將根據result_type決定不同的處理方式。于是,我寫了一個caller:




















caller將根據不同的返回值類型決定如何去回調_func。比較遺憾的是,我們需要為每一個lua_binder編寫這么一個
caller,因為caller要調用_func,并且_func的參數個數不同。
那么現在,我們的adapter函數變為:







END
最后一段的標題不帶問號,所以這就結束了。下載看看我的代碼吧,為了給不同參數個數的函數寫binder,始終需要
粘貼復制的手工勞動。
值得注意的是,在最終的代碼里,我使用了以前實現的functor,將函數類型泛化。這樣,lua_binder就可以綁定類
成員函數,當然,還有operator()。
開源代碼某個時候還需要勇氣,因為開源意味著你的代碼會被人們考驗。不過我對我代碼的風格比較自信。:D
posted on 2008-08-13 09:33 Kevin Lynx 閱讀(6977) 評論(15) 編輯 收藏 引用 所屬分類: c/c++ 、lua