RFC-0061:可擴充的聯集 | |
---|---|
狀態 | 已接受 |
區域 |
|
說明 | 為了提供更多方式來表達酬載,而酬載的形狀可能需要隨著時間演進,我們建議以可擴充的聯集取代現有的聯集。 |
作者 | |
提交日期 (年-月-日) | 2018-09-26 |
審查日期 (年-月-日) | 2018-10-11 |
「Catering to Hawaii and Alaska」
摘要
為了提供更多方式來表達酬載,而其形狀可能需要隨時間演進,我們建議將現有的聯集替換為可擴充的聯集。
提振精神
目前,集合無法提供任何方法,讓集合能夠隨時間演進,我們甚至會警告「一般來說,變更集合的定義會破壞二進位相容性」。
目前定義了許多需要可擴充性的聯集,例如 fuchsia.modular/TriggerCondition (其中欄位已淘汰但未移除),或 fuchsia.modular/Interaction。
如後文所述,許多聯合體目前的表示方式都很適當,因為這些聯合體不太可能在近期演進。不過,同時保留 static unions
和 extensible unions
會導致不必要的複雜性,請參閱優缺點。
設計
如要引入可擴充的聯集,我們需要修改 FIDL 的多個部分:語言和 fidlc
、JSON IR、線路格式和所有語言繫結。我們也需要在各處記錄這項新功能。
我們將逐一討論每項變更。
語言
在語法上,可擴充的聯集與靜態聯集完全相同:
union MyExtensibleUnion {
Type1 field1;
Type2 field2;
...
TypeN fieldN;
}
在幕後,每個欄位都會指派一個序數:這與資料表為每個欄位指派序數,以及方法的序數如何自動指派的情況相似。
具體違規事項如下:
- 系統會使用與方法序號相同的演算法計算序號(詳細資料),我們會連結程式庫名稱「
.
」、可擴充的聯集名稱「/
」和最後的會員名稱,然後取得 SHA256,並以0x7fffffff
遮罩。 - 序號為
uint32
,兩個欄位不得使用相同的序號,且我們不允許使用0
。在序數衝突的情況下,應使用[Selector]
屬性提供替代名稱 (或重新命名的成員)。 - 序數可以是稀疏,也就是不像資料表那樣需要密集的序數。
- 在可擴充的聯結上,不允許使用可為空值的欄位。
- 可擴充的聯集必須至少包含一個成員。
可擴充的聯集可用於語言中目前可使用聯集的任何位置。特別是:
- 結構體、表格和可擴充的聯集可包含可擴充的聯集。
- 可擴充的聯集可包含結構體、表格和可擴充的聯集。
- 介面引數或傳回值可以是可擴充的聯集;
- 可擴充的聯集可為空值。
JSON IR
在下列表格中,我們會在每個聯集欄位宣告中新增一個鍵「ordinal」。
線路格式
在網路上,可擴充的聯集會以序號表示,用於區分選項 (填入 8 個位元組),後面接著產生者所知的各種成員的封包。具體來說,這包括:
uint32
標記:包含所要編碼成員的序號。uint32
邊框間距,以對齊 8 個位元組;uint32
num_bytes 會儲存信封中的位元組數量,一律為 8 的倍數,如果信封為空值,則必須為 0;uint32
num_handles 會儲存信封中的句柄數量,如果信封為空值,則必須為 0;uint64
資料指標,用於指出離線資料是否存在:0
當信封為空值時;- 當信封存在且下一個離線物件時,則為 FIDL_ALLOC_PRESENT (或 UINTPTR_MAX);
- 在解碼供使用時,如果封套為空值,這個 data 指標會是 nullptr,否則會是封套的有效指標。
- 信封會為內容後面的句柄保留儲存空間。
可為空的擴充可結合式具有 標記 0、num_bytes 設為 0、num_handles 設為 0,以及資料指標為 FIDL_ALLOC_ABSENT,也就是0。基本上,空值可延伸的聯集是 24 個位元組的 0。
語言繫結
可擴充的聯合體與聯合體類似,但在讀取聯合體時,也需要處理「unknown」情況。理想情況下,大多數語言繫結會處理
union Name { Type1 field1; ...; TypeN fieldN; };
這類函式會是可擴充的聯集,因此程式碼可以輕鬆從一個切換到另一個,並支援未知情況的模塊,這只有在可擴充的聯集情況下才有意義。
首先,我們建議不要在語言繫結中公開保留成員:雖然這些成員會出現在 JSON IR 中以確保完整性,但我們不建議在語言繫結中公開這些成員。
導入策略
實作方式分為兩個步驟。
首先,我們會為可擴充的聯集建立支援功能:
- 使用不同的關鍵字 (
xunion
) 來區分靜態聯集和可擴充聯集,在語言 (fidlc
) 中引入這項功能。 - 實作各種核心語言繫結 (C、C++、Rust、Go、Dart)。並據此擴充相容性測試和其他測試。
其次,我們會將所有靜態聯集遷移至可擴充聯集:
為靜態聯集產生序數,並將其放入 JSON IR。後端應一開始忽略這些序數。
在讀取路徑中,同時提供兩種讀取聯集的方式,就像是靜態聯集和可擴充聯集 (需要序號才能實現)。請根據交易訊息標頭中的標記,選擇要使用哪一個。
更新寫入路徑,將聯集編碼為可擴充的聯集,並在交易訊息標頭中設定標記,以便指出這項情況。
所有寫入器都已更新、部署及傳播後,請移除靜態聯集處理和軟轉換的暫架程式碼。
說明文件和範例
您至少需要在下列位置提供文件:
回溯相容性
可擴充的聯集「不會」與「靜態」聯集相容。
成效
未使用時不會影響效能。建構期間對效能影響微乎其微。
安全性
不會影響安全性。
測試
編譯器中的單元測試、各種語言繫結的編碼/解碼單元測試,以及用於檢查各種語言繫結的相容性測試。
缺點、替代方案和未知事項
可擴充的聯集效率較低。此外,非可擴充的聯集無法透過該語言的其他方式表達。因此,我們建議同時保留這兩項功能。
不過,我們可以決定只保留可擴充的聯集,並移除目前定義的聯集。這會違反 Fuchsia 中的各種規定,其中聯集代表效能重要訊息,且擴充預期很少,例如 fuchsia.io/NodeInfo
、fuchsia.net/IpAddress
。
保留靜態聯集的優缺點
優點
- 相較於集合,可擴充的集合會產生 8 個位元組的成本 (針對信封大小和句柄數量)。此外,可擴充的聯集資料一律會離線儲存 (即資料指標的額外 8 個位元組),而只有可為空的聯集資料會離線儲存。
- 由於組合的編碼方式,無法使用 FIDL 中的其他原始類型來表示組合。因此,如果從語言中移除這些字詞,某些類型的訊息就無法以簡潔有效率的方式表達。
- 在某些情況下,視用途而定,集合可以以不同方式有效呈現;但這只是例外狀況,並非常態。fuchsia.net.stack/InterfaceAddressChangeEvent 是其中一個可在不使用聯集的情況下重寫的例子,這個例子只會用於 fuchsia.net.stack/InterfaceAddressChange,因為在該例子中,InterfaceAddress 可以直接寫入,並使用
enum
指出是否已新增或移除。
缺點
- 同時保留靜態聯集和可擴充的聯集,會使編譯器、JSON IR、所有後端以及編碼/解碼的複雜度增加。效益極小:在 FIDL 編碼一開始就不是特別節省空間的情況下,大小差異微乎其微。此外,如有需要,可在原地對可延伸的聯集進行解碼。
- 以下是 fuchsia.io/NodeInfo 的分析結果,可作為效益微小程度的例子:
- 目前 NodeInfo 有 6 個選項:服務 (大小 1)、檔案 (大小 4)、目錄 (大小 1)、管道 (大小 4)、vmofile (大小 24)、裝置 (大小 4)。
- 因此,NodeInfo 的總大小一律為 32 個位元組,即 tag + max(options size) = 8 + 24 = 32。
- 使用可擴充的聯集時,NodeInfo 大小取決於要編碼的選項。總會有 16 個位元組的「稅金」(相較於 8 個),因此各自的大小會是:service = 24、file = 24、directory = 24、pipe = 24、vmofile = 40、device = 24。
- 因此,在所有情況下,我們都會減少 8 個位元組,但在 vmofile 的情況下,我們會額外增加 8 個位元組。
- 在語言中同時具備靜態聯集和可擴充聯集的複雜性也是令人擔心的地方。我們預期程式庫作者會在使用這兩種方法之間猶豫不決,因為選擇可擴充的聯集是長期而言更安全的選擇,而且成本極低。
總而言之,我們決定以可擴充的聯集取代靜態聯集。
標記與序數
我們使用序數來表示指派給欄位的內部數值,也就是透過雜湊運算所計算的值。我們使用標記來表示繫結中的變化版本:在 Go 中,這可能是 alias
類型的常數,在 Dart 中則可能是 enum
。
fidlc
編譯器只會處理序數。開發人員大多只會處理標記。而繫結則可將高層級標記轉譯為低層級內部序號。
禁止使用空的擴充可選項
在設計階段,我們考慮讓可擴充的聯集為空。不過,我們最後決定不允許這麼做:選擇可為空的擴充可為空的聯集,並使用單一變數 (例如空結構體) 清楚模擬意圖。這也避免了可擴充的聯集有兩個「單位」值,也就是空值和空白值。