RFC-0057:預設沒有控制代碼

RFC-0057:預設不使用控制代碼
狀態已接受
區域
  • FIDL
說明

我們建議預設禁止在 FIDL 型別宣告中使用控制代碼,並新增關鍵字資源,標記允許包含控制代碼或其他資源型別的型別。新增或移除資源修飾符可能會導致來源中斷。

作者
提交日期 (年-月-日)2020-01-16
審查日期 (年-月-日)2020-01-23

「媽,你看,沒有把手!」

摘要

我們建議預設禁止在 FIDL 型別宣告中使用控制代碼,並新增 resource1 關鍵字,標記允許包含控制代碼或其他資源型別的型別。新增或移除 resource 修飾符「可能」會造成來源中斷。

提振精神

FIDL 的顯著特點是支援 Zircon 控制代碼。控制碼是記憶體中的 32 位元整數,但會經過特殊處理:控制碼必須移動而非複製,且必須關閉,以免資源洩漏。如果考慮的特徵只適用於沒有控點的純資料,這種特殊處理方式就會導致問題。雖然 FIDL 繫結可以根據控制代碼是否存在,有條件地啟用程式碼,但這麼做會破壞可演進性保證,因此不建議。舉例來說,在資料表中新增欄位通常很安全,但新增控制代碼欄位會導致來源中斷,不僅是該資料表,所有遞移包含該欄位的型別都會受到影響。這會將繫結推送到保守狀態,一律假設型別可能包含控制代碼,即使程式庫作者從未打算新增這些控制代碼也一樣。

為配合帳號代碼,我們不得不做出妥協:

  • 在 Dart 中,實作 FIDL 至 JSON 編碼的嘗試遭到反對,因為這只適用於沒有控制代碼的型別,會損害可演化性。最終是使用 MaxHandles 屬性建構而成,但這只是暫時的解決方案,因為該屬性只適用於最外層的型別,不適用於可從該型別遞移存取的所有型別。
  • 在 Rust 中,首次將控制代碼新增至型別時,會導致來源中斷,因為型別不再衍生 Clone 特徵。(如要正確複製控制代碼,必須叫用 zx_handle_duplicate 系統呼叫,但這可能會失敗)。
  • 通訊協定的 Rust 繫結會透過可變動的參照取得 FIDL 物件,並將控制代碼歸零,而不是明確取得擁有權,因此沒有控制代碼的物件之後可以重複使用。

如果我們要求程式庫作者指出型別是否可能包含控制代碼,且變更指標預期會導致來源中斷,就能以更安全、更符合人體工學的方式處理所有這些情況。

設計

術語

FIDL 類型可以是類型或資源類型。資源類型包括:

  • handlehandle<H>,其中 H 是控制碼子類型
  • Prequest<P>,其中 P 是通訊協定的名稱
  • resource 修飾符宣告的結構體、資料表或聯集
  • 參照資源型別的型別別名
  • 包裝資源類型的新型別 RFC-0052
  • T?,其中 T 是不可為空值的資源類型
  • array<T>vector<T>,其中 T 是資源類型

所有其他型別都是值型別。

正確使用 resource 修飾符時,值型別絕不會包含控制代碼,但資源型別可能會 (現在或日後) 包含控制代碼。

語言

新的修飾符 resource 可套用至結構體、資料表和聯集宣告。

如果沒有 resource,宣告就不得包含資源類型。 FIDL 編譯器必須驗證這點。只需要檢查直接欄位:如果 A 包含 B,兩者都不會標示為資源,且 B 包含控制代碼,則編譯會因 B 而失敗,不需要針對 A 遞移包含控制代碼的情況,另外顯示錯誤訊息。

使用 resource 時,宣告可包含資源類型。即使不含資源,宣告的新型別也會視為資源型別。

原則上,語言可允許 resource 新型別宣告 RFC-0052。不過,包裝值型別的資源 newtype 沒有實際用途,因此 newtype 會從包裝的型別隱含地繼承值/資源狀態。

文法

這項提案會修改 FIDL 文法中的一項規則:

declaration-modifiers = "strict" | "resource" ;

JSON IR

這項提案會在 "struct_declarations""table_declarations""union_declarations" 陣列中的所有物件新增布林值鍵 "resource"

請注意,這個鍵與 "max_handles" 並非多餘。值類型必須將 max_handles 設為零,但資源類型可以有任意數量的 max_handles,因為這會反映宣告的實際內容 (而非程式庫作者允許控制代碼的意圖)。

繫結

這項提案不包含繫結的具體變更。不過,這可讓 FIDL 繫結作者 (包括 FIDL 團隊) 解決「動機」一節討論的問題。以下列舉幾個可透過這項 FTP 實現的例子,但接受這項 FTP 並不代表您必須這麼做:

  • 實作 JSON 序列化和值型別的序列化 (或更可能是 FIDL 文字格式,而非 RFC-0058 中建議的 JSON)。
  • 在值/資源型別上,為 C++ Clone() 方法使用不同的型別簽章,強調只有資源複製作業可能會失敗。
  • 讓 Rust 協定將值型別引數視為 &T,資源型別引數視為 T,而非同時使用 &mut T,且只會變動資源型別。

API 評分標準

API 評分標準應提供使用 resource 的時機相關指引。簡單案例:

  • 沒有資源類型的結構體「不應」標示為 resource,因為結構體並非設計為可擴充 (稍後新增控制代碼在大多數情況下會中斷 ABI)。
  • 如果嚴格資料表或聯集沒有資源類型,則「不應」標示為 resource,因為嚴格性已表示修改其欄位會造成來源中斷。

此外,也應處理最初沒有控點的彈性表格和聯集。舉例來說,我們可能會根據程式庫的用途、使用範圍、預期在所用語言中發生來源中斷變更的成本,以及其他因素,建議您偏向其中一側。

導入策略

高階實作步驟包括:

  • 在 fidlc 中剖析 resource 關鍵字。
  • 遷移現有 FIDL 程式庫,改用 resource (詳情請參閱「不明」)。
  • 在 fidlc 中驗證值/資源類型規則,並進行測試。
  • resource 旗標儲存在 JSON IR 中,並在 fidlgen 中公開。

人體工學

這項提案引進新概念,因此會讓 FIDL 更加複雜。與「struct」和「通訊協定」等其他 FIDL 建構體不同,新使用者不太可能猜到「resource」的意義,因此需要從說明文件中瞭解。

這項提案是否能讓 FIDL 語言更符合人體工學,仍有待商榷。這項功能有助於醒目顯示含有控制代碼的宣告,特別是實際控制代碼值隱藏在巢狀結構中時。任何瀏覽程式庫的人都會立即發現,結構體會攜帶控制代碼,而不只是資料。另一方面,您可能覺得擔心是否要使用 resource 和輸入關鍵字,會比較不符合人體工學。將一個宣告從值變更為資源,可能會產生痛苦的連鎖效應,導致許多型別必須成為資源 (不過這可以視為好事,否則會顯示為來源中斷)。

FIDL 繫結的改善措施可證明複雜度增加是合理的。您可以自由為值類型和資源類型提供不同的 API,讓繫結更安全且更符合人體工學。如需這些改良項目的範例,請參閱「繫結」。

說明文件和範例

您需要完成下列工作:

  • 更新所有涉及控制代碼的文件,視情況使用 resource
  • 更新 FIDL 語言規格,說明 resource 修飾符。
  • 在 FIDL 教學課程中提及 resource。請附上簡短附註,說明所有修飾符 (即 strictresource)。
  • 針對不含控制代碼的新型別是否應為資源提供指引。
  • 繫結運用值/資源區別後,請更新文件,說明值類型和資源類型提供的 API 差異,並提供兩者之間的轉換說明 (如有可能)。

回溯相容性

這項提案不會影響 ABI 相容性。

修正案 (2021 年 7 月)。在實作過程中,我們發現這項提案與 RFC-0033:處理不明欄位和嚴格程度的互動存在極端情況。解碼表格和彈性聯集時,部分繫結會儲存不明成員;如果不明成員包含控制代碼,值型別就無法執行這項操作,因此解碼作業必須在此情況下失敗。詳情請參閱相容性指南

修訂內容 (2021 年 10 月)。RFC-0137:捨棄 FIDL 中的不明資料之後,繫結不再儲存不明資料,因此不會再有極端情況。因此,值/資源的區別不會影響 ABI 相容性。

新增或移除 resource 修飾符既不相容於來源,也無法轉換2RFC-0024 而言。繫結明確允許為僅在修飾符存在與否方面不同的兩種型別,產生不相容的 API,而且事實上,在新增/移除修飾符前後,可能無法編寫可編譯的程式碼。程式庫作者如要以來源相容的方式轉換至/自 resource,必須建立新的型別和方法,而非變更現有型別和方法。

一旦繫結作者開始利用值/資源區別,我們就會重新審視這項決定。建議您要求可轉換的路徑 (或許使用具有 [Transitional] 屬性的中繼階段)。一開始,這點並不清楚:這項限制可能過於嚴格,反而會阻礙提案中旨在實現的 API 改良。

效能

這項提案對建構效能的影響微乎其微:FIDL 編譯器會稍微多做一些工作,以剖析新關鍵字並驗證其用途。這不會直接影響 IPC 效能。如果繫結使用值/資源區別來建立 API,避免不必要的副本,則可能在某些語言中略有改善。舉例來說,您應該不需要複製值型別物件,即可多次傳送該物件。

安全性

這項提案不會直接影響安全性,但可讓繫結提供更安全的 API。舉例來說,C++ 可以對具有 [[nodiscard]] 的資源型別強制執行 Clone() 的錯誤處理,或者 Rust 可以透過移動來取得資源型別方法引數,防止之後意外使用變動的物件。這類變更可避免錯誤,包括安全性錯誤。

測試

這項功能的測試方式如下:

  • 在 fidlc 中新增測試,用於剖析和驗證程式碼路徑。這些測試應涵蓋各種案例,包括標示為 resource (或未標示) 的宣告不符合資源類型 (或值類型) 定義的情況。
  • 除了修正需要 resource 的現有宣告,也請在黃金版本中新增一些資源類型宣告。
  • 更新 fidl-changes 測試套件,示範從值型別轉換為資源型別,以及從資源型別轉換為值型別的步驟。

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

這項提案導入了新關鍵字,使語言更加複雜。 關鍵字過多可能會造成問題;「嚴格資源聯集」有點難以理解。

本提案會透過兩種方式削弱 FIDL 演進能力保證:

  • 先前,將控制代碼新增至型別時,不應會造成來源中斷。現在,系統會明確允許並預期這麼做 (除非類型已標示為 resource,預期需要新增控制代碼)。
  • 先前,您可以宣告類型,並預期 (1) 為其新增控制代碼,以及 (2) 將其做為任何其他類型中的欄位。現在,程式庫作者必須在一開始就選擇 (1) 或 (2)。

這項提案有兩種主要替代方案:

  • 不採取任何行動。允許在任何位置使用控制代碼,並接受新增或移除控制代碼時,繫結必須保留來源相容性。
  • 預設允許控點。與這項提案類似,但假設宣告預設為資源類型,且需要 value 關鍵字,才能在宣告中禁止資源類型。

「動機」和「人體工學」部分會說明不採取任何行動的缺點。至於其他替代方案,經驗顯示大多數訊息不含控制代碼,且在通訊協定中傳遞控制代碼需要謹慎處理和事先規劃。換句話說,值類型是常見情況,而事後新增控制代碼的功能可能不如想像中實用。這表示不允許控制代碼是較好的預設值。

這項提案主要對使用 FIDL 繫結的開發人員有益,但缺點則適用於設計 API 的程式庫作者。這項取捨符合 Fuchsia API 委員會章程,該章程優先考量的是終端開發人員,而非 API 設計人員和實作者。

我們建議的另一種替代方案是將控制代碼做為參照。與其禁止從值型別處理控制代碼,不如將控制代碼表示為參照,解決值/資源問題。複製含有控制碼的結構只會建立相同控制碼的另一個參照。這項作業可使用 C++ 中的 shared_ptr 完成,而且不必新增 resource 關鍵字,就能大幅簡化作業。不過,這項技術也面臨一些挑戰:

  • 所有繫結都需要記帳機制,確保控制代碼只會在最後一個參照消失時關閉。在某些語言中,這可能很困難。
  • 將控制代碼傳送至其他程序後,對該控制代碼的所有其他參照都會失效,就像懸空指標一樣。將控制代碼視為一般值可帶來便利性,但這表示在這些情況下,編譯時間安全性較低。
  • 由於這項作業會變更所有控制代碼的型別,因此很可能在所有語言中造成重大變更。順利轉移需要大量工作。

這項提案仍有幾個開放式問題:

  • 如何遷移現有的 FIDL 程式庫?以 resource 標記所有現有宣告是安全的,但不會反映程式庫作者的意圖。只標記最低限度的項目 (即包含控制代碼的型別) 即可,但假設任何不含控制代碼的項目都不會包含控制代碼,可能過於激進。
  • 如果採用一般資料類型,這項功能會如何與其互動?舉例來說,如果我們定義 Pair<A, B> 型別,則 AB 為資源型別時,該型別應在邏輯上為資源型別,而不是必須註解 Pair 本身。是否有其他情況適合判斷型別是否為資源?

先前技術和參考資料

這項提案的目標是允許在變更型別的值/資源狀態時,發生來源中斷的變更。RFC-0024 與這個目標相關,因為它為 FIDL 建立了來源相容性標準。提案也提到控制代碼問題,導致難以在 Rust 中使用 Clone 特徵,而這項提案可解決這個問題。

我們不清楚是否有其他 IPC 系統可解決這個確切問題 (區分可能含有控制代碼或系統資源的型別)。不過,在程式設計語言中,以「感染」所有使用網站的方式註解型別的概念很常見。舉例來說,JavaScript、Python 和 Rust 中的非同步函式,以及 Haskell 中的 IO monad,都有這種行為。


  1. 這個提案的舊版將關鍵字稱為 entity。 

  2. 這個提案的較早版本要求變更必須可轉換。