FlatBuffers 是Google推出的一個跨平臺、跨語言的序列化和反序列化庫,主要用于游戲以及對性能要求較高的系統(tǒng)中,例如RPC框架、保存端測推理的模型文件等(如TFLite)。端測不同于服務(wù)器,內(nèi)存和算力等資源相對于服務(wù)器十分有限,想要縮短整個推理的時間和內(nèi)存消耗,模型加載的階段也需要考慮。FlatBuffers可以只使用一塊內(nèi)存進(jìn)行解析,恰好滿足這些要求。其使用步驟如下:
- 下載源碼編譯得到一個編譯該庫指定的IDL(Interface Definition Language)所定義的Schema的編譯器
flatc
; - 按照IDL的語法編寫Schema;
- 使用第一步編譯出的
flatc
編譯第二步寫出的Schema,得到對應(yīng)語言的序列化和反序列化接口; - 使用第三步得到的接口進(jìn)行序列化和反序列化。
具體使用方法參考官方文檔即可。一般情況下,我們只需要知道FlatBuffers這個庫是怎么使用的就夠了,并不需要知道我們編寫的Schema是如何被編譯生成對應(yīng)語言的接口的。
但是有意思的是,F(xiàn)latBuffers包含了兩個我感興趣的東西:一個是它序列化數(shù)據(jù)的時候的思想,之前在FlatBuffer內(nèi)部解析原理簡介一文中有做過總結(jié);另一個就是它的編譯器。
俗話說麻雀雖小五臟俱全,作為一個編譯器,雖然相比于GCC、LLVM等它非常簡單,但是它的代碼中對于詞法分析、語法分析以及代碼生成等都有體現(xiàn)。
1. 工作流程
flatc
的入口位于flatbuffers/src/flatc_main.cpp
中,其具體工作流程如圖1所示。整個工作流程可以分為三部分:
- 解析命令行、初始化;
- 對源文件進(jìn)行解析,涉及詞法分析和語法分析,這兩個階段是合并在一起的;
- 目標(biāo)語言的代碼生成。
首先,
flatc
開辟了一個結(jié)構(gòu)體Generator
的數(shù)組空間,該結(jié)構(gòu)體如下所示。
struct Generator {
typedef bool (*GenerateFn)(const flatbuffers::Parser &parser,
const std::string &path,
const std::string &file_name);
typedef std::string (*MakeRuleFn)(const flatbuffers::Parser &parser,
const std::string &path,
const std::string &file_name);
GenerateFn generate;
const char *generator_opt_short;
const char *generator_opt_long;
const char *lang_name;
bool schema_only;
GenerateFn generateGRPC;
flatbuffers::IDLOptions::Language lang;
const char *generator_help;
MakeRuleFn make_rule;
};
后續(xù)通過匹配用戶命令行的參數(shù)選生成哪些語言的API,例如下面的結(jié)構(gòu)體實例是用于生成C++ API的,當(dāng)用戶的命令中存在-c
或者--cpp
,最終就會有C++的API生成。
{ flatbuffers::GenerateCPP, "-c", "--cpp", "C++", true,
flatbuffers::GenerateCppGRPC, flatbuffers::IDLOptions::kCpp,
"Generate C++ headers for tables/structs", flatbuffers::CPPMakeRule
}
緊接著,flatc
解析命令行參數(shù),解析完成后便開始編譯。FlatCompiler
對源文件進(jìn)行加載,之后委托Parser
進(jìn)行解析,DoParse()
就是整個解析的核心。
源文件解析完成后,通過查看Generator
數(shù)組,再相應(yīng)的委托BaseGenerator
對應(yīng)的子類進(jìn)行代碼生成,例如要生成C++代碼就委托CppGenerator
。
2. 詞法分析
詞法分析是每個編譯器進(jìn)行編譯的第一個階段,詞法分析的目的就是掃描從源碼文件中讀入的字符串,并將它們分成一個一個的Token,以便后面做語法分析。
雖然詞法分析和語法分析是編譯過程中的兩個階段,但通常情況下,它們之間并不是完全獨(dú)立的。語法分析并不會等待詞法分析將整個源文件都分成一個個Token才開始工作,語法分析會以命令的方式要求詞法分析器提供一個一個的Token。
在FlatBuffers flatc中,詞法分析和語法分析的代碼都是在類Parser
中完成的,其中Next()
方法負(fù)責(zé)詞法分析,每一次調(diào)用,它就會從當(dāng)前光標(biāo)開始掃描,然后返回下一個Token。Parser
中有一塊用于存放從文件中讀入的字符串的緩存source_
,它是一塊連續(xù)的內(nèi)存區(qū)域,可以看做是一個存放字符的數(shù)組;還有一個光標(biāo)cursor_
用于表示當(dāng)前掃描位置。
Parser
的語法分析器(其實也就是一個函數(shù)Parse()
)通過調(diào)用Next()
獲得一個一個的Token進(jìn)行語法分析。在Next()
方法中光標(biāo)cursor_
在source_
上從左向右滑動,并返回一個一個Token。Parse()
負(fù)責(zé)分析。例如在下面的例子,例子所示為一個名為Monster
的結(jié)構(gòu)體的定義。
table Monster {
pos:Vec3;
mana:short = 150;
hp:short = 100;
name:string;
friendly:bool = false (deprecated, priority: 1);
inventory:[ubyte];
color:Color = Blue;
test:Any;
}
一開始,光標(biāo)位于最開始得字符t
,然后開始滑動,直到劃過table
這個詞,遇見第一個空格,根據(jù)規(guī)則,此時table
被識別成一個Token,因此Next()
函數(shù)便將這個Token返回給調(diào)用者Parse()
。Parse()
在得到該Token后,識別到它是一個關(guān)鍵字,它后面應(yīng)該需要跟上的是一個標(biāo)識符,因此它再次調(diào)用Next()
去獲取下一個Token,并判斷這個Token是不是所期望的標(biāo)識符。如果得到的并不是一個標(biāo)識符,那么說明語法有誤,終止編譯并報錯。如果此時得到的Token是標(biāo)識符,那么根據(jù)要求,需要緊接著的是又花括號包含的成員定義,因此Parse()
在此調(diào)用Next()
去獲取下一個Token。語法分析器和詞法分析器就是這樣反復(fù)交互,直到整個文件掃描分析結(jié)束或者出錯終止。
這個Next()
的邏輯如圖2所示(狀態(tài)圖更合適,但是奈何手頭沒有適合畫狀態(tài)圖的工具)。
3. 語法分析
通常情況下,一般編譯器的語法分析器會構(gòu)造一顆解析樹,并將這顆解析樹傳遞給后續(xù)的編譯階段進(jìn)行進(jìn)一步處理。但是由于flatc編譯的是接口描述語言,語言本身并不復(fù)雜也不包含計算,并且最終生成的是其他語言的代碼,并不是直接運(yùn)行的機(jī)器碼,因此它只需要解析的同時提取到每個定義的結(jié)構(gòu)的名字、初始值等信息即可。
還是以上面的代碼為例,當(dāng)解析Monster
的時候,Parser
會將Monster
的信息保存在一個名叫struct_
的數(shù)組中。后續(xù)讀取此數(shù)組便可以獲取到用戶定義的信息進(jìn)行代碼生成。
整個解析過程如圖3所示。
4. 總結(jié)
看這部分的代碼最大的收獲就是對于如何解析一個文件豁然開朗,很多需要文本處理的軟件中都有著編譯器前端的部分影子。甚至是正則表達(dá),其實仔細(xì)想想,不就是一個詞法分析器么?
5. References
[1] http://google.github.io/flatbuffers/index.html
[2] https://github.com/google/flatbuffers