本文件將概略說明 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
。參照Name
member_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
。