FlatBuffers:flatc源碼簡析

FlatBuffers 是Google推出的一個跨平臺、跨語言的序列化和反序列化庫,主要用于游戲以及對性能要求較高的系統(tǒng)中,例如RPC框架、保存端測推理的模型文件等(如TFLite)。端測不同于服務(wù)器,內(nèi)存和算力等資源相對于服務(wù)器十分有限,想要縮短整個推理的時間和內(nèi)存消耗,模型加載的階段也需要考慮。FlatBuffers可以只使用一塊內(nèi)存進(jìn)行解析,恰好滿足這些要求。其使用步驟如下:

  1. 下載源碼編譯得到一個編譯該庫指定的IDL(Interface Definition Language)所定義的Schema的編譯器flatc;
  2. 按照IDL的語法編寫Schema;
  3. 使用第一步編譯出的flatc編譯第二步寫出的Schema,得到對應(yīng)語言的序列化和反序列化接口;
  4. 使用第三步得到的接口進(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所示。整個工作流程可以分為三部分:

  1. 解析命令行、初始化;
  2. 對源文件進(jìn)行解析,涉及詞法分析和語法分析,這兩個階段是合并在一起的;
  3. 目標(biāo)語言的代碼生成。

圖1 flatbuffers workflow sequence

首先,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)圖的工具)。

圖2 Parser::Next()

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所示。


圖3 Parser::DoParse()

4. 總結(jié)

看這部分的代碼最大的收獲就是對于如何解析一個文件豁然開朗,很多需要文本處理的軟件中都有著編譯器前端的部分影子。甚至是正則表達(dá),其實仔細(xì)想想,不就是一個詞法分析器么?

本文首發(fā)于個人微信公眾號TensorBoy,微信掃描上方二維碼或者微信搜索TensorBoy并關(guān)注,及時獲取最新文章。C++ | Python | Linux | 原理 | 源碼,有一起玩耍的么?

5. References

[1] http://google.github.io/flatbuffers/index.html
[2] https://github.com/google/flatbuffers

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。