Fidl Compiler 前端

本文件會概略說明 FIDL 編譯器 fidlc 的內部運作。 fidlc 是會接收多個 FIDL 檔案的指令列工具,並會輸出 FIDL JSON IR

總覽

編譯器的主要輸入內容是 --files 旗標的引數,用於描述按照程式庫分組的檔案清單。編譯器會個別剖析每個檔案,以取得每個檔案的剖析樹狀結構:

  1. 每個檔案都會載入至 SourceFile,擁有備份檔案的緩衝區
  2. 編譯器會初始化 Lexer,後者會將 SourceFile 做為引數傳送至其建構函式。此類別公開 Lex() 方法,此方法會傳回檔案中的下一個 Token;您可以重複呼叫此方法,取得檔案中的 Token 序列。
  3. 編譯器使用 Lexer 初始化 Parser,然後呼叫 Parse() 方法,建構剖析樹狀結構。在程式碼中,剖析樹狀結構是原始 AST。這個函式會傳回 raw::File,這是代表原始 AST 根節點的類別。
  4. 編譯器將每個檔案剖析成剖析樹狀結構後,就會針對每個程式庫,將剖析樹狀結構分組為單一 AST (在程式碼中稱為平面 AST)。此固定費率 AST 的根是 flat::Library
    • 針對每個程式庫,編譯器會掃遍每個程式庫對應的 raw::File 剖析樹狀結構,將 raw:: 節點轉換為其 flat:: 對應項目。舉例來說,raw::StructDeclaration 會變為 flat::Struct,而 raw::Constant 則會變為 flat::Constant。系統會將轉換後的 flat:: AST 節點儲存在單一 flat::Library 底下。這些扁平的 AST 節點一開始包含與原始 AST 節點相同的資訊,但還包含值 value 等欄位,以及之後在編譯步驟中設定的類型欄位的 typeshape 欄位。
  5. AST 完全初始化後,編譯器會評估常數,並判斷宣告類型的記憶體對齊方式和大小資訊。

最後,我們最終將一個扁平的 AST 經過處理,可用於將後端產生至 C 繫結或 JSON IR。

勒克斯

Lexer 主要是透過追蹤檔案資料中的兩個 char * 指標。current_ 指標會指出類別目前的位置。token_start_ 指標會標示目前正在處理的 lexe 的開頭。每次呼叫 Lex() 方法時,current_ 指標都會進階,直到完成周遊的完整 lexe 為止。接著,系統會使用兩個指標之間的資料建構 Token

以下範例說明在簡短 FIDL 程式碼片段 const bool flag = true; 上呼叫 Lex() 期間的指標變化:

排序 const 關鍵字之後的初始狀態:

 const bool flag = true;
      ^current_
      ^token_start_

系統會略過空白字元,直到下一個項目:

const bool flag = true;
      ^current_
      ^token_start_

current_ 指標會更新,直到 lexeme 結束為止:

const bool flag = true;
          ^current_
      ^token_start_

此時,傳回的下一個 Token 已經準備好建構。location_ 設為 token_start_current_ 之間的資料。種類設為 Identifier。傳回之前,指標會重設,且最終會呈現與初始狀態類似的狀態。接著,您可以重複這項程序,產生下一個符記:

 const bool flag = true;
           ^current_
           ^token_start_

在內部,這兩個指標都是透過以下主要方法操控:

  • Skip()。將 current_token_start_ 指標向前移動,略過任何不必要的字元 (例如空白字元)。
  • Consume():傳回目前的字元並推進 current_
  • Reset()。傳回 token_start_current_ 之間的資料。接著,將 token_start_ 設為 current_ 的值。

剖析

Parser 的目標是使用 Parse() 方法,將 Token 串流轉換為剖析樹狀結構 (raw:: AST)。在 FIDL 檔案上重複呼叫 Lex,就會產生 Token 串流。這個剖析器是以遞迴下降實作。原始 AST 的每個節點都有對應的 ParseFoo() 方法,此方法會使用 LexerToken,並將 unique_ptr 傳回該節點的執行個體。如果失敗,系統會傳回 nullptr

Parser 會追蹤以下項目:

  • 目前使用 SourceElements 堆疊建構的節點,這些堆疊儲存在 active_ast_scopes_ 中。
  • 正在處理中的當前和先前 Tokenlast_token_previous_token_
  • 其在 Parser::Lex 方法中提供了狀態機器。目前的權杖 (last_token_) 一律會是下一個即將使用的權杖,這會有效為剖析器提供一個權杖先行 (即 LL(1))。

Parser 會根據 last_token_Token::Kind (使用 Peek() 方法),決定目前 Token 所屬的節點類型。接著,Parser 會更新狀態,並使用 ASTScope 類別以及 ConsumeTokenMaybeConsumeToken 輔助方法來建構節點。

這個範例逐行顯示簡易的非週期性案例。剖析器方法如下所示:

std::unique_ptr<raw::StringLiteral> Parser::ParseStringLiteral() {
    ASTScope scope(this);
    ConsumeToken(OfKind(Token::Kind::kStringLiteral));
    if (!Ok())
        return Fail();

    return std::make_unique<raw::StringLiteral>(scope.GetSourceElement());
}

這個函式會使用單一權杖並傳回分葉節點 raw::StringLiteral

class StringLiteral : public SourceElement {
public:
    explicit StringLiteral(SourceElement const& element) : SourceElement(element) {}

    virtual ~StringLiteral() {}
}

該方法的第一步是建立新的 ASTScope,用於初始化 SourceElement;之後會將這個節點推送至 active_ast_scopes_,藉此建立傳回的節點。SourceElement 的起始值會設為使用的第一個符記,最後會在呼叫 GetSourceElement() 前設定結束的符記。

StringLiteral 的新 SourceElement 會在 ASTScope 建構函式中初始化:

parser_->active_ast_scopes_.push_back(raw::SourceElement(Token(), Token()));

接著,系統會呼叫 ConsumeToken 來處理下一個符記。這個方法使用以 OfKind() 建構的述詞,並呼叫 last_token_ 的種類或子種類。OfKind() 會傳回函式,檢查其輸入是否與指定種類或子種類相符。如果述詞失敗 (在本例中,如果目前的符記不是字串常值),錯誤就會儲存在類別中,而方法會傳回該錯誤。Ok() 呼叫會擷取錯誤,並透過剖析錯誤停止編譯器。如果述詞成功,堆疊中任何未初始化的 SourceElement 起始權杖就會設為目前的符記:

for (auto& scope : active_ast_scopes_) {
    if (scope.start_.kind() == Token::Kind::kNotAToken) {
        scope.start_ = token;
    }
}

在此範例中,啟動符記設為堆疊的頂端元素,因為系統在方法開始時初始化這個符記。接著,Parser 會設定 previous_token_ = last_token_last_token_ = Lex(),導向下一個符記。

最後,系統會使用 scope.GetSourceElement() 傳回產生的 StringLiteral 節點。這項操作會將堆疊頂端 SourceElement 的結束權杖設為 previous_token_,然後傳回 SourceElement。最終節點的起始與結束符記相同,因為 StringLiteral 只有單一符記的長度,但其他方法可能會在呼叫 GetSourceElement() 之前使用許多符記。方法傳回時,ASTScope 的解構函式會彈出 active_ast_scopes_ 的頂端元素。

編譯

此時,編譯器已成功為每個輸入檔案建構 raw::File,然後繼續透過三個步驟將這些原始 AST 編譯為平面 AST:

  1. 使用:原始 AST (其表示法試圖與 FIDL 文法相符) 是以 FIDL 程式庫分組,並將糖分解為平面 AST (其表示法會嘗試與 FIDL 語言語意相符)。這個步驟會將每個檔案的一個 raw::File 轉換為每個程式庫一個 flat::Library
  2. 拓撲排序:決定解析扁平的 AST 節點的順序 (請參閱下一步)。
  3. 解析度:執行名稱解析度、類型解析度,以及計算類型與大小。解析程序會依節點完成,結果相關資訊會儲存在 flat:: 節點本身。

觀看長片

將每個檔案剖析為 raw::File,且為每個程式庫初始化空白的 AST (flat::Library) 後,就必須使用對應 raw::File 的所有資料更新 AST。使用 ConsumeFoo() 方法以遞迴方式完成此操作。每個 ConsumeFoo() 方法通常都會採用對應的原始 AST 節點做為輸入內容,更新 Library 類別,然後傳回 bool 來表示成功或失敗。這些方法負責:

  • 驗證屬性的位置。例如,檢查 Transport 屬性是否只針對通訊協定指定。
  • 檢查是否有任何未定義的程式庫依附元件 (例如,如未匯入 textures 程式庫,則使用 textures.Foo 就會發生錯誤)
  • 將原始 AST 節點轉換為扁平的 AST 節點對等項目,並儲存在 Libraryfoo_declarations_ 屬性中。系統一開始不會設定扁平的 AST 節點值,但這些值會在編譯期間稍後計算。
  • 將各個宣告新增至 declarations_ 向量,以進行註冊。宣告宣告和列舉/位元欄位 (宣告值) 也會新增至 constants_ 向量,而所有其他宣告 (宣告類型) 也會將其對應的類型範本新增至程式庫的類型空間

地形排序

將特定 Library 的所有宣告新增至 declarations_ 向量後,編譯器即可繼續解析每項個別宣告。不過,必須以正確順序執行此操作 (確保宣告的任何依附元件會在此之前解析);如要這麼做,請先將宣告排序為獨立的 declarations_order_ 向量,然後反覆執行此動作,以編譯每個宣告。排序作業是在 SortDeclarations() 方法中完成,並使用 DeclDependencies() 判斷特定宣告的依附元件。

解析度

如果有已排序的宣告,編譯會透過 CompileFoo 方法執行,通常對應至 AST 節點 (例如 CompileStructCompileConst),並以 CompileDecl 做為進入點。CompileDecl 的主要用途為:

完成這個步驟後,flat::Library 會包含產生程式碼的所有必要資訊。FIDL 編譯器可以直接產生 C 繫結,或產生可由獨立後端使用的 JSON IR

其他檢查

將編譯作業標示為成功之前,FIDL 編譯器還會執行幾項額外檢查:例如,它會檢查屬性是否有任何限制,並確實使用所有宣告的程式庫依附元件。

後端產生

FIDL 編譯器會發出 JSON 中繼表示法 (IR)。JSON IR 由另一個名為「後端」的程式使用,該程式會從 JSON IR 產生語言繫結。

官方支援的 FIDL 語言後端如下:

C 繫結

基於歷史因素,系統會直接從 FIDL 編譯器產生 C 繫結:C 繫結不支援搭配 C 繫結使用的所有功能和類型,必須使用 Layout = "Simple" 屬性註解。

C 系列執行階段

fidlc 也會負責產生「程式設計資料表」,這是用於代表執行階段的 FIDL 訊息的 fidl_type_t 例項,供 C 語言系列 (C、LLPP、HLCPP) 的所有繫結使用。為此,固定 AST 會透過 CodedTypesGenerator 轉換為名為「編碼」的 AST 的中繼表示法,藉此疊代指定 flat::Library 中的 flat::Decl,並將每個 raw::Decl 節點轉換為對應的 coded::Type 節點 (例如 flat::Struct 變成 coded::StructType)。

接著,TablesGenerator 會產生程式碼表,進而為每個建構相等 fidl_type_t 類型的 coded::Type 產生 C 程式碼。舉例來說,對於名為 MyStructcoded::StructTypeTablesGenerator 會編寫 C 程式碼,建構對等的 fidl::FidlCodedStruct,如下所示:

const fidl_type_t MyStruct = fidl_type_t(::fidl::FidlCodedStruct(
    my_struct_fields, 1u, 32u, "mylibrary/MyStruct"));

//sdk/fidl 中 FIDL 程式庫的編碼資料表會產生至 fidling/gen/sdk/fidl/LIBRARY/LIBRARY.fidl.tables.c 下的外部目錄中。例如:out/default/fidling/gen/sdk/fuchsia.sys/fuchsia.sys.fidl.tables.cREADME 提供額外的背景資訊。fidl_type_t定義 (例如 FidlCodedStruct) 位於 internal.h 中。

詞彙解釋

刪除

Decl 是所有扁平的 AST 節點的基礎,就像 SourceElement 是所有剖析器樹狀結構節點的基底,對應使用者可在 FIDL 檔案中建立的所有可能宣告。Decl 分為兩種類型:

  • Const,用於宣告值,並有會在編譯期間解析的 value 屬性。
  • TypeDecl,可宣告訊息類型或介面,並擁有可在編譯期間設定的 typeshape 屬性。

代表匯總類型的 TypeDecl (例如結構體、資料表和聯集) 也有靜態的 Shape() 方法,其中包含用來決定該類型 Typeshape 的邏輯。

FieldShape

FieldShape 會說明匯總類型成員 (例如結構體或聯集) 的偏移和邊框間距資訊。一般來說,這些欄位需要 TypeshapeFieldShape

名稱

Name 代表範圍變數名稱,由名稱所屬的程式庫組成 (全域名稱的 nullptr),以及變數名稱本身為字串 (適用於匿名名稱) 或 SourceLocation。在參照已宣告變數欄位 (例如 MyEnum.MyField) 時,Name 也可以選擇性加入 member_name_ 欄位。

SourceElement

SourceElement 代表 fidl 檔案中的程式碼區塊,並由 start_end_ Token 參數化。所有剖析器樹狀結構 (「原始」AST) 節點都繼承自這個類別。

SourceFile

檔案周圍的包裝函式,負責擁有該檔案的資料。另請參閱虛擬 SourceFile

SourceLocation

StringView 和其來源 SourceFile 的包裝函式。提供多種方法,取得 StringView 的周圍線條,以及以 "[filename]:[line]:[col]" 字串形式呈現的位置

SourceManager

一組與單一 Library 相關的 SourceFile 的包裝函式。

權杖

符記本質上是一種偏誤 (以 SourceLocation 的形式儲存成 location_ 屬性),在後續的編譯階段為剖析器提供實用的另外兩項資訊:

  • 一起將狐猴分類的種類和子類別。可能的種類如下:
    • 使用特殊字元,例如 Kind::LeftParenKind::DotKind::Comma 等...
    • 字串和數字常數
    • ID。關鍵字的符記 (例如 conststruct) 會視為 ID,但也會定義子種類,用來識別其所屬的關鍵字 (例如 Subkind::ConstSubkind::Struct)。所有其他符記都有 None 的子種類。
    • 未初始化權杖具有 kNotAToken 類型。

類型

代表型別執行個體的結構。舉例來說,vector<int32>:10? 類型會對應至 VectorType TypeDecl 的執行個體,其中 max_size_ = 10maybe_arg_type 會設為與 int32 對應的 Type。內建類型全都有靜態 Shape() 方法,可根據該類型執行個體的參數傳回 Typeshape。使用者定義的類型 (例如結構或聯集) 都有一種 IdentifierType 類型 - 對應的 TypeDecl,例如 Struct 改為提供靜態的 Shape() 方法。

TypeDecl

查看 Decl

TypeShape

瞭解特定類型的物件在記憶體中的配置方式,包括其大小、對齊方式、深度等。

打字空間

型別空間是 Type 名稱與 TypeTypeTemplate 的對應關係。在編譯期間,類型空間會初始化,以便納入所有內建類型 (例如 "vector" 對應至 VectorTypeTemplate),而使用者定義的類型則會在編譯程序中新增。這也能確保每個類型都有單一類型的範本執行個體,避免類型的名稱衝突或陰影 (例如 https://fxbug.dev/42156366)。

TypeTemplate

TypeTemplates 的執行個體提供 Create() 方法,用於建立特定 Type 的新執行個體,因此每個內建 FIDL 類型 (例如 ArrayTypeTemplatePrimitiveTypeTemplate 等) 都有 TypeTemplate 子類別,且所有使用者定義類型 (TypeDeclTypeTemplate) 和類型別名 (TypeAliasTypeTemplate) 都有一個類別。Create() 會採用可能的類型參數做為參數:引數類型、空值和大小。

舉例來說,如要建立代表 vector<int32>:10? 類型的物件,編譯器會呼叫 VectorTypeTemplateCreate() 方法,引數類型為 int3210 的大小上限為 10,以及 types::Nullability::kNullable 的可為空值性。這個呼叫會傳回含有這些參數的 VectorType 例項。請注意,這 3 個參數不一定適用於所有類型 (例如 int32PrimitiveType 沒有這 3 種參數)。每種類型範本的 Create() 方法會自動檢查是否只傳遞相關的參數。

使用者定義類型的具體類型是 TypeDeclTypeTemplate 產生的 IdentifierType

虛擬 SourceFile

具有假「檔案名稱」的 SourceFile 子類別,在初始化且沒有備份資料的情況下初始化。這會公開 AddLine() 方法,將資料新增至檔案,並做為未直接顯示在任何輸入 SourceFile 中的內容備用 SourceFile,例如產生的匿名 Name