RFC-0020:接口序数哈希

RFC-0020:接口序数哈希
状态已接受
领域
  • FIDL
说明

我们提议使程序员能够手动指定接口方法的序数。相反,编译器会根据完全限定方法名称的哈希值(即库名称、接口名称和方法名称)生成序数。

作者
提交日期(年-月-日)2018-10-26
审核日期(年-月-日)2018-11-29

“有 60% 的时候,这只是面试问题的答案”

总结

我们提议使程序员能够手动指定接口方法 1 的序数。相反,编译器会根据完全限定方法名称(即库名称、接口名称和方法名称)的哈希生成序数。方法重命名将通过新的 Selector 属性与 ABI 兼容(请参阅下文)。

我们明确限制此 FTP 只能提议对接口进行序数哈希处理;不适用于枚举、表或可扩展联合。我们认为这些结构的用例差异很大,因此需要进一步调查并使用不同的 FTP。

示例

目前,FIDL 作者会这样写:

library foo;

interface Science {
    1: Hypothesize();
    2: Investigate();
    3: Explode();
    4: Reproduce();
};

此 FTP 可以删除序数索引:

interface Science {
    Hypothesize();  // look, no ordinals!
    Investigate();
    Explode();
    Reproduce();
};

在后台,编译器会有效地生成如下所示的序数:

interface Science {
    // ordinal = SHA-256 of the fully-qualified method name,
    // i.e. "foo.Science/MethodName", truncated to 32 bits
    0xf0b6ede8: Hypothesize();
    0x1c50e6df: Investigate();
    0xff408f25: Explode();
    0x0c2a400e: Reproduce();
};

设计初衷

  • 手动指定序数主要是机械的。如果您不需要考虑这些因素,那么编写接口就很少了。
  • 如果使用良好的哈希,则哈希处理不可能产生序数冲突,这是人工编写序数的改进(尤其是在使用接口继承的情况下)。如需了解详情,请参阅下文的序数冲突部分
  • 程序员目前必须确保不同方法的序数不会冲突。对于方法较少的接口来说,这很容易,但是如果一个接口有很多方法,这可能变得非常重要。对于序数编号,有不同的编码风格和思想流派,从而导致编码风格不一致。
    • 大多数接口从 1 开始,然后向上。
    • 不过,有些作者更喜欢在范围内对不同的接口方法进行分组(例如,1-10、100-110 等)。
    • 移除手动编号序数也会移除这种不一致的样式,而且无需作者决定使用哪种样式。
  • 接口继承可能会导致序数出现意外冲突。迄今为止,我们已为解决此问题进行了两次尝试:
    • FTP-010(已拒绝)提出了 OrdinalRange 属性,以便接口继承的可预测性更高;被拒绝。
    • FragileBase 2 是当前的权宜之计解决方案,但并没有解决确保序数不会冲突的核心问题。
    • 如果对序数进行哈希处理且使用接口和库名称计算哈希值,则对序数进行哈希处理不会导致序数发生冲突,从而解决接口继承问题(除了极为罕见的哈希冲突)。

设计

哈希

经过哈希处理的序数通过以下形式的 SHA-256 哈希派生:

library name (encoded as UTF-8; no trailing \0)
".", ASCII 0x2e
interface name (encoded as UTF-8; no trailing \0)
"/", ASCII 0x2f
method name (encoded as UTF-8; no trailing \0)

例如,以下 FIDL 声明:

library foo;

interface Science {
    Hypothesize();
    Investigate();
    Explode();
    Reproduce();
};

具有用于计算序数哈希的以下字节模式:

foo.Science/Hypothesize
foo.Science/Investigate
foo.Science/Explode
foo.Science/Reproduce

之所以使用 ./ 分隔符,是因为 fidlc 已经以这种格式输出完全限定的方法名称(例如 fidlcNameName() 方法)。

计算出 SHA-256 哈希后:

  1. 系统会提取 SHA-256 哈希的前 32 位(例如,echo -n foo.Science.Hypothesize | shasum -a 256 | head -c8)
  2. 最高位被设置为 0,从而得到有效的 31 位哈希值,这些值将用零填充为 32 位。(使用 31 位,因为 FIDL 有线格式保留 32 位序数中的最高有效位。)

在伪代码中:

full_hash = sha256(library_name + "." + interface_name + "/" + method_name)
ordinal = full_hash[0] |
        full_hash[1] << 8 |
        full_hash[2] << 16 |
        full_hash[3] << 24;
ordinal &= 0x7fffffff;

选择器属性和方法重命名

我们定义了一个 Selector 属性,编译器将使用该属性(而不是使用方法名称)来计算经过哈希处理的序数。如果方法名称没有 Selector 属性,则方法名称将用作 Selector。(哈希计算中仍会使用接口和库名称。)

Selector 可用于重命名方法,而不会破坏 ABI 兼容性,这是手动指定的序数的一个优势。例如,如果我们希望在 Science 接口中将 Investigate 方法重命名为 Experiment,可以写入:

interface Science {
    [Selector="Investigate"] Experiment();
};

我们只允许在方法中使用 Selector 属性。重命名库被认为很少见,在这种情况下保留 ABI 兼容性并不是首要任务。同样,在重命名接口时也是如此。此外,重命名的接口与 Discoverable 属性的交互也会令人困惑:哪个名称是可检测到的?

序数冲突与冲突解决

如果某个经过哈希处理的序数导致与同一接口中另一个经过哈希处理的序数发生冲突或冲突,编译器就会发出错误,并依靠人工指定 Selector 属性来解决冲突 3

例如,如果方法名称 Hypothesize 与方法名称 Investigate 冲突,我们可以将 Selector 添加到 Hypothesize 以避免冲突:

interface Science {
    [Selector="Hypothesize_"] Hypothesize();
    Investigate();  // should no longer conflict with Hypothesize_
};

我们将更新 FIDL API 评分准则,建议在 Selector 的方法名称中附加“_”以解决冲突。fidlc 也会提供此修复建议。

请注意,每个接口的序数只有唯一性要求,与手动指定的序数类似。如果我们希望序数在所有界面上都是唯一的,则应在另一个 FTP 中提议。

包封计算表明,接口上的 31 位和 100 种方法发生冲突的可能性为 0.0003%,因此我们预计哈希冲突将极为罕见。

带选择器的自行车轮

还有其他关于“Selector”的建议:

  • WireNameabarth
  • OriginalNamectiller
  • Saltabarth;略有不同,因为它建议添加编译器指定的盐,而不是备用名称)
  • OrdinalName

我们之所以选择 Selector,是因为我们认为与 WireName 或 OriginalName 相比,它能更准确地反映属性的意图。

我们选择让程序员指定序号名称,而不是序号,原因如下:

  • 要求索引会更繁琐(例如,如果存在冲突,则复制并粘贴原始 SHA-256 哈希值),
  • 指定序数名称会启用与 ABI 兼容的方法重命名;
  • 指定名称而非索引可以使编程人员在编写接口时在头脑中保持相同的抽象级别,而不是降低一个抽象级别,这就要求他们考虑序数。

零序数

零是无效序数。如果方法名称的哈希为零,编译器就会将其视为哈希冲突,并要求用户指定不会哈希为 0 的 Selector

我们考虑过让 fidlc 通过确定性地对其进行转换来自动对名称进行重新哈希处理,但感觉:

  • 任何此类算法都是不明显的,
  • 零情况极为罕见,

因此,这种方法并不能保证使人体工程学和编译器实现都变得复杂。

事件

此 FTP 还涵盖事件,根据 FIDL 语言文档 4 将事件视为方法的子集。

编译器和绑定变更

我们认为,只需修改 fidlc 即可支持普通哈希处理;无需修改代码生成后端。这是因为 fidlc 会计算序数,并在 JSON IR 中将其发出给后端。

绑定无需更改。

实施策略

我们计划分几个阶段实现这一目标:

  1. 向 fidlc 添加代码以计算哈希值。
  2. 向库添加了对属性的支持。
  3. 向 fuchsia eng 广播 intent 更改,以便其发现潜在问题。 a.提议在预计下一步完成的某一日期弃用手动序数。
  4. 在同一 CL 中: a. 修改 FIDL 语法的接口方法规则,使序数变为可选;如需了解详情,请参阅下文。 b.忽略手动指定的序数,并针对传递给代码生成后端的序数使用经过哈希处理的序数。 c.添加 Selector 属性,手动修复任何现有的哈希冲突。
  5. 在两周内对更改进行测试,确保不存在生产环境中的问题。 a.目前写入的新 FIDL 接口不应使用序数。 b.手动序数被视为已弃用,但 fidlc 不会发出相关警告。 c.与相关团队合作,确保接口中没有手动指定的序数。 d.两周后,更新 FIDL 格式设置工具以移除序数,并将其批量应用到整个 Fuchsia 树。
  6. 取消了对手动指定的序数的支持。

以上是一个软转换;将 fidlc 更改为使用经过哈希处理的序数(第 4b 步)应该不会破坏滚轮,因为滚轮基于整个树的单个版本构建。

jeremymanson@google.com 在此 FTP 实现过程中,他选择优先使用手动指定的序数,而非经过哈希处理的序数,这与上面的步骤 4b 不同。这样会使使用手动指定的序数的所有现有接口保持 ABI 兼容性,并且仅在未指定序数时才使用经过哈希处理的序数。

工效学设计

优点:

  • 接口的编写应该比较简单。

缺点:

  • 程序员需要了解一个新的属性 Selector,它有两个用途:重命名和解决冲突。
  • 更改方法名称会破坏 ABI 兼容性,而程序员指定的序数并非如此。用户培训(例如更好的文档内容)可以进一步解决此问题。
    • 请注意,重命名接口方法时,其他组件系统(如 COM 和 Objective-C)通常也会破坏 ABI 兼容性。使用类似系统的开发者可能比较熟悉此行为。
  • 在一些特殊情况(例如,在同一 Zircon 通道上使用多个 FIDL 接口)下,失去对序数的手动控制可能会导致可调试性降低。

请注意,这些作者创建此 FTP 很大程度上是出于工效学原因。

文档和示例

我们预计会对 FIDL 属性、语法、语言和电汇格式文档进行更改。还应更新 API 可读性评分准则文档,如 Selector 部分中所述。

向后兼容性

  • 从设计上讲,经过哈希处理的序数与手动指定的序数不兼容 ABI。我们希望这并不是问题,因为
    • fidlc 更改是二进制数据(将使用经过哈希处理的异或手动序数),并且
    • fidlc 用于构建整个树,因此
    • 树的所有部分都将一致地使用所选的序数架构。
  • 经过哈希处理的序数与 API(来源)兼容。现有源文件将保持兼容;将弃用手动序数(请参阅“实现策略”)。
  • 如果使用了两个不同的 fidlc build(即平台源代码树的两个不同 build),并且使用了 FIDL 接口在机器之间进行通信,就会发生错误。作者知道目前还没有此功能的使用,因此这应该不是问题。

性能

我们预计 fidlc 的运行速度明显减慢,因为它现在必须对接口中的所有方法名称进行哈希处理才能计算它们。

我们预计,对运行时性能的影响不会明显。编译器可能会为手动指定的序数生成跳转表,这些序数之前是小型连续序数;在使用哈希序数时,这些序数将通过稀疏序数空间变为二进制搜索。同一机制也可能会以无关紧要的方式影响二进制文件的大小。(表驱动的调度可能缓解大小和速度问题。)

安全性

我们预计不会出现运行时安全问题,因为除了更改通过网络发送的序数值之外,序数哈希处理没有任何运行时更改。

使用加密哈希 (SHA-256) 可能会让某些人认为哈希需要具有强加密强度;我们认为不存在安全问题,原因如下:

  • FIDL 编译器将在编译时检查哈希冲突,并需要人工输入来解决这些问题,并且
  • 我们使用 SHA-256 不是出于加密目的,而是希望获得一个极不会导致冲突的哈希。CRC-32(甚至 strlen())也可以正常运行,但可能会导致更多碰撞,这实在是不便。

SHA-256 哈希的截断可能也会导致一些问题,不过,我们并不认为存在安全问题,因为 FIDL 编译器会静态检查是否存在哈希冲突5

测试

ianloic@google.com 分析了现有的 FIDL 接口,并确定了没有任何哈希冲突。

我们会仔细考虑如何测试实际哈希冲突的情况,因为通过人为生成具有良好哈希的哈希冲突(从设计上来说)很困难。

否则,典型的单元测试、CQ 测试、兼容性测试和手动测试的电池应该足以确保序数哈希处理的可靠性。

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

此 FTP 专门处理接口的序数哈希处理。 它不建议更改枚举、表或可扩展联合的手动枚举序数。

jeffbrown@google.com 建议采用完美哈希技术,并考虑了此问题。FTP 作者不太熟悉完美的哈希方案,但他们认为随着时间的推移,添加额外的方法会改变现有方法的哈希,进而破坏 ABI 兼容性,使得完美的哈希不适合。动态完美哈希或许是可行的,但也会引发同样的问题,即改变哈希。此外,标准哈希并不为人熟知,也比标准哈希更复杂,因此无需做进一步研究。

移除手动序数的另一种方法是通过网络发送完整的方法名称,许多(大多数?)其他 RPC 系统都是这么做的(请参阅下面的参考资料)。这会对运行时性能造成影响,明显与 FIDL 的预期用例相冲突。

我们曾考虑过能够指定所使用的哈希,以便日后在 SHA-256 最终遇到其他哈希可以解决的问题时可以进行更改。这种设计在安全应用中很常见,广泛使用的加密哈希在后面会存在漏洞。但是,指定哈希可能需要更改传输格式,并且需要所有语言绑定来实现代码以选择哈希算法,从而使编译器和绑定代码变得非常复杂。我们觉得这种取舍不值得。 我们认识到,git 也对 SHA-1 采取了这种态度,现在在一定程度上回溯这一决策,但我们认为我们的用例差异足够多,足以证明对哈希算法进行硬编码的合理性。

探索

  • 通过节省空间的方法标识方法,可以实现方法的高效一级表示法,使方法成为头等。
    • 例如,这可以启用一些方法,使其在 FIDL 调用中用作参数,或者让 FIDL 方法因此返回其他方法。 毫无疑问,现在存在这样一些用例,即方法会返回一个接口,该接口将有一个方法作为返回实际方法的代理。
  • 提议的 31 位哈希可以扩展为,例如,64/128/53 位;SHA-256 提供了很多位。
  • ordinal 重命名为 selector,后者是一个现有概念,在其他语言和组件系统中具有相同的用途。
  • 可能有必要区分方法名称和接口名称,因此我们有两项不同的数据。 这样,您就可以唯一地引用接口名称,并且能够唯一地引用方法名称。为此,我们可能需要超过 32 位。
  • 如上所述,枚举、表和可扩展联合体不在范围内。 不过,我们确实认为此 FTP 也适用于他们。 初步想法:
    • 我们不确定枚举是否需要此功能。 更简单、标准化的连续整数编号似乎已足够。
    • 这可能会按原样应用于可扩展联合。
    • 表需要不同的传输格式才能采用序数哈希处理,因为由于采用打包表示法,序数当前需要是连续的。
  • FIDL 目前预留了序数高位,并在文档中明确指出,高位范围将用于控制流等。作者认为,这种情况的原因之一可能也与序数冲突有关。我们是否想要回顾一下这些内容?
    • 将序数空间扩展到 64 位(如上所述)可以在很大程度上解决此问题。
    • 建议在 Fuchsia IPC 聊天室中仅预留 0xFFFFxxxxabarth@google.com
  • 我们可以在计算出的哈希值中包含方法的参数类型,以便将来需要时支持方法重载。
    • jeffbrown@google.com 提到,对完整的方法签名进行哈希处理可能会限制接口扩展的机会,并且映射在许多编程语言上会严重过载。
  • 由于序数哈希应该能够解决使用接口继承时序数冲突的问题,因此 FragileBase 属性也可能会被移除。
    • 代码搜索会显示 FragileBase 的大约 9 种使用情况。
  • 作者担心,如果许多方法都具有 Selector 属性,那么随着时间发生显著变化的接口可能会变得难以阅读。
    • 解决此问题的一种方法是采用类似于 Objective-C 类别C# 部分类的内容,其中已有已声明的接口可以“扩展”,以便在单独的声明中添加属性。

早期技术和参考资料

有趣的是,我们不知道有任何其他方法调度或 RPC 系统使用方法名称的哈希值来标识要调用的方法。

大多数 RPC 系统按名称(如 gRPC/Protobuf 服务、Thrift、D-Bus)调用方法。对于进程内方法调用,Objective-C 会使用保证唯一 char* 指针值(称为选择器)来标识应在类上调用的方法。Objective-C 运行时可以将选择器映射到字符串化的方法名称,反之亦然。对于进程外方法调用,Objective-C 分布式对象使用方法名称进行调用。COM 直接将 C++ vtable 用于进程内调用,因此依赖 ABI 和编译器支持来支持方法调度。apang@google.com 建议对 ctiller@google.com 的 Phickle 提案中的表进行序数哈希处理。ianloic@google.com 和 apang@google.com 已于 2018 年 4 月 10 日与 this.board 会面


  1. Mojo/FIDL1 也不要求程序员指定序数,而是按顺序生成序数(类似于 FlatBuffers 针对表字段的隐式标记编号)。 

  2. 以前,您可以创建一个从您喜欢的任何其他 FIDL 接口继承的 FIDL 接口。不过,接口和超接口共用相同的序号空间,这意味着,如果您向接口添加了方法,则可能会破坏其他一些很远的库中的子接口。有一些提案涉及 FIDL-land,用于解决继承 / 序数冲突问题,但在我们弄清楚如何解决这个问题之前,我们已将接口的默认设置切换为禁止继承。接口仍然可以使用 [FragileBase] 属性选择允许添加子接口。如果您遇到此问题,编译器应输出错误消息及简要说明。 我 (abarth@google.com) 在所有使用平台源代码树中的 FIDL 接口继承的任何位置都添加了 [FragileBase] 属性(希望如此)。如果您有任何疑问或遇到任何问题,请与我联系。 --abarth@google.com

  3. 我们不认为存在足够的序数冲突,并不意味着自动解决冲突会导致任何额外的实现和认知复杂性。如果数据表明序数冲突变得存在问题,我们可以重新审视这一决定,而不会破坏向后兼容性。 

  4. 如果只声明结果,相应方法就会称为事件。然后定义来自服务器的垃圾消息。 

  5. jln@google.com 写道:“可以,截断 SHA-2 是可以的,并且不,无论在哪里截断。”