#
摘要: 一般的資源包文件格式基本上是由包文件頭和包內容組成。文件頭描述資源包內打包的文件
信息,例如文件名、在資源包里的偏移、大小、壓縮加密相關信息等;包內容則是實際文件
打包在一起后的內容,可能直接是未打包前文件連續存放在一起的內容,也可能是相同類型
文件除掉文件頭的內容(例如某個資源包里打包的全部是相同格式的圖片文件,那么這些圖
片文件被打包后包內只需要保存一個圖片文件頭,包內容全部是直接的圖片數據)。
閱讀全文
在上次的編譯原理練習中,生成的目標代碼是別人寫的一個基于寄存器的簡單虛擬機。這
回這個簡單的基于棧的虛擬機,純碎是為了彌補上回的練習不足。
基于寄存器(register based)的虛擬機和基于棧(stack based)的虛擬機主要的不同在于
對指令運算的中間值的保存方式。這些中間值包括各種運算的結果值,傳給各個指令的參
數等等。前者一般會設置幾個寄存器,如累加寄存器;后者則沒有寄存器,只有一個用來
保存這些值的棧。例如,這里我實現的SM(stack based machine)中的ADD指令:
ADD:從棧中依次彈出兩個數a和b,然后將b+a壓棧(b是左操作數)。基于這樣一個方
式,SM中大部分指令都不需要操作數,其操作數都直接從棧頂取。因為這個虛擬機僅僅是
用于上回設計的簡單語言的運行,即沒有函數、只有數字類型、有if和while。在這回練習中
我甚至把邏輯運算符給閹割了,只保留了大于小于之類的關系運算符。以下是該語言計算階
乘的例子:
read x;
if( x > 0 )
{
fac = 1;
while( x > 0 )
{
fac = fac * x;
x = x - 1;
}
write fac;
}
else
{
write 0;
}
基本上同《編譯原理與實踐》里的例子一樣,這樣省得我去琢磨語言文法。
不過,SM中還是有一個寄存器,即指令指針寄存器(pc),永遠指向將要執行的指令。在實現中,
所有指令都被保存一個數組里,所以pc就是一個指向數組索引的整數值。
SM中有一個簡單的內存,只用于保存程序中的全局變量(只有全局變量)。同樣,這個虛擬的
內存也被簡單地用一個數組來實現,所以,指令中的所有地址,都是數組索引值。
SM的代碼文件直接就是指令序列的二進制表示。在這個二進制文件中,內容依次為:操作碼(1
字節),操作數(4字節,如果有的話),操作碼,操作數,。。。SM讀取這樣的文件,將這些
指令放進指令數組,然后逐條解釋執行,直到遇到空指令。
代碼中的test是上面簡單提到的編程語言的編譯程序,該程序將源代碼編譯為SM可執行的二進制
文件(sm后綴)。為了方便調試,SM本身可以根據命令行參數輸出二進制文件對應的反匯編代碼,
這可以方便我調試編譯程序的代碼生成是否正常工作。同時,當初為了測試SM的正確性,還寫了
個簡單的匯編程序(sasm),可以把SM的匯編代碼轉換為二進制文件。
這回我特地在文法中間插入action丟給yacc處理,在賦值語句中一切正常。但是在if中yacc依然
提示警告,看起來應該跟if中的懸掛else二義性有關系。不過通過添加空的文法符號,居然解決了。
不清楚為什么上回死活有問題,詭異了。
下載SM
語法制導翻譯、中間代碼生成、目標代碼生成在很多時候并不存在嚴格的劃分。對于目標
代碼是某個簡單的虛擬機代碼而言,中間代碼完全可以就是目標代碼。中間代碼生成中結
合了語法制導翻譯,講述了大部分常規的編程語言結構是怎樣被翻譯成一種接近目標代碼
的形式(所謂的中間代碼形式)。本身,匯編代碼就是對應于機器碼的一種字符表示形式,
而中間代碼的大部分表示形式--三地址碼,也是一種接近匯編代碼的形式。
簡單來說,詞法分析階段將字符整理為單詞;語法分析則將這些代碼整理為一種層次結構
(識別了程序代碼要表達的意思);那么,在接下來的階段里,則是將這些層次結構翻譯
為線性結構。也就是類似于匯編代碼這種格式。這種格式容易被機器識別:機器只需要順
序地一條一條地取指令并執行之即可。這種簡單直接性也使得要實現類似的虛擬機變得容
易。
翻譯過程并不需要先生成語法樹,在語法分析階段的語法識別過程中,即可以對應地翻譯。
因為無論是自頂向下還是自底向上的語法分析,都可以先去識別葉子節點。在自頂向下中,
可以使用語法樹(并不真實存在)的后續遍歷,使得葉子節點先于父節點翻譯;而在自底
向上的分析中,因為其本身就是先識別葉子節點(所謂的規約),所以可以更自然地翻譯。
因為我也是想實踐下這些東西,所以還是使用lex/yacc來進行練習,省得自己去寫詞法和
語法分析。不過在使用yacc的過程中,經常會出現一些shift/reduce conflicts的警告/錯
誤,解決這些問題也費了不少時間。不過,也可能是我對LALR細節不熟悉,加之于文法本
身寫的有問題,才弄得如此折騰。現在我覺得上下文無關文法在整個編譯原理理論中非常
重要。一個好的文法可以準確無誤地描述一種編程語言的語法,還可以指導編譯器的開發。
當然,大部分常規的語言都可以找到現成的文法。
例子程序構造了一個簡單的翻譯程序,支持簡單的算術表達式、整數變量、if、while、以
及僅用于if和while的邏輯表達式。為了省力,虛擬機用的是《編譯原理與實踐》中現成的。
目標代碼也就直接是該虛擬機對應的代碼。該虛擬機主要有5個寄存器:指令指針寄存器、
2個累加寄存器、全局變量基址寄存器、臨時變量基址寄存器。這里的臨時變量不同于編
程語言說的臨時變量,它是表達式計算的臨時值,例如a+b+c,a+b的結果值就可以被實現
為存儲到一個臨時值中。
對于算術表達式,其實翻譯起來很簡單。主要是if/while和邏輯表達式的翻譯。邏輯表達
式的翻譯例子中我甚至沒有處理短路代碼:a && func(1)中如果已經計算出a為false了,
就沒必要計算func(1)了。這可能是受限于yacc,暫不深究。對于if和while,則主要涉及
到所謂的“回填”技術。
回填主要是應對向前跳轉這種問題。例如在if的代碼生成中,需要測試if的邏輯表達式的
真假,如果為假,則需要跳轉到then-part之后。這里的then-part也就是if為真時要執行
的代碼序列。而這個跳轉指令具體要跳到哪里,則需要在生成完then-part之后才能確定。
回填的解決方法,就是預留下這個跳轉指令的位置,等到then-part生成完了,得到了具
體的跳轉位置,再回去填寫那個跳轉指令。
在這個問題上,yacc也讓我折騰了一番。在if文法中:
selection_statement
: IF '(' logical_or_expr ')' {
// 本來想在這里預留這個跳轉指令的位置
} statement %prec IFX {
}
結果,yacc又給我conflicts和never reduced之類的警告,并且最終生成的代碼也不正常
(果然是無法忽略的警告)。看起來,yacc似乎不支持在文法內部添加action。通過一個
空文法符號效果一樣。對于這個問題,我甚至莫名其妙地在某個晚上的夢里當面問了yacc
的作者。他肯定地告訴我:支持,當然支持(中文)。今天仔細地在yacc文檔里找了下,
還真支持。而且對于空符號的使用,似乎也有些規則:$Sign: {action }。
后來解決這個問題的方法,算是我取巧了:
selection_statement
: IF '(' logical_or_expr IfBracket statement %prec IFX { ....}
IfBracket
: ')' {
// 邪惡地利用了這個括號
}
另外,因為需要支持嵌套的if/while,還專門使用了一個棧,用于保存這些需要回填的預留地址。
下載例子
語義分析理論中并沒有語法和詞法分析階段中那么多算法。如同整個編譯原理里大部分理論
一樣,其都是為實踐編碼提供理論支持,從而可以讓實現簡單機械地進行---語法制導翻譯
也正是出于這個目的:通過建立理論,使得從語法樹翻譯到中間代碼(或者虛擬機代碼)更
為簡單。
個人理解,語法制導翻譯就是在文法中加上屬性和各種動作,這些動作基本也就是這里的“
翻譯”;而語義分析,則是在整個過程所要進行的一些上下文相關的分析動作(如類型檢查
)。
羅列一些概念:
- 屬性:就是語法樹各個節點上所需要的“值”,例如對于算術表達式a=b+c,在其語法樹
中,每一個節點都會有一個數字屬性。屬性明顯不止包含數字/值這些東西,某個節點包含
哪些具體屬性完全取決于編譯器實現的需要。對于表達式a=b如果需要檢查a和b的類型是否
可以賦值(如在c語言中,struct XXX b就無法傳給int a),就需要在其節點中附加類型屬
性。---這里舉的例子也正是一種語義分析行為。
- 綜合屬性:某個節點的屬性依賴于其子節點的屬性,這種屬性計算起來很簡單,尤其在遞
歸下降分析程序中。
- 繼承屬性:某個節點的屬性依賴于其父節點或者其兄弟節點。這個屬性計算起來要稍微麻
煩一些,需要一種屬性傳遞機制。在上一篇LL分析法的練習程序中,就使用了一個“值棧”
來傳遞屬性。
- 依賴圖:上面提到屬性之間的依賴,在一棵語法中,通過箭頭描繪出這種依賴關系就得到
依賴圖,說白了就是拿來方便看的,無視。
- 語法制導定義(SDD):學編譯原理最煩的就是這些定義,一句話里總覺得有若干未知概
念,尤其在翻譯比較爛的時候。我覺得這個SDD就是在文法中穿插了若干屬性和翻譯動作的
表示。
- S屬性的SDD:如果一個SDD的每一個屬性都是綜合屬性,那它就是S屬性的。
- L屬性的SDD:無視了,就是夾雜著綜合屬性和繼承屬性的SDD,不過繼承屬性有諸多條件
限制,大致上就是其某個屬性的依賴關系僅限于其左兄弟或者父節點。
其實這一切都并非它看上去的那么繁雜。在有了語法分析的基礎上,因為馬上涉及到翻譯為
中間代碼(甚至會直接翻譯為虛擬機代碼),在這個階段直接把代碼中會做的事情書寫到文
法里,就得到了SDD。按照這個SDD,就可以較為機械地對應寫出代碼。另一方面,在實際中
為了處理不同的翻譯問題,如類型檢查、各種控制語句的翻譯,直接參考相關的資料,看看
別人怎么處理的就行了。
練習程序是一個簡單地處理c語言定義變量、定義struct類型的代碼。因為c語言里的變量會
附帶類型屬性,用于類型檢查之類的問題,所以程序中保存這些變量的符號表自然也要保存
其類型。定義新的struct類型我這里直接建立了一個類型表,用于存儲所有的類型:基本類
型和程序員自定義類型。
練習程序直接使用了lex和yacc來生成詞法和語法分析模塊,通過直接在文法文件里(*.y)的
文法中穿插各種動作來完成具體的處理邏輯。本來我最開始是想個類型檢查程序的,起碼可
以檢查一般的類型不匹配錯誤/警告,不過后來僅僅做了變量/類型定義,就發現有一定代碼
量了,索性就懶得做下去了。
下載例子
LL(1)分析法和遞歸下降分析法同屬于自頂向下分析法。相對于遞歸下降而言,LL通過顯示
地維護一個棧來進行語法分析,遞歸下降則是利用了函數調用棧。
LL分析法主要由分析棧、分析表和一個驅動算法組成。其實LL的分析算法還是很容易懂的,
主要就是一個匹配替換的過程。而要構造這里的分析表,則還涉及計算first集和follow集
的算法。
個人覺得龍書在解釋這些算法和概念時都非常清楚細致,雖然也有人說它很晦澀。
first集和follow集的計算,拋開書上給的嚴密算法,用人的思維去理解(對于compiler
compiler則需要用程序去構造這些集合,這是讓計算機去理解),其實很簡單:
1、對于某個非終結符A的first集(first(A)),簡單地說就是由A推導得到的串的首符號的
集合:A->aB,那么這里的a就屬于first(A),很形象。
2、follow(A),則是緊隨A的終結符號集合,例如B->Aa,這里的a就屬于follow(A),也很形
象。
當然,因為文法符號中有epsilon,所以在計算上面兩個集合時則會涉及到一種傳遞性。例
如,A->Bc, B->epsilon,B可以推導出epsilon,也就是基本等同于沒有,那么first(A)中
就會包含c符號。
在了解了first集和follow集的計算方法后,則可以通過另一些規則構造出LL需要的分析表。
編譯原理里總有很多很多的理論和算法。但正是這些理論和算法,使得編譯器的實現變得簡
單,代碼易維護。
在某個特定的編程語言中,因為其文法一定,所以對于其LL(1)實現中的分析表就是確定的
。我們也不需要在程序里動態構造first和follow集合。
那么,要實現一個LL(1)分析法,大致步驟就集中于:設計文法->建立該文法的分析表->編
碼。
LL分析法是不能處理左遞歸文法的,例如:expr->expr + term,因為左遞歸文法會讓對應
的分析表里某一項存在多個候選式。這里,又會涉及到消除左遞歸的方法。這個方法也很簡
單,只需要把文法推導式代入如下的公式即可:
A -> AB | C 等價于:A -> CX, X -> BX | epsilon
最后一個問題是,如何在LL分析過程中建立抽象語法樹呢?雖然這里的LL分析法可以檢查文
法對應的語言是否合法有效,但是似乎還不能做任何有意義的事情。這個問題歸結于語法制
導翻譯,一般在編譯原理教程中語法分析后的章節里。
LL分析法最大的悲劇在于將一棵在人看來清晰直白的語法樹分割了。在遞歸下降分析法中,
一個樹節點所需要的屬性(例如算術運算符所需要的操作數)可以直接由其子節點得到。但
是,在為了消除左遞歸而改變了的文法式子中,一個節點所需要的屬性可能跑到其兄弟節點
或者父節點中去了。貌似這里可以參考“繼承屬性”概念。
不過,綜合而言,我們有很多業余的手段來處理這種問題,例如建立屬性堆棧。具體來說,
例如對于例子代碼中計算算術表達式,就可以把表達式中的數放到一個棧里。
例子中,通過在文法表達式中插入動作符號來標識一個操作。例如對于文法:
expr2->addop term expr2,則可以改為:expr2->addop term # expr2。當發現分析棧的棧
頂元素是'#'時,則在屬性堆棧里取出操作數做計算。例子中還將操作符壓入了堆棧。
下載例子,例子代碼最好對照arith_expr.txt中寫的文法和分析表來看。
PS,最近在云風博客中看到他給的一句評論,我覺得很有道理,并且延伸開來可以說明我們
周圍的很多現象:
”很多東西,意識不到問題比找不到解決方法要嚴重很多。比如one-pass 這個,覺得實現
麻煩不去實現,和覺得實現沒有意義不去實現就是不同的。“
對于以下現象,這句話都可以指明問題:
1、認為造輪子沒有意義,從不考慮自己是否能造出;
2、常告訴別人某個技術復雜晦澀不利于團隊使用,卻并不懂這個技術;
3、籠統來說,【覺得】太多東西沒有意義,雖然并不真正懂這個東西。
tolua++自動生成綁定代碼時,不支持插入預編譯頭文件。雖然可以插入直接的C++代碼例如
,如$#include xxxx,但插入位置并沒有位于文件頭。對于使用預編譯頭的大型工程而言,
尤其是某個綁定代碼依賴了工程里其他很多東西,更萬惡的是預編譯頭文件里居然包含很多
自己寫的代碼時,支持插入預編譯頭文件這個功能很重要。
說白了,也就是要讓tolua++在生成的代碼文件開頭插入#include "stdafx.h"。
修改代碼其實很簡單。tolua++分析pkg文件及生成代碼文件其實都是通過lua代碼完成的。
在src/bin/lua目錄下,或者在源代碼里toluabind.c里(把對應的lua代碼直接以ASCII碼值
復制了過來)即為這些代碼。
首先修改package.lua里的classPackage::preamble函數,可以看出該函數會生成一些代碼
文件頭,模仿著即可寫下如下代碼:
if flags['I'] then
output( '#include "..flags['I'] )
end
從上下文代碼可以看出flags是個全局變量,保存了命令行參數。
然后修改tolua.c代碼文件,讓其往lua環境里傳入命令行參數:
case 'I':setfield(L,t,"I",argv[++i];break;
本來,這樣修改后基本就可以讓tolua++支持通過命令行指定是否插入預編譯頭:
tolua++ -o test.cpp -H test.h -I stdafx.h test.pkg
不過事情并非很順利,通過開啟TOLUA_SCRIPT_RUN宏來讓tolua++通過src/bin/lua下的lua
代碼來完成功能,結果后來發現basic.lua似乎有問題。無奈之下,只好用winhex之類的工
具把修改過的package.lua轉換為unsigned char B[]置于toluabind.c里,即可正常處理。
之所以說是“簡要實現”一方面是因為算法不算高深,算法的實現也不精致,甚至連我對其的理解也不夠本質。
我只不過不想在工作若干年后還是一個只會打字的程序員。學點什么東西,真正精通點什么東西才對得起喜歡
技術的自己。
附件中的代碼粗略實現了《編譯原理》龍書中的幾個算法。包括解析正則表達式,建立NFA,然后用NFA去匹
配目標字符串;或者從NFA建立DFA,然后匹配。解析正則表達式我用了比較繁瑣的方法,有詞法和語法分析
過程。詞法分析階段將字符和一些操作符整理出來,語法分析階段在建立語法樹的過程中對應地建立NFA。
當然,因為語法樹在這里并沒有用處,所以并沒有真正地建立。
從正則表達式到NFA比較簡單,很多編譯原理書里都提到過,如:s|t表達式對應于下面的NFA:
代碼中用如下的結構描述狀態和狀態機中的轉換:
#define E_TOK (0)
/* transition */
struct tran
{
char c;
struct state *dest;
struct tran *next;
};
struct state
{
/* a list of transitions */
struct tran *trans;
/* inc when be linked */
int ref;
};
即,每一個狀態都有一個轉換列表,每個轉換都有一個目標狀態(即該轉換指向的狀態)以及轉換字符。
貌似通過以上方法建立出來的狀態機每個狀態最多只會有2個轉換?
建立好NFA后,由NFA匹配目標字符串使用了一種構造子集法(《編譯原理》3.7.2節):
這個算法里針對NFA的幾個操作,如e-closure、move等在由NFA轉換DFA時也被用到,因此代碼里單獨
做了封裝(state_oper.c)。這個算法本質上貌似就是一次步進(step)多個狀態。
至于由NFA轉DFA,則是相對簡單的子集構造法:
在我以前編譯原理課考試的前一天晚上(你懂的)我就對這些算法頗為疑惑。在以后看各種編譯
原理教材時,我始終不懂NFA是怎么轉到DFA的。就算懂了操作步驟(我大學同學曾告訴我這些步驟,雖然
不知道為什么要那樣做),一段時間后依然搞忘。很喜歡《編譯原理》龍書里對這個算法最本質的說明:
源代碼我是用GCC手工編譯的,連makefile也沒有。三個test_XXX.c文件分別測試幾個模塊。test_match.c
基本依賴除掉test外所有c文件,全部鏈接在一塊即可。當然,就經驗而言我知道是沒幾個人會去折騰我的這些
代碼的。這些在china的領導看來對工作有個鳥用的代碼讀起來我自己也覺得費力,何況,我還不倫不類地用了
不知道算哪個標準的c寫了這些。
你不是真想下載。對于這種代碼,有BUG是必然的,你也不用在此文若干個月后問我多少行是什么意思,因為
那個時候我也忘了:D。
在我自己寫的一個工廠類實現中,每個產品會注冊創建接口到這個工廠類。工廠類使用這些
注冊進來的創建接口來完成產品的創建。其結構大致如下:
product *factory::create( long product_type )
{
creator c = m_creators[product_type];
return c();
}
factory::instance().register( PRODUCT_A_TYPE, productA::create );
...
factory::instance().create( PRODUCT_A_TYPE );
這個很普通的工廠實現中,需要寫上很多注冊代碼。每次添加新的產品種類時,也需要修改
這些的注冊代碼。而恰好,這些注冊代碼可能會被放在一個統一的地方。為了消除這個地方
,我使用了偶然間看到的<Modern C++ design>里的做法:
const bool _local = factory::instance().register( PRODUCT_A_TYPE,...
也就是說,通過對全局常量_local的自動初始化,來自動完成對該產品的注冊。
結果,因為這些代碼全部被放置于一個靜態庫。最終的代碼文件結構大致為:
lib
- product_a.cpp : 定義了全局常量_local
- product_a.h
- factory.cpp
- factory.h
exe
- main.cpp
現在看起來世界很美,因為factory甚至不知道世界上還有個跟上層邏輯相關的product_a。
這種模塊耦合幾乎為0的結構讓我竊喜。
悲劇的事情首先發生于,開VC調試器,發現打在product_a.cpp里的斷點失效。就是那個總
是提示說沒有為該文件加載調試符號。開始還不在意,以為又是代碼和調試符號文件不匹配
的原因,折騰了好久,不得其果。
后來分析了下,發現這個調試提示,就像我開著調試器打開了一個非本工程的代碼文件,而
斷點就打在這個文件里一樣。也就是說,VC把我product_a.cpp當成不是這個工程里的代碼
文件。
按照這個思路寫些實驗代碼,最終發現問題所在:VC鏈接器根本沒鏈接進product_a.cpp里
的代碼。表現出來的情況就是,該編譯單元里的全局常量(全局變量一樣)根本沒有得到初
始化,因為我跟到factory::register并沒有被調用到。為什么VC不鏈接這個編譯單元對應
的目標文件?或者說,為什么VC不初始化這個全局常量?
原因就在于,product_a.cpp太獨立了。一個在整個編譯鏈接階段都無法確定該文件是否被
使用的文件,VC就直接不鏈接了。相反,當在factory.cpp里寫下類似代碼:
void test()
{
product_a obj;
}
雖然說test函數不會被調用,一切情況也變得正常了。好了,不扯了,給最后我的結論:
1、如果靜態庫中某個編譯單元在編譯階段被確認為它并沒有被外部使用,那么當這個靜態
庫被鏈接進可執行文件時,鏈接器忽略掉該編譯單元里的代碼,那么,鏈接器自然也不會為
該編譯單元里出現的全局變量常量生成初始化代碼(關于這部分初始化代碼可以閱讀
<linker and loader>一書);
2、上面那條結論存在一種傳染性,意思是,當可執行文件里的代碼使用到靜態庫中文件A里
的代碼,A里又有地方使用到B里的代碼,那么B依然會被鏈接。這種依賴性,應該可以讓編
譯器在編譯階段就發現(顯然,上面我舉的例子里,factory只有在運行期間才會依賴到
product_a.cpp里的代碼)
很早前在折騰掛起LUA腳本支持時,接觸到lua_yield這個函數。lua manual中給的解釋是:
This function should only be called as the return expression of a C function。
而這個函數一般是在一個注冊到LUA環境中的C函數里被調用。lua_CFunction要求的原型里
,函數的返回值必須返回要返回到LUA腳本中的值的個數。也就是說,在一個不需要掛起的
lua_CFunction實現里,也就是一個不需要return lua_yield(...的實現里,我應該return
一個返回值個數。
但是為什么調用lua_yield就必須放在return表達式里?當時很天真,沒去深究,反正發現
不按照lua manual里說的做就是不行。而且關鍵是,lua manual就不告訴你為什么。
最近突然就想到這個問題,決定去搞清楚這個問題。侯捷說了,源碼面前了無秘密。我甚至
在看代碼之前,還琢磨著LUA是不是操作了堆棧(系統堆棧)之類的東西。結果隨便跟了下
代碼真的讓我很汗顏。有時候人犯傻了真的是一個悲劇。諾簡單的一個問題會被人搞得很神
秘:
解釋執行調用一個注冊進LUA的lua_CFunction是在ldo.c里的luaD_precall函數里,有如下
代碼:
n = (*curr_func(L)->c.f)(L); /* do the actual call */
lua_lock(L);
if (n < 0) /* yielding? */
return PCRYIELD;
else {
luaD_poscall(L, L->top - n);
return PCRC;
}
多的我就不說了,別人注釋寫得很清楚了,注冊進去的lua_CFunction如果返回值小于0,這
個函數就向上層返回PCRYIELD,從名字就可看出是告訴上層需要YIELD。再找到lua_yield函
數的實現,恰好該函數就返回-1。
要再往上層跟,會到lvm.c里luaV_execute函數,看起來應該就是虛擬機在解釋執行指令:
case OP_CALL: {
int b = GETARG_B(i);
int nresults = GETARG_C(i) - 1;
if (b != 0) L->top = ra+b; /* else previous instruction set top */
L->savedpc = pc;
switch (luaD_precall(L, ra, nresults)) {
case PCRLUA: {
nexeccalls++;
goto reentry; /* restart luaV_execute over new Lua function */
}
case PCRC: {
/* it was a C function (`precall' called it); adjust results */
if (nresults >= 0) L->top = L->ci->top;
base = L->base;
continue;
對于PCRYIELD返回值,直接忽略處理了。
用途
在一個UI與邏輯模塊交互比較多的程序中,因為并不想讓兩個模塊發生太大的耦合,基本目標是
可以完全不改代碼地換一個UI。邏輯模塊需要在產生一些事件后通知到UI模塊,并且在這個通知
里攜帶足夠多的信息(數據)給接收通知的模塊,例如UI模塊。邏輯模塊還可能被放置于與UI模
塊不同的線程里。
最初的結構
最開始我直接采用最簡單的方法,邏輯模塊保存一個UI模塊傳過來的listener。當有事件發生時,
就回調相應的接口將此通知傳出去。大致結構如下:

/**//// Logic
class EventNotify

{
public:
virtual void OnEnterRgn( Player *player, long rgn_id );
};


/**//// UI
class EventNotifyImpl : public EventNotify

{
};


/**//// Logic
GetEventNotify()->OnEnterRgn( player, rgn_id );

但是,在代碼越寫越多之后,邏輯模塊需要通知的事件越來越多之后,EventNotify這個類開始
膨脹:接口變多了、不同接口定義的參數看起來也越來越惡心了。
改進
于是我決定將各種事件通知統一化:
struct Event


{
long type; // 事件類型
// 附屬參數
};
這樣,邏輯模塊只需要創建事件結構,兩個模塊間的通信就只需要一個接口即可:
void OnNotify( const Event &event );
但是問題又來了,不同的事件類型攜帶的附屬參數(數據)不一樣。也許,可以使用一個序列化
的組件,將各種數據先序列化,然后在事件處理模塊對應地取數據出來。這樣做總感覺有點大動
干戈了。當然,也可以使用C語言里的不定參數去解決,如:
void OnNotify( long event_type, ... )
其實,我需要的就是一個可以表面上類型一樣,但其內部保存的數據卻多樣的東西。這樣一想,
模塊就能讓事情簡單化:
template <typename P1, typename P2>
class Param


{
public:
Param( P1 p1, P2 p2 ) : _p1( p1 ), _p2( p2 )

{
}
P1 _p1;
P2 _p2;
};

template <typename P1, typename P2>
void OnNotify( long event_type, Param<P1, P2> param );

GetNotify()->OnNotify( ET_ENTER_RGN, Param<Player*, long>( player, rgn_id ) );
GetNotify()->OnNotify( ET_MOVE, Param<long, long>( x, y ) );

在上面這個例子中,雖然通過Param的包裝,邏輯模塊可以在事件通知里放置任意類型的數據,但
畢竟只支持2個參數。實際上為了實現支持多個參數(起碼得有15個),還是免不了自己實現多個
參數的Param。
幸虧我以前寫過宏遞歸產生代碼的東西,可以自動地生成這種情況下諸如Param1、Param2的代碼。
如:
#define CREATE_PARAM( n ) \
template <DEF_PARAM( n )> \
struct Param##n \

{ \
DEF_PARAM_TYPE( n ); \
Param##n( DEF_FUNC_PARAM( n ) ) \

{ \
DEF_MEM_VAR_ASSIGN( n ); \
} \
DEF_VAR_DEF( n ); \
}

CREATE_PARAM( 1 );
CREATE_PARAM( 2 );

即可生成Param1和Param2的版本。其實這樣定義了Param1、Param2的東西之后,又使得OnNotify
的參數不是特定的了。雖然可以把Param也泛化,但是在邏輯層寫過多的模板代碼,總感覺不好。
于是又想到以前寫的一個東西,可以把各種類型包裝成一種類型---對于外界而言:any。any在
boost中有提到,我只是實現了個簡單的版本。any的大致實現手法就是在內部通過多態機制將各
種類型在某種程度上隱藏,如:
class base_type

{
public:
virtual ~base_type()

{
}
virtual base_type *clone() const = 0;
};
template <typename _Tp>
class var_holder : public base_type

{
public:
typedef _Tp type;
typedef var_holder<type> self;
public:
var_holder( const type &t ) : _t( t )

{
}

base_type *clone() const

{
return new self( _t );
}
public:
type _t;
}

這樣,any類通過一個base_type類,利用C++多態機制即可將類型隱藏于var_holder里。那么,
最終的事件通知接口成為下面的樣子:
void OnNotify( long type, any data );
OnNotify( ET_ENTER_RGN, any( create_param( player, rgn_id ) ) );其中,create_param
是一個輔助函數,用于創建各種Param對象。
事實上,實現各種ParamN版本,讓其名字不一樣其實有點不妥。還有一種方法可以讓Param的名字
只有一個,那就是模板偏特化。例如:
template <typename _Tp>
struct Param;

template <>
struct Param<void()>;

template <typename P1>
struct Param<void(P1)>

template <typename P1, typename P2>
struct Param<void(P1,P2)>

這種方法主要是通過組合出一種函數類型,來實現偏特化。因為我覺得構造一個函數類型給主模版,
并不是一種合情理的事情。但是,即使使用偏特化來讓Param名字看起來只有一個,但對于不同的
實例化版本,還是不同的類型,所以還是需要any來包裝。
實際使用
實際使用起來讓我覺得非常賞心悅目。上面做的這些事情,實際上是做了一個不同模塊間零耦合
通信的通道(零耦合似乎有點過激)。現在邏輯模塊通知UI模塊,只需要定義新的事件類型,在
兩邊分別寫通知和處理通知的代碼即可。
PS:
針對一些評論,我再解釋下。其實any只是用于包裝Param列表而已,這里也可以用void*,再轉成
Param*。在這里過多地關注是用any*還是用void*其實偏離了本文的重點。本文的重點其實是Param:
OnNotify( NT_ENTER_RGN, ang( create_param( player, rgn_id ) ) );

->
void OnNotify( long type, any data )


{
Param2<Player*, long> ParamType;
ParamType *p = any_cast<ParamType>( &data );
Player *player = p->p1;
long rgn_id = p->p2;
}


下載相關代碼