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)。
  • 兩個欄位不得使用相同的序數。
  • 編譯器會在檢查序數衝突後,捨棄「reserved」欄位。這可讓您在註解中指出某個欄位曾在某個舊版資料表中使用,但已遭到刪除,以免日後的修訂版本不小心重複使用該序號。
  • 資料表不允許使用可空欄位。

表格可用於目前可在該語言中使用結構體的任何位置。特別是:

  • 結構體和聯集可包含表格
  • 表格可包含結構體和聯集
  • 介面引數可以是資料表
  • 可將表格設為選用

線路格式

表格會以填入的 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 的測試。

使用擴充版的回音測試套件來使用表格會比較適合。

為表格編碼/解碼作業新增模糊測試器會很有幫助,因為在剖析過程中,總會遇到棘手的情況。

缺點、替代方案和未知事項

這個空間有兩個重大問題需要回答:

  • 欄位識別的序數與字串 (序數會強制傳送結構定義)
  • If ordinals: sparse vs dense ordinal spaces per message

表格做為聯集的向量

我們建議將 table 視為 vector<union>。這會導致兩個問題:

  • 這種格式的讀取器最有效率的實作方式,必須比建議的資料表格式讀取器最有效率的實作方式效率低,因此我們會永久限制最高效能。
  • 但不保證線路相容性!向量必須攜帶長度和內容,因此在傳輸時,此提案永遠無法將聯集轉換為資料表 (而且我們希望進行此轉換的次數似乎很少)。

相反地,透過引入封套原始類型,我們可以以相同方式記錄及推論相容性保證...我們可以將一些棘手的實作細節,在表格和可擴充的聯集 (正在開發中) 之間共用,我們可以將實用原始類型公開在語言中,幾乎不需付費。

序數與字串

使用序數時,必須在編譯時提供結構定義,但可實現更有效率的實作方式 (字串處理速度一律會比整數處理速度慢)。由於 FIDL 已要求在編譯期間提供結構定義,因此希望這裡的字串序數不會造成爭議。

密集與稀疏打包

至於密集與稀疏序數空間的問題,可能會引起更多爭議。現行做法分為兩派:

  • Thrift 和 Protobuf 使用稀疏序數空間,可為欄位指定任何序數值。
  • FlatBuffers 和 Cap'n'Proto 使用密集序數空間,因此欄位必須指定連續序數。

Protobuf 傳輸格式與剖析為固定大小結構體的典型 Protobuf 實作搭配時,會有錯誤,導致解碼記憶體使用的記憶體量與傳輸線路上傳輸的位元組數量不相關。舉例來說,假設郵件含有 10000 個 (選用) int64s 欄位。傳送端可以選擇只傳送一個,導致訊息在網路上只有幾個位元組,但在記憶體中卻有近 100 KB。透過將許多這類訊息當做 RPC 傳送,很容易就能阻撓執行流程控制,並導致 OOM。

針對稀疏序數的另一種實作策略 (如先前討論的建議),是傳送 (ordinal, value) 元組的排序陣列。選擇就地解碼的實作方式必須仰賴透過資料進行二進位搜尋,才能找到序數。這可避免先前提到的流程控制錯誤,但由於我們執行的二元搜尋作業可能會非常多,因此這可能會導致執行階段出現一些效率不佳的情況。

Cap'n'Proto 實作了處理序數的非常複雜演算法,但我們希望避免這種複雜性,因此不會在此進一步討論。

FlatBuffers 的匯出格式與本文所提及的格式非常相似:利用密集的序號空間提供單一陣列查詢,以便找出欄位的資料 (或空值)。

既有技術與參考資料

  • FlatBuffers 演算法與這項演算法類似,但已在這裡調整,以便更符合 FIDL 慣例。
  • 我們認為,Protobuf 最初是將序數/值表示法推廣開來,而這項功能在 Google 內部的大規模使用,也證明瞭這項配置方案多年來的穩健性。
  • Cap'n'Proto 和 Thrift 各自對上述內容做出了一些調整。