| RFC-0047:表格 | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 在 FIDL 語言中新增機制,以支援向前和向後相容的複合資料型別。 |
| 作者 | |
| 提交日期 (年-月-日) | 2018-07-27 |
| 審查日期 (年-月-日) | 2018-09-20 |
摘要
在 FIDL 語言中新增機制,以支援向前和向後相容的複合資料型別。
與其他 RFC 的關係
這項 RFC 後來經過以下修訂:
提振精神
FIDL 結構體無法提供隨時間變更結構定義的機制。表格與結構體類似,但會為每個欄位新增序數,以利結構演變:
- 現有程式碼可以新增及忽略欄位
- 新版程式碼可以略過舊 (已淘汰) 欄位
資料表必然比結構體複雜,因此處理速度較慢,序列化時也會使用更多空間。因此,建議保留結構體,並導入新項目。
此外,可演進的結構定義可讓 FIDL 變體合理地序列化至磁碟或網路。
範例表格可能如下所示:
table Station {
1: string name;
3: bool encrypted;
2: uint32 channel;
};
設計
原文語言
將 table_declaration 新增至 FIDL 文法:
declaration = const-declaration | enum-declaration | interface-declaration |
struct-declaration | union-declaration | table-declaration ;
table-declaration = ( attribute-list ) , "table" , IDENTIFIER , "{" , ( table-field , ";" )* , "}" ;
table-field = table-field-ordinal , table-field-declaration ;
table-field-ordinal = ordinal , ":" ;
table-field-declaration = struct-field | "reserved" ;
注意:
- 序數必須從 1 開始,且序數空間不得有間隙 (如果最大序數為 7,則必須存在 1、2、3、4、5、6、7)。
- 兩個欄位不得使用相同的序數。
- 編譯器會在檢查序數衝突後,捨棄「保留」欄位。 這項功能可標註欄位曾用於某個舊版表格,但已遭捨棄,因此日後修訂時不會意外重複使用該序數。
- 資料表不得包含可為空值的欄位。
目前語言中可使用結構體的任何位置,都可以使用表格。 特別是:
- struct 和聯集可包含表格
- 表格可包含結構體和聯集
- 介面引數可以是資料表
- 表格可設為選填
Wire 格式
表格會儲存為封裝的 vector<envelope>,向量的每個元素都是一個序數元素 (因此索引 0 是序數 1,索引 1 是序數 2,依此類推)。信封的說明如下。
資料表只能儲存信封,最多到最後一個信封,也就是最大集合序數。這可確保標準表示法。舉例來說,如果未設定任何欄位,正確的編碼就是空向量。如果資料表在序數 5 有欄位,但只設定到序數 3 的欄位,正確的編碼是 3 個封包的向量。
信封
envelope 會儲存變數大小、未解譯的非行內酬載。酬載可包含任意數量的位元組和控制代碼。
這個機構允許將一個 FIDL 訊息封裝在另一個訊息內。
信封會儲存為記錄,內含以下資訊:
num_bytes:信封中的位元組數 (32 位元不帶正負號),一律為 8 的倍數,如果信封為空值,則必須為零num_handles:信封中的控制代碼數量 (32 位元不帶正負號的數字),如果信封為空值,則必須為零data:64 位元存在指標或指向行外資料的指標
data 欄位有兩種不同的行為。
編碼以供轉移時,data 表示內容存在:
FIDL_ALLOC_ABSENT(所有位元皆為 0):信封為空值FIDL_ALLOC_PRESENT(所有 1 位元):信封為非空值,資料是下一個行外物件
解碼供使用時,data 是指向內容的指標。
0:信封為空值<valid pointer>:信封不得為空值,資料位於指定記憶體位址
如果是控制代碼,封包會為內容後方的控制代碼保留儲存空間。
解碼後,假設 data 不是空值,data 會指向資料的第一個位元組。
信封會填補至下一個 8 位元組物件對齊位置 (實際上表示沒有額外填補)。
語言繫結
資料表不會產生 struct 等資料欄位,而是會為每個欄位產生一組方法。舉例來說,在 C++ 中,我們會使用:
class SampleTable {
public:
// For "1: int32 foo;"
const int32* foo(); // getter, returns nullptr if foo not present
bool has_foo(); // presence check
int32* mutable_foo(); // mutable getter, forces a default value if not set
void set_foo(int32 x); // set value
void clear_foo(); // remove from structure
optional<int32> take_foo(); // get foo if present, remove from structure
};
樣式指南
我應該使用結構體還是表格?
結構體和表格提供語意相似的概念,因此決定偏好哪一個可能很複雜。
如要使用非常高階的 IPC,或是用於持續性儲存空間 (序列化效能通常不是問題):
- 表格提供部分向前和向後相容性,因此可確保日後適用:建議您針對大多數概念使用表格。
- 只有在概念不太可能在未來發生變化時 (例如
struct Vec3 { float x; float y; float z }或Ipv4Address),才可採用結構體的效能優勢。
一旦序列化效能成為首要考量 (例如,裝置驅動程式的資料路徑上常見這種情況),我們就可以開始只偏好結構體,並依賴在介面中新增方法來因應未來的變更。
回溯相容性
這項變更會導入兩個關鍵字:table 和 reserved。
不必擔心回溯相容性問題。
效能
這項功能為選用功能,如未使用,應不會影響 IPC 效能。 我們預期建構效能差異會落在可測量的雜訊範圍內。
安全性
不會影響安全性。
測試
您需要為每個語言繫結進行額外測試,以及 fidlc 的測試。
使用資料表的擴充版 Echo 套件會比較合適。
加入表格編碼/解碼的模糊測試工具會很有幫助,因為剖析時總是會遇到棘手的情況。
缺點、替代方案和未知事項
這個空間有兩個大問題需要回答:
- 用於欄位識別的序數與字串 (序數會強制傳送結構定義)
- 如果是序數:每則訊息的稀疏與密集序數空間
以聯集向量表示的表格
有人提議將 table 視為 vector<union>。
這會帶來兩個問題:
- 這個格式的讀取器實作效率最高,但仍不如建議的表格格式,因此我們永久限制了尖峰效能。
- 但無法保證與任何電線相容!向量必須攜帶長度和主體,因此根據這項提案,聯合體永遠無法在網路上轉換為表格 (而且我們想要進行這項轉換的次數似乎很少)。
相反地,透過導入信封基本型別,我們可以以相同方式撰寫及推論相容性保證... 並在表格和可擴充聯集 (開發中) 之間分享一些棘手的實作詳細資料,並以近乎免費的方式,在語言中公開實用的基本型別。
序數與字串
使用序數時,編譯期間必須有結構定義,但可提高實作效率 (字串處理速度一律比整數處理慢)。由於 FIDL 在編譯期間已需要結構定義,因此希望這裡的序數超過字串不會引起爭議。
密集與稀疏封裝
有關密集與稀疏序數空間的問題,可能較具爭議性。 現有做法分為兩大陣營:
- Thrift 和 Protobuf 使用稀疏序數空間,因此欄位可獲得任何序數值。
- FlatBuffers 和 Cap'n'Proto 使用密集序數空間,因此欄位必須提供連續序數。
如果 Protobuf 傳輸格式搭配的典型 Protobuf 實作會剖析成固定大小的結構體,就會發生錯誤,導致解碼記憶體使用的記憶體量與透過線路傳輸的位元組數無關。如要查看這項資訊,請想像一則訊息包含 10000 個 (選用) int64 欄位。傳送者可以選擇只傳送一個,這樣一來,訊息在網路上只會佔用幾個位元組,但在記憶體中卻會佔用近 100 KB。如果以 RPC 形式傳送大量這類訊息,通常很容易就能阻撓流量控管實作,並導致 OOM。
如要採用稀疏序數的替代實作策略 (如先前對話中所述),請傳送 (ordinal, value) 元組的排序陣列。選擇就地解碼的實作項目必須依賴透過資料進行的二進位搜尋,才能找到序數。這可避免先前所述的流量控制錯誤,但由於我們可能會執行大量二元搜尋,因此在執行階段可能會造成一些效率不彰的問題。
Cap'n'Proto 實作了非常複雜的序數處理演算法,由於我們想避免這種複雜性,因此在此不進一步討論。
FlatBuffers 的連線格式與本文建議的格式非常相似:利用其密集序數空間提供單一陣列查閱,以找出欄位的資料 (或該欄位為空值)。
既有技術和參考資料
- FlatBuffers 演算法與這個演算法類似,但已在此處經過調整,以更符合 FIDL 慣例。
- 我們認為 Protobuf 最初普及了序數/值表示法,而 Google 大規模使用這項表示法,也證明瞭這項架構多年來的穩健性。
- Cap'n'Proto 和 Thrift 則分別提供上述內容的微幅變化。