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::Structraw::Constant 會變為 flat::Constant。然後,轉換後的 flat:: AST 節點就會儲存在單一 flat::Library。這些扁平的 AST 節點一開始包含與原始 AST 相同的資訊 但包含 value 欄位和 typeshape 欄位等欄位 針對之後在編譯步驟中設定的類型
  5. AST 完全初始化後,編譯器會評估常數,然後判斷 所宣告型別的記憶體對齊和大小資訊。
,瞭解如何調查及移除這項存取權。

最後,我們得出一個固定的 AST 供處理,可用於將後端產生到 C 繫結至 JSON IR。

萊克斯

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

這個範例說明在短 FIDL 程式碼片段呼叫 Lex() 期間,指標的變化方式 const bool flag = true;:

容錯 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 的目標是將 Token 資料流轉換為剖析樹狀結構 (raw:: AST),並使用 Parse() 方法。系統會在 FIDL 檔案中重複呼叫 Lex,藉此產生 Token 串流。這個 剖析器是以遞迴方式實作 descent原始 AST 的每個節點都有 對應的 ParseFoo() 方法會使用 Lexer 中的 Token,並傳回 unique_ptr 變更為該節點的執行個體如果失敗,會傳回 nullptr

Parser 會追蹤以下項目:

  • 目前透過 SourceElements 堆疊建構的節點,這些節點 儲存在「active_ast_scopes_」中。
  • 目前正在處理的 Token 和先前的 Tokenlast_token_previous_token_, 。
  • Parser::Lex 方法中擁有狀態機器。目前的權杖 (last_token_) 是 那就是下一個即將使用的符記,能有效為剖析器提供一個 預先檢查符記 (即 LL(1))。

Parser 會根據 Token::Kind 決定目前 Token 所屬的節點類型 使用 Peek() 方法建立 last_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) 後 針對每個程式庫初始化,AST 需使用 raw::File 的所有資料更新 也就是三個星球請使用 ConsumeFoo() 方法遞迴執行。每項 ConsumeFoo() 方法通常會使用對應的原始 AST 節點做為輸入,更新 Library 類別,然後傳回 bool 表示成功或失敗。這些方法是 負責以下事項:

  • 驗證屬性的位置;例如,檢查 Transport 屬性 僅適用於通訊協定。
  • 檢查任何未定義的程式庫依附元件 (例如,使用 textures.Foo 會在 textures 程式庫未匯入)
  • 將原始 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 為 名為 back-end 的獨立程式,用於產生語言繫結 例如 JSON IR。

官方支援的 FIDL 語言後端為:

C 繫結

基於過往原因,C 繫結是直接透過 FIDL 編譯器 (C 繫結) 產生 不支援與 C 繫結搭配使用的所有功能和類型都需要加上 Layout = "Simple" 屬性。

C 系列執行階段

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

接著 TablesGenerator 會產生程式設計資料表,然後為每個模型產生 C 程式碼 用於建構對等 fidl_type_t 類型的 coded::Type。舉例來說 coded::StructType 呼叫 MyStructTablesGenerator 會寫出 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 會針對匯總類型成員 (例如 struct 或 union。一般來說,這些欄位需要 TypeshapeFieldShape

名稱

Name 代表範圍變數名稱,由名稱所屬的程式庫組成 (或 nullptr 代表全域名稱),而變數名稱則是字串本身 (如果是匿名名稱),也可以是 SourceLocation。參照Namemember_name_ 所宣告變數的欄位 (例如 MyEnum.MyField)。

SourceElement

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

SourceFile

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

SourceLocation

StringView 及其來源 SourceFile 的包裝函式。它提供了讓 StringView 的周圍線條,以及其位置 (格式為 "[filename]:[line]:[col]" 字串

SourceManager

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

憑證

符記基本上是 LEXE (格式為 SourceLocation), location_ 屬性,可加入其他兩段資訊,對剖析器相當實用 就能進行下列操作:

  • 將韻律分類的種類與子種類。可能的種類如下:
    • 特殊字元 (例如 Kind::LeftParenKind::DotKind::Comma 等)...
    • 字串和數字常數
    • ID。關鍵字的符記 (例如 conststruct) 會視為 ID,但 也定義了子種類,以識別該關鍵字 (例如 Subkind::ConstSubkind::Struct)。所有其他權杖的子類型都是 None
    • 未初始化的符記的種類為 kNotAToken

類型

代表類型執行個體的結構。舉例來說,vector<int32>:10? 類型會對應 設為 VectorType TypeDecl 的例項,其中 max_size_ = 10maybe_arg_type 已設為 Type 對應 int32。所有內建類型都具有靜態 Shape() 方法, 傳回 Typeshape,且該類型例項的參數。使用者定義的類型 (例如 結構體或聯集) 都會有 IdentifierType 的類型,也就是 和 Struct 一樣,TypeDecl 會提供靜態 Shape() 方法。

TypeDecl

查看 Decl

TypeShape

關於類型物件在記憶體中的配置方式的資訊,包括其大小 校正、深度等

類型空間

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

TypeTemplate

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

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

使用者定義類型的具體類型為 IdentifierType,也就是 TypeDeclTypeTemplate

虛擬來源檔案

具有假「檔案名稱」的 SourceFile 子類別並在沒有備份資料的情況下初始化這項服務 公開 AddLine() 方法,可將資料新增至檔案,且做為備份資料的 SourceFile 不直接在任何輸入 SourceFile 中出現的內容,例如系統產生的 匿名 Name