RFC-0051:更安全的 C++ 結構

RFC-0051:C++ 的安全結構體
狀態已遭拒
區域
  • FIDL
說明

允許 C++ 開發人員編寫 FIDL 程式碼,如果結構體未完全初始化,則會在編譯期間中斷。

作者
提交日期 (年-月-日)2018-07-19
審查日期 (年-月-日)2019-03-14

拒絕理由

這項 RFC 已於 2019 年 3 月 14 日初步通過。不過,實作從未合併,且 C++ 繫結自此後已大幅變更。因此,我們將追溯拒絕這項 RFC。

摘要

允許 C++ 開發人員編寫 FIDL 程式碼,如果結構體未完全初始化,則會在編譯時中斷。

提振精神

在 Peridot 中,我們有複雜的 FIDL 結構體,我們正在改變這些結構體,以便更瞭解如何解決我們正在處理的問題。結構體通常會深層巢狀,並在遠離建構位置的程式碼中傳送。在疊代結構體時,我們通常會對語意進行重大變更,例如新增必要欄位,或將先前的選用欄位設為必要欄位。很難追蹤所有需要更新的程式碼。這些錯誤不會顯示為編譯時間錯誤,而是顯示為執行階段錯誤,因此很難與錯誤初始化結構體的程式碼建立關聯。

在 Dart 程式碼中,同類問題相當普遍,直到我們做出變更,要求所有必填欄位都必須傳入結構體建構函式為止。這項變更讓 Dart 程式碼的開發作業變得更有效率且更穩健。

設計

這會修改 C++ 繫結程式庫和程式碼產生器。不會移除任何現有介面,只會新增建構 FIDL 結構體例項的新方法。

這會為 FIDL 結構體新增建構工具模式。使用方式如下:

FooPtr foo = Foo::Builder()->set_bar("hello")->set_baz("world");

結構體類別上的 Builder() 靜態方法會傳回範本建構工具物件。建構工具範本參數會擷取要建構的結構體類型,以及結構體中每個未設定欄位的類別。它會保留結構體的例項。

欄位類別有兩個方法:set_name(value) 方法會在例項上設定欄位值,並傳回已從建構工具的範本引數中移除欄位的建構工具;Check() 方法則會針對選用欄位執行無操作,針對必要欄位則會傳回 static_assert 錯誤。

建構工具類別會擴充範本參數中的所有欄位類型,讓開發人員可以存取 setter 方法。開發人員呼叫 setter 並接收新的建構工具類型時,建構工具範本引數中的欄位類別清單會縮減。例如,略過部分範本內容:

Foo::Builder() 是具有 set_bar()set_baz() 方法的 Builder<Foo, Foo::Field_bar, Foo::Field_baz>

Foo::Builder()->set_bar(...) 是具有 set_baz() 方法的 Builder<Foo, Foo::Field_baz>

Foo::Builder()->set_bar(...)->set_baz(...) 是沒有任何 setter 方法的 Builder<Foo>

建構工具具有結構體型別和結構體指標型別的隱含轉換運算子。這些方法會對剩餘的欄位類型呼叫 Check() 方法,並傳回建構工具所保留的結構體例項。Check() 方法會是無操作 (選填欄位) 或 static_assert 失敗,指出未設定哪個必填欄位。

說明文件和範例

我們會更新 FIDL 教學課程和範例,以示範建立結構體例項的傳統和新方法。

回溯相容性

這項提案純粹是新增內容。不會導致回溯不相容性。

成效

這項變更不會產生執行階段效能成本。我們在 Compiler Explorer 中製作原型,特別是為了確保不會產生或執行其他程式碼。

它會在繫結程式庫中新增標頭檔案,並在產生的 C++ 程式碼中為每個結構體欄位新增幾行額外內容。C++ 編譯器必須進行一些額外作業來解析範本,但不會在編譯作業中加入任何會造成重大影響的額外步驟。

安全性

這項變更可讓我們將程式設計錯誤從執行階段錯誤轉換為建構時間錯誤。這麼做可減少程式的狀態空間,並減少必須正確處理和測試的錯誤案例數量。減少非預期行為有助於提升安全性。

測試

應擴充 C++ 繫結單元測試,以測試建構工具是否正確設定不同類型的欄位。

要測試編譯器是否偵測到建構工具的錯誤用法 (例如:未設定必要欄位) 相當困難。我們不清楚該如何測試。

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

這會在 FIDL C++ 繫結程式庫中新增一些相當棘手的範本。這會帶來維護負擔,並可能產生一些小型建構時間額外負擔。

先前的範本方法使用位元遮罩,雖然範本更簡單,但會設下 64 個必填欄位等限制,並增加 FIDL 編譯器的複雜度。

我們也可以建立 Linter,嘗試追蹤是否已設定所有必要欄位。這似乎是相當複雜的資料流分析。

既有技術與參考資料

Dart 繫結已於去年變更,因此結構體建構函式會為每個欄位採用命名的引數。系統會將必填欄位標示為必填,讓 Dart Analyzer 拒絕讓部分欄位未初始化的變更。