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 都必須存在)。
- 每個欄位都無法聲明相同的序數。
- 檢查序數衝突後,編譯器會捨棄「保留」欄位。 這允許註解,指出在舊版資料表的部分版本中使用了某個欄位,但該欄位已遭捨棄,這樣未來的修訂版本就不會意外重複使用該序數。
- 資料表不得包含可為空值的欄位。
凡是目前可在該語言中使用結構體的任何位置,都能使用資料表。特別是:
- 結構體和聯集可包含資料表
- 資料表可以包含結構體和聯集
- 介面引數可以是資料表
- 資料表可設為
電匯格式
資料表會儲存為封裝的 vector<envelope>
,向量的每個元素都是一個序數元素 (因此索引 0 為序數 1,索引 1 為序數 2 等)。我們會在下文說明信封。
表格只能儲存截至最後一張清單的信封,也就是最大集序。這可確保標準表示法。舉例來說,如未設定任何欄位,正確的編碼就會為空白向量。如果資料表的欄位為序數 5,但欄位只設為序數 3,則正確的編碼是 3 個信封的向量。
信封
envelope
會儲存變數大小、未經解讀的酬載。酬載可能包含任意數量的位元組和處理。這個機構允許將一則 FIDL 訊息封裝在另一個 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 位元組物件對齊方式 (實務上表示沒有額外的邊框間距)。
語言繫結
資料表不會產生結構等資料欄位,而是要為每個欄位產生一組方法。以 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 實作搭配使用時會出現錯誤,其中已解碼的記憶體使用的記憶體量與傳輸的位元組數沒有關聯。如要查看這項資訊,請設想含有 10, 000 (選用) int64
個欄位的訊息。傳送者可以選擇只傳送一封郵件,產生只有幾位元組的線,但在記憶體中卻幾乎是 100 KB。藉由傳送其中許多項目做為 RPC,往往很容易就會遏止流程控管的實作作業,並造成 OOM。
稀疏序數的替代實作策略 (如先前的對話中的建議) 將傳送已排序的 (ordinal, value)
元元組。選擇就地解碼的實作方式,必須依賴透過二進位檔搜尋來尋找序數。這可避免之前提到的流程控制錯誤,但隨著我們執行數量可能驚人的二元搜尋時,可能在執行階段導入一些重大的效率問題。
Cap'n'Proto 實作了一個非常複雜的演算法來處理序數,我們想要避免這件事不再複雜。
FlatBuffers 的傳輸格式與本文件中的提議非常類似:利用稠密的序線空間提供單一陣列查詢,找出特定欄位的資料 (或為空值)。
先前的圖片和參考資料
- FlatBuffers 演算法類似於此演算法,但在這裡經過調整,以便更符合 FIDL 慣例。
- Protobuf (我們相信) 最初廣受歡迎的是序數/值表示法,而 Protobuf (我們相信) 過去幾年來也廣為採用該配置。
- Cap'n'Proto 和 Thrift 各具特色。