| RFC-0057:預設不使用控制代碼 | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 我們建議預設禁止在 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 類型可以是值類型或資源類型。資源類型包括:
handle和handle<H>,其中H是控制碼子類型P和request<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。請附上簡短附註,說明所有修飾符 (即strict和resource)。 - 針對不含控制代碼的新型別是否應為資源提供指引。
- 繫結運用值/資源區別後,請更新文件,說明值類型和資源類型提供的 API 差異,並提供兩者之間的轉換說明 (如有可能)。
回溯相容性
這項提案不會影響 ABI 相容性。
修正案 (2021 年 7 月)。在實作過程中,我們發現這項提案與 RFC-0033:處理不明欄位和嚴格程度的互動存在極端情況。解碼表格和彈性聯集時,部分繫結會儲存不明成員;如果不明成員包含控制代碼,值型別就無法執行這項操作,因此解碼作業必須在此情況下失敗。詳情請參閱相容性指南。
修訂內容 (2021 年 10 月)。在 RFC-0137:捨棄 FIDL 中的不明資料之後,繫結不再儲存不明資料,因此不會再有極端情況。因此,值/資源的區別不會影響 ABI 相容性。
新增或移除 resource 修飾符既不相容於來源,也無法轉換,2就 RFC-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>型別,則A或B為資源型別時,該型別應在邏輯上為資源型別,而不是必須註解Pair本身。是否有其他情況適合判斷型別是否為資源?
先前技術和參考資料
這項提案的目標是允許在變更型別的值/資源狀態時,發生來源中斷的變更。RFC-0024 與這個目標相關,因為它為 FIDL 建立了來源相容性標準。提案也提到控制代碼問題,導致難以在 Rust 中使用 Clone 特徵,而這項提案可解決這個問題。
我們不清楚是否有其他 IPC 系統可解決這個確切問題 (區分可能含有控制代碼或系統資源的型別)。不過,在程式設計語言中,以「感染」所有使用網站的方式註解型別的概念很常見。舉例來說,JavaScript、Python 和 Rust 中的非同步函式,以及 Haskell 中的 IO monad,都有這種行為。