本文件會概略說明 FIDL 編譯器 fidlc
的內部運作。
fidlc
是會接收多個 FIDL 檔案的指令列工具,並會輸出 FIDL JSON IR。
總覽
編譯器的主要輸入內容是 --files
旗標的引數,用於描述按照程式庫分組的檔案清單。編譯器會個別剖析每個檔案,以取得每個檔案的剖析樹狀結構:
- 每個檔案都會載入至
SourceFile
,擁有備份檔案的緩衝區 - 編譯器會初始化
Lexer
,後者會將SourceFile
做為引數傳送至其建構函式。此類別公開Lex()
方法,此方法會傳回檔案中的下一個Token
;您可以重複呼叫此方法,取得檔案中的Token
序列。 - 編譯器使用
Lexer
初始化Parser
,然後呼叫Parse()
方法,建構剖析樹狀結構。在程式碼中,剖析樹狀結構是原始 AST。這個函式會傳回raw::File
,這是代表原始 AST 根節點的類別。 - 編譯器將每個檔案剖析成剖析樹狀結構後,就會針對每個程式庫,將剖析樹狀結構分組為單一 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
欄位。
- 針對每個程式庫,編譯器會掃遍每個程式庫對應的
- 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()
方法,此方法會使用 Lexer
的 Token
,並將 unique_ptr
傳回該節點的執行個體。如果失敗,系統會傳回 nullptr
。
Parser
會追蹤以下項目:
- 目前使用
SourceElements
堆疊建構的節點,這些堆疊儲存在active_ast_scopes_
中。 - 正在處理中的當前和先前
Token
。last_token_
和previous_token_
。 - 其在
Parser::Lex
方法中提供了狀態機器。目前的權杖 (last_token_
) 一律會是下一個即將使用的權杖,這會有效為剖析器提供一個權杖先行 (即 LL(1))。
Parser
會根據 last_token_
的 Token::Kind
(使用 Peek()
方法),決定目前 Token
所屬的節點類型。接著,Parser
會更新狀態,並使用 ASTScope
類別以及 ConsumeToken
和 MaybeConsumeToken
輔助方法來建構節點。
這個範例逐行顯示簡易的非週期性案例。剖析器方法如下所示:
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:
- 使用:原始 AST (其表示法試圖與 FIDL 文法相符) 是以 FIDL 程式庫分組,並將糖分解為平面 AST (其表示法會嘗試與 FIDL 語言語意相符)。這個步驟會將每個檔案的一個
raw::File
轉換為每個程式庫一個flat::Library
。 - 拓撲排序:決定解析扁平的 AST 節點的順序 (請參閱下一步)。
- 解析度:執行名稱解析度、類型解析度,以及計算類型與大小。解析程序會依節點完成,結果相關資訊會儲存在
flat::
節點本身。
觀看長片
將每個檔案剖析為 raw::File
,且為每個程式庫初始化空白的 AST (flat::Library
) 後,就必須使用對應 raw::File
的所有資料更新 AST。使用 ConsumeFoo()
方法以遞迴方式完成此操作。每個 ConsumeFoo()
方法通常都會採用對應的原始 AST 節點做為輸入內容,更新 Library
類別,然後傳回 bool
來表示成功或失敗。這些方法負責:
- 驗證屬性的位置。例如,檢查
Transport
屬性是否只針對通訊協定指定。 - 檢查是否有任何未定義的程式庫依附元件 (例如,如未匯入
textures
程式庫,則使用textures.Foo
就會發生錯誤) - 將原始 AST 節點轉換為扁平的 AST 節點對等項目,並儲存在
Library
的foo_declarations_
屬性中。系統一開始不會設定扁平的 AST 節點值,但這些值會在編譯期間稍後計算。 - 將各個宣告新增至
declarations_
向量,以進行註冊。宣告宣告和列舉/位元欄位 (宣告值) 也會新增至constants_
向量,而所有其他宣告 (宣告類型) 也會將其對應的類型範本新增至程式庫的類型空間。
地形排序
將特定 Library
的所有宣告新增至 declarations_
向量後,編譯器即可繼續解析每項個別宣告。不過,必須以正確順序執行此操作 (確保宣告的任何依附元件會在此之前解析);如要這麼做,請先將宣告排序為獨立的 declarations_order_
向量,然後反覆執行此動作,以編譯每個宣告。排序作業是在 SortDeclarations()
方法中完成,並使用 DeclDependencies()
判斷特定宣告的依附元件。
解析度
如果有已排序的宣告,編譯會透過 CompileFoo
方法執行,通常對應至 AST 節點 (例如 CompileStruct
、CompileConst
),並以 CompileDecl
做為進入點。CompileDecl
的主要用途為:
完成這個步驟後,flat::Library
會包含產生程式碼的所有必要資訊。FIDL 編譯器可以直接產生 C 繫結,或產生可由獨立後端使用的 JSON IR
其他檢查
將編譯作業標示為成功之前,FIDL 編譯器還會執行幾項額外檢查:例如,它會檢查屬性是否有任何限制,並確實使用所有宣告的程式庫依附元件。
後端產生
FIDL 編譯器會發出 JSON 中繼表示法 (IR)。JSON IR 由另一個名為「後端」的程式使用,該程式會從 JSON IR 產生語言繫結。
官方支援的 FIDL 語言後端如下:
- C++:
- 高層級:fidlgen_hlcpp
- 低層級:fidlgen_cpp
- 統合:fidlgen_cpp
- Go:fidlgen_go
- Rust:fidlgen_rust
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 程式碼。舉例來說,對於名為 MyStruct
的 coded::StructType
,TablesGenerator
會編寫 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.c
。README 提供額外的背景資訊。fidl_type_t
定義 (例如 FidlCodedStruct
) 位於 internal.h 中。
詞彙解釋
刪除
Decl
是所有扁平的 AST 節點的基礎,就像 SourceElement
是所有剖析器樹狀結構節點的基底,對應使用者可在 FIDL 檔案中建立的所有可能宣告。Decl
分為兩種類型:
Const
,用於宣告值,並有會在編譯期間解析的value
屬性。TypeDecl
,可宣告訊息類型或介面,並擁有可在編譯期間設定的typeshape
屬性。
代表匯總類型的 TypeDecl
(例如結構體、資料表和聯集) 也有靜態的 Shape()
方法,其中包含用來決定該類型 Typeshape
的邏輯。
FieldShape
FieldShape
會說明匯總類型成員 (例如結構體或聯集) 的偏移和邊框間距資訊。一般來說,這些欄位需要 Typeshape
和 FieldShape
名稱
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::LeftParen
、Kind::Dot
、Kind::Comma
等... - 字串和數字常數
- ID。關鍵字的符記 (例如
const
、struct
) 會視為 ID,但也會定義子種類,用來識別其所屬的關鍵字 (例如Subkind::Const
、Subkind::Struct
)。所有其他符記都有None
的子種類。 - 未初始化權杖具有
kNotAToken
類型。
- 使用特殊字元,例如
類型
代表型別執行個體的結構。舉例來說,vector<int32>:10?
類型會對應至 VectorType
TypeDecl
的執行個體,其中 max_size_ = 10
和 maybe_arg_type
會設為與 int32
對應的 Type
。內建類型全都有靜態 Shape()
方法,可根據該類型執行個體的參數傳回 Typeshape
。使用者定義的類型 (例如結構或聯集) 都有一種 IdentifierType
類型 - 對應的 TypeDecl
,例如 Struct
改為提供靜態的 Shape()
方法。
TypeDecl
查看 Decl
TypeShape
瞭解特定類型的物件在記憶體中的配置方式,包括其大小、對齊方式、深度等。
打字空間
型別空間是 Type
名稱與 Type
的 TypeTemplate
的對應關係。在編譯期間,類型空間會初始化,以便納入所有內建類型 (例如 "vector"
對應至 VectorTypeTemplate
),而使用者定義的類型則會在編譯程序中新增。這也能確保每個類型都有單一類型的範本執行個體,避免類型的名稱衝突或陰影 (例如 https://fxbug.dev/42156366)。
TypeTemplate
TypeTemplates 的執行個體提供 Create()
方法,用於建立特定 Type
的新執行個體,因此每個內建 FIDL 類型 (例如 ArrayTypeTemplate
、PrimitiveTypeTemplate
等) 都有 TypeTemplate 子類別,且所有使用者定義類型 (TypeDeclTypeTemplate
) 和類型別名 (TypeAliasTypeTemplate
) 都有一個類別。Create()
會採用可能的類型參數做為參數:引數類型、空值和大小。
舉例來說,如要建立代表 vector<int32>:10?
類型的物件,編譯器會呼叫 VectorTypeTemplate
的 Create()
方法,引數類型為 int32
、10
的大小上限為 10
,以及 types::Nullability::kNullable
的可為空值性。這個呼叫會傳回含有這些參數的 VectorType
例項。請注意,這 3 個參數不一定適用於所有類型 (例如 int32
等 PrimitiveType
沒有這 3 種參數)。每種類型範本的 Create()
方法會自動檢查是否只傳遞相關的參數。
使用者定義類型的具體類型是 TypeDeclTypeTemplate
產生的 IdentifierType
。
虛擬 SourceFile
具有假「檔案名稱」的 SourceFile
子類別,在初始化且沒有備份資料的情況下初始化。這會公開 AddLine()
方法,將資料新增至檔案,並做為未直接顯示在任何輸入 SourceFile
中的內容備用 SourceFile
,例如產生的匿名 Name
。