RFC-0045:零大小的空结构体

RFC-0045:大小为零的空结构体
状态已拒绝
区域
  • FIDL
说明

RFC-0056(“空结构体”)通过允许定义空结构体来改进了语言工效学。空结构体不带任何内容,但目前在线格格式中占用一个字节,以便与所有 FIDL 语言实现保持兼容。这会占用不必要的空间,而由于 FIDL 在许多上下文中的八字节对齐,这种情况通常会更糟糕。

作者
提交日期(年-月-日)2018-12-26
审核日期(年-月-日)2019-05-29

拒绝理由

由于有更高优先级的工作,且影响较小,因此作者已拒绝此 RFC。如果这些想法日后产生更大的影响,我们可以日后再考虑。

因此,部分部分未填写完整。

摘要

RFC-0056(“空结构体”)通过允许定义空结构体来改进语言工效学。空结构体不带任何内容,但目前在线格格式中占用一个字节,以便与所有 FIDL 语言实现保持兼容。这会占用不必要的空间,而由于 FIDL 在许多上下文中的八字节对齐,这种情况通常会更糟糕。

此 RFC 基于 RFC-0056,通过增强空结构体使其在线路上占用零字节。

设计初衷

RFC-0056 介绍了空结构体的应用场景:

  • 计划日后使用,
  • 作为联合中的选项,
  • 作为命令模式的一部分,

除了携带零信息且占用线路上非零空间的对象总体上不雅观之外,高效实现空结构体对于未来可能进行的 FIDL 工作(例如代数数据类型泛型)也至关重要。

大小为 1 的空结构体设计还会使所有其他 FIDL 目标语言都付出 C++ 独有的代价(如需了解详情,请参阅下面的设计部分)。其他语言通常可以使用零字节表示空结构体。

设计

两种设计:任选其一。我们必须做出选择。

我更喜欢设计 1,其中我们使用长度为零的数组来表示空结构体。这对用户来说不会太过意外,并且在所有用例中都保持一致。缺点是,我们需要 C 扩展程序,这可能不符合您的要求。

设计 2 是可行的,但如果将空结构体用作 FIDL 方法的参数,可能会出现令人惊讶的人体工学问题。期待收到您对此的想法和反馈。

如果编译器支持零长度数组,我们也可以假定采用设计 1;如果不支持,则采用设计 2。我很喜欢这样。

设计 1:长度为零的数组

此 RFC 建议使用长度为零的数组来表示空结构体。这是 FIDL 的目标 C 和 C++ 编译器支持的常见扩展。

简化后的示例在 C 和 C++ 中都类似:

// FIDL: struct Empty {};

// define a zero-length array type
typedef int FIDLEmptyStructInternal[0];

typedef struct {
  FIDLEmptyStructInternal reserved;
} Empty;

上述代码段会针对 C 和 C++ 断言 sizeof(Empty) == 0。在实践中,为绑定生成的代码应关闭针对 C 和 C++ 的各种警告1Github Gist 展示了生成的 C 和 C++ 绑定的更完整示例。

设计 2:完全省略发出空结构体

在 C 和 C++ 中,必须将 FIDL 结构体转换为等效的结构体。由于 C 和 C++ 对空结构体的处理方式不同,因此空结构体属于特殊情况:

  • C 会将空结构体的大小定义为未定义2。许多编译器(例如gccclang),因此将空结构体定义为大小为零
  • C++ 将空结构体定义为大小为 1。大多数编译器都会采用“空基类”优化,以便在特定情况下将其优化为 0。

作为一种权宜解决方法,RFC-0056 建议生成一个包含单个 uint8 成员的结构体来表示空结构体,这在 C 和 C++ 中是一致的。

空结构体出现在以下三种不同的上下文中:

  • 在包含的结构体内,
  • 在包含的联合体或表格内,或者
  • 作为 FIDL 接口方法的参数,作为“顶级”结构体。

这三个上下文可以单独处理。

在包含的结构体内

包含结构体中的空结构体可以省略空结构体的成员。例如,以下 FIDL 结构体:

// FIDL
struct FooOptions {
    // There is currently no information here but
    // we have preserved the plumbing for now.
};

struct Foo {
    uint32 before;
    FooOptions options;
    uint32 after;
};

只需在 C/C++ 绑定中省略生成“FooOptions”空结构体成员即可:

// generated C or C++ code
struct Foo {
    uint32_t before;
    // "FooOptions options" is missing here
    uint32_t after;
};

然后,序列化的 FIDL 线格式与 C/C++ 内存布局兼容,并且可以直接转换为/从这两种格式。

由于空结构体不包含任何信息,因此无法访问 .options 成员的影响微乎其微。如果结构体稍后更改为非空,则包含的结构体可以以源代码兼容的方式发出以前为空的结构体成员 options 3

用户可能希望执行的一个合理操作是获取空结构体(即 &(foo.options))的地址,但在此次变更后,将无法再执行此操作。我们认为,为了实现一致的跨语言零大小空结构体,这是可以接受的权衡。

TODO(apang):Go、Rust、Dart。

在包含表格或联合体内

表或(静态或可扩展的)联合都有序数(“标记”),用于指明表/联合携带的信息。在本例中,空结构体“包含信息”的意思是,它的存在代表信息,即使空结构体本身不包含任何信息。

因此,表或联合体仍会发出序数,以便客户端代码可以检查它以确定表/联合体中包含哪些信息。不过,无法访问空结构体本身。例如,空结构体的联合:

// FIDL
struct Option1 {};
struct Option2 {};
union {
    // an "empty" union!
    Option1 o1;
    Option2 o2;
};

仍然可以:

  1. 具有明确定义的内存布局,其中包含单个 uint32 枚举标记。
  2. 发出表示序数和适当访问器方法的枚举,以便客户端代码可以创建和检查此类联合。

TODO(apang):添加 C/C++ 绑定示例。

表格也是如此:将空结构体作为表格字段表示信息。与联合体相同的方法(为序数和客户端代码发出枚举,但省略对空结构体的访问)也可用于表。

TODO(apang):Go、Rust、Dart。

作为 FIDL 方法参数

在现有的用例中,空结构体被用作 FIDL 方法参数,例如在 fuchsia.ui.viewsv1 中:

interface ViewContainerListener {
    // |ViewInfo| is an empty struct
    OnChildAttached(uint32 child_key, ViewInfo child_view_info) -> ();
};

struct ViewInfo {};

任何为空结构体的函数参数都可以是:

  1. 从方法签名中省略(推荐),
  2. 规范化为 C 或 C++ 中的单个空结构体单例类型(例如 fidl::EmptyStruct),该类型不会直接映射到零字节线格格式,或者
  3. 按原样发出,语言表示法不会直接编码/解码为线格格式。

变更

  • 无需更改 FIDL 源代码语言。
  • FIDL 线格格式和文档将发生变化,以便空结构体在线路上占用 0 字节,而不是 1 字节。
  • fidlc 无需更改。
  • 需要更新每种语言后端(C、C++、Rust、Go、Dart),以反映本部分中所述的绑定变更。这应作为硬过渡进行,以便保留跨层级 ABI 兼容性。

实施策略

实现将类似于 RFC-0056,并且需要拆分到多个 CL:

  • 用于更新所有语言的生成绑定的 CL,而无需更新跨语言兼容性测试。
  • 硬转换集成 CL,以确保滚动条成功。
  • 更新了跨语言功能。
  • 更新。

对于此 RFC,无需更改 FIDL 源语言。

缺点、替代方案和未知情况

请注意:

  • 在 C 和 C++ 中保持一致
  • 这两种语言的大小实现不同
  • C++ 理论上要求
  • 零长度数组
  • C++ [[no_unique_address]]

在先技术和参考文档

https://herbsutter.com/2009/09/02/when-is-a-zero-length-array-okay/


  1. -Wzero-length-array,其中 -Wc++-compat 适用于 C 语言,-Wextern-c-compat 适用于 C++ 语言。这些警告可以使用常用的 #pragma 诊断 push/ignored/pop 编译器指令进行限定,以便警告仅应用于空结构体代码。 

  2. C99, 6.7.2.1:[...] 如果 struct-declaration-list 不包含命名成员,则行为未定义。 

  3. 请注意,对结构体的大多数更改都会破坏 ABI。