FlatBuffers 是Google推出的一個跨平臺、跨語言的序列化和反序列化庫仙畦,主要用于游戲以及對性能要求較高的系統(tǒng)中输涕,例如RPC框架、保存端測推理的模型文件等(如TFLite)慨畸。端測不同于服務(wù)器莱坎,內(nèi)存和算力等資源相對于服務(wù)器十分有限,想要縮短整個推理的時(shí)間和內(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ù)的時(shí)候的思想兑巾,之前在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)體實(shí)例是用于生成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í)也就是一個函數(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ī)則像鸡,此時(shí)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)識符荠医,那么說明語法有誤吁脱,終止編譯并報(bào)錯。如果此時(shí)得到的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ù)雜也不包含計(jì)算别厘,并且最終生成的是其他語言的代碼虱饿,并不是直接運(yùn)行的機(jī)器碼,因此它只需要解析的同時(shí)提取到每個定義的結(jié)構(gòu)的名字、初始值等信息即可氮发。
還是以上面的代碼為例渴肉,當(dāng)解析Monster
的時(shí)候,Parser
會將Monster
的信息保存在一個名叫struct_
的數(shù)組中爽冕。后續(xù)讀取此數(shù)組便可以獲取到用戶定義的信息進(jìn)行代碼生成仇祭。
整個解析過程如圖3所示。
4. 總結(jié)
看這部分的代碼最大的收獲就是對于如何解析一個文件豁然開朗颈畸,很多需要文本處理的軟件中都有著編譯器前端的部分影子乌奇。甚至是正則表達(dá),其實(shí)仔細(xì)想想眯娱,不就是一個詞法分析器么礁苗?
5. References
[1] http://google.github.io/flatbuffers/index.html
[2] https://github.com/google/flatbuffers