RFC-0051:适用于 C++ 的更安全的结构体

RFC-0051:为 C++ 使用更安全的结构体
状态已拒绝
领域
  • FIDL
说明

允许 C++ 开发者编写将在编译时因结构未完全初始化而中断的 FIDL 代码。

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

遭拒的理由

2019 年 3 月 14 日,我们最初接受了此 RFC。不过,implementation 从未合并过,并且自那以后,C++ 绑定发生了重大变化。认识到这一点后,我们将主动拒绝此 RFC。

总结

允许 C++ 开发者编写将在编译时因结构未完全初始化而中断的 FIDL 代码。

设计初衷

在 Peridot 中,我们有复杂的 FIDL 结构体,并随着我们更好地了解如何解决我们正在解决的问题而对结构进行了更改。结构体通常是深层嵌套的,并且代码发送在远离构造位置的代码中。在对结构体进行迭代时,我们通常会对语义做出破坏性更改,添加必填字段或将之前的可选字段设为必需字段。很难跟踪需要更新的所有代码。这些错误并不显示为编译时错误,而是显示为运行时错误,难以与未正确初始化结构体的代码相关联。

同一类问题在 Dart 代码中经常存在,直到进行了一项更改,要求将所有必填字段都传递到结构体构造函数中。这一变化使得开发 Dart 代码变得更加高效和可靠。

设计

这会修改 C++ 绑定库和代码生成器。它不会移除任何现有接口,而只是添加一种构建 FIDL 结构体实例的新方法。

这会为 FIDL 结构体添加构建器模式。使用方法如下:

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

结构体类上的 Builder() 静态方法会返回模板化构建器对象。构建器模板参数可捕获正在构建的结构体的类型以及结构体上每个未设置的字段的类。其中包含结构体的一个实例。

Field 类有两个方法:一种是 set_name(value) 方法,用于为实例上设置字段值,并返回一个构建器,并将该字段从构建器的模板参数中移除;而 Check() 方法对于可选字段是空操作,对于必填字段是 static_assert 失败项。

Builder 类会扩展其模板参数中的所有字段类型,以便开发者能够访问 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 绑定去年进行了更改,因此结构体构造函数会采用每个字段的具名实参。必需的字段会标记为必需,以便 dartanalyzer 可以拒绝某些字段未初始化的更改。