本文件將概略說明 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_ 指標
就是正在處理的語系開頭每次呼叫 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和先前的Token。last_token_和previous_token_, 。 - 在
Parser::Lex方法中擁有狀態機器。目前的權杖 (last_token_) 是 那就是下一個即將使用的符記,能有效為剖析器提供一個 預先檢查符記 (即 LL(1))。
Parser 會根據 Token::Kind 決定目前 Token 所屬的節點類型
使用 Peek() 方法建立 last_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) 後
針對每個程式庫初始化,AST 需使用 raw::File 的所有資料更新
也就是三個星球請使用 ConsumeFoo() 方法遞迴執行。每項
ConsumeFoo() 方法通常會使用對應的原始 AST 節點做為輸入,更新
Library 類別,然後傳回 bool 表示成功或失敗。這些方法是
負責以下事項:
- 驗證屬性的位置;例如,檢查
Transport屬性 僅適用於通訊協定。 - 檢查任何未定義的程式庫依附元件 (例如,使用
textures.Foo會在textures程式庫未匯入) - 將原始 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 為 名為 back-end 的獨立程式,用於產生語言繫結 例如 JSON IR。
官方支援的 FIDL 語言後端為:
- C++:
- 最高層級:fidlgen_hlcpp
- 低層級:fidlgen_cpp
- 統一:fidlgen_cpp
- Go:fidlgen_go
- Rust:fidlgen_rust
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 呼叫 MyStruct,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 會針對匯總類型成員 (例如
struct 或 union。一般來說,這些欄位需要 Typeshape 和 FieldShape
名稱
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::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 已設為
Type 對應 int32。所有內建類型都具有靜態 Shape() 方法,
傳回 Typeshape,且該類型例項的參數。使用者定義的類型 (例如
結構體或聯集) 都會有 IdentifierType 的類型,也就是
和 Struct 一樣,TypeDecl 會提供靜態 Shape() 方法。
TypeDecl
查看 Decl
TypeShape
關於類型物件在記憶體中的配置方式的資訊,包括其大小 校正、深度等
類型空間
類型空間是從 Type 名稱到該 Type 的 TypeTemplate 的對應。過程中
編譯後,型別空間會初始化,納入所有內建型別 (例如 "vector"
對應至 VectorTypeTemplate),但在編譯過程中,則會新增使用者定義的類型。
這也能確保每種類型都有單一類型範本執行個體,並避免名稱
類型的衝突/陰影 (例如 https://fxbug.dev/42156366)。
TypeTemplate
TypeTemplates 的執行個體提供 Create() 方法,用於建立特定
Type - 因此,每種內建 FIDL 類型都有一個 TypeTemplate 子類別 (例如
ArrayTypeTemplate、PrimitiveTypeTemplate 等),以及所有使用者定義的單一類別
type (TypeDeclTypeTemplate),另一個用於類型別名 (TypeAliasTypeTemplate)。Create()
使用參數做為可能的參數:引數類型、是否可為空值以及大小。
舉例來說,如要建立物件,代表編譯器會呼叫的 vector<int32>:10? 類型
為 VectorTypeTemplate 的 Create() 方法,引數類型為 int32,大小上限為
10,以及 types::Nullability::kNullable 的是否可為空值。這個呼叫會傳回
VectorType 取代為這些參數。請注意,這 3 個參數不一定適用於所有
例如 PrimitiveType,例如 int32 不代表這 3 種類型。類型的 Create() 方法
每個類型的範本,都會自動檢查是否只傳遞相關參數。
使用者定義類型的具體類型為 IdentifierType,也就是
TypeDeclTypeTemplate。
虛擬來源檔案
具有假「檔案名稱」的 SourceFile 子類別並在沒有備份資料的情況下初始化這項服務
公開 AddLine() 方法,可將資料新增至檔案,且做為備份資料的 SourceFile
不直接在任何輸入 SourceFile 中出現的內容,例如系統產生的
匿名 Name。