RFC-0047:表格

RFC-0047:表格
狀態已接受
區域
  • FIDL
說明

在 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),才可採用結構體的效能優勢。

一旦序列化效能成為首要考量 (例如,裝置驅動程式的資料路徑上常見這種情況),我們就可以開始只偏好結構體,並依賴在介面中新增方法來因應未來的變更。

回溯相容性

這項變更會導入兩個關鍵字:tablereserved。 不必擔心回溯相容性問題。

效能

這項功能為選用功能,如未使用,應不會影響 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 則分別提供上述內容的微幅變化。