RFC-0050:FIDL 语法修订

RFC-0050:FIDL 语法改进
状态已接受
区域
  • FIDL
说明

我们制定了语法选择的指导原则,并根据这些原则做出了一些语法更改。

作者
提交日期(年-月-日)2020-02-26
审核日期(年-月-日)2019-06-04

摘要

我们制定了语法选择的指导原则,并根据这些原则做出了一些语法更改。

变更

  • 将类型放在第二位,例如在方法参数中,名称位于其各自类型之前;在表声明中,成员名称位于其各自类型之前;
  • 更改类型以将布局与约束条件分开,将与布局相关的类型信息放在 : 分隔符的左侧,并将约束条件信息放在右侧,例如 array<T, 5>vector<T>:5 更清楚地传达了数组的大小会影响布局,而对于矢量,它是约束条件。
  • 引入了匿名布局。例如,table { f1 uint8; f2 uint16; } 可直接在方法参数列表中使用。
  • 您可以借助形式为 type Name = Layout;类型引入声明,使用匿名布局声明顶级类型。
  • 最后,对于协议 P Prequest<P> 分别重命名为 client_end:Pserver_end:P。请注意,协议是客户端或服务器端的约束条件,而不是之前的位置,因为之前的位置会错误地指明布局相关问题。

与其他 RFC 的关系

此 RFC 后来被以下 RFC 修订:

设计初衷

入门示例

代数数据类型

该语法足够灵活,可以流畅地表示代数数据类型 (ADT),而无需任何其他糖衣。例如:

/// Describes simple algebraic expressions.
type Expression = flexible union {
    1: value int64;
    2: bin_op struct {
        op flexible enum {
            ADD = 1;
            MUL = 2;
            DIV = 3;
        };
        left_exp Expression;
        right_exp Expression;
    };
    3: un_op struct {
        op flexible enum {
            NEG = 1;
        };
        exp Expression;
    };
};

从模式上讲,我们选择使用 structunionunion 具有可扩展性,因此无需(并且最好)使用更严格的变体。如果需要更改变体,我们可以改为批量添加新的变体,并改用此新变体。(在需要可扩展性的其他位置,例如二元或一元运算符列表,则选择灵活的枚举。)

除了人体工学语法来描述数据类型之外,支持 ADT 还需要更多内容。例如,预期的一个关键功能是轻松构建和销毁(例如通过模式匹配或访问者模式)。

此 RFC 不会向 FIDL 引入新功能,并且对递归类型的限制会导致该示例无法编译。我们计划添加对泛型递归类型的支持,此扩展将成为未来 RFC 的对象。

更轻松地将不可演化消息与可演化消息组合使用

例如,表达一个既具有结构体元素(紧凑、内嵌、快速编码/解码)又具有可扩展性的“可扩展结构体”:

type Something = struct {
    ...

    /// Provide extension point, initially empty.
    extension table {};
};

例如,fuchsia.test.breakpoints 库需要定义一个名为 Invocation 的可扩展事件。这些事件都具有共同的值,以及事件的每个变体对应的特定载荷。现在,这可以更直接、更简洁地表示为:

type Invocation = table {
    1: target_moniker string:MAX_MONIKER_LENGTH;
    2: handler Handler;
    3: payload InvocationPayload;
};

type InvocationPayload = union {
    1: start_instance struct{};
    2: routing table {
        1: protocol RoutingProtocol;
        2: capability_id string:MAX_CAPABILITY_ID_LENGTH;
        3: source CapabilitySource;
    };
};

可扩展的方法参数

例如,可扩展的方法参数:

protocol Peripheral {
    StartAdvertising(table {
        1: data AdvertisingData;
        2: scan_response AdvertisingData;
        3: mode_hint AdvertisingModeHint;
        4: connectable bool;
        5: handle server_end:AdvertisingHandle;
    }) -> () error PeripheralError;
};

使用 table 作为参数不是“最佳实践”。这种方法可能适用,但也存在一些问题,例如,如果有 N 个字段,则有 2N 种可能的组合,这可能会给收件人带来很多复杂性。

指导原则

FIDL 主要关注定义应用二进制接口 (ABI) 问题,其次是应用编程接口 (API) 问题。这可能会导致语法比您可能习惯的或与其他编程语言相比预期的要冗长。例如,联合体的 unit 变体将表示为空结构体,如上文的 InvocationPayload 示例所示。我们可以选择引入语法糖来省略此类型,但这与突出显示 ABI 问题的做法背道而驰。

将布局与约束条件分离

对齐语法

    layout:constraint

对于类型(即控制布局的任何内容),应放在冒号前面;对于控制约束条件的任何内容,应放在冒号后面。布局描述的是字节的布局方式,而不是字节的解释方式。约束条件会限制在给定布局下可以表示的内容,它是在编码/解码期间执行的验证步骤。

这种语法提供了一种简化的方式来考虑更改对 ABI 的影响,尤其是会导致两个简写规则:

  1. 如果两种类型的布局不同,则无法从一种类型平滑过渡到另一种类型,反之亦然 1,即更改左侧会破坏 ABI
  2. 约束条件可以演变,只要写入器的约束条件比读取器的约束条件更严格,一切就都兼容,即可以演变右侧并保留 ABI

以下是遵循此原则的更改示例:

  • array<T>:N 变为 array<T, N>
  • handle<K> 变为 handle:K
  • vector<T>? 变为 vector<T>:optional
  • Struct? 变为 box<Struct>
  • Table? 变为 Table:optional
  • Union? 变为 Union:optional

本 RFC 的设计部分对这些变更进行了讨论。

先使用二进制线格式

虽然许多格式都可以表示 FIDL 消息,但 FIDL 线格式(或“FIDL 二进制线格式”)是优先处理的格式,系统会优先处理此格式。

这意味着,为了使语法一致性与 ABI 一致性保持一致,语法选择应考虑二进制线格格式下的 ABI(而不是 JSON 等其他格式)。

例如,对于类型的 ABI,名称无关紧要;对于协议和方法,名称很重要。虽然名称对于可能的 JSON 格式可能很重要,但在选择语法时,我们会过度倾向于二进制 ABI 格式,并且如果语法会妨碍理解 ABI 规则,我们不会为了优先使用文本表示法而更改语法。

功能最少

赖特的“形式和功能应合二为一”原则促使我们努力让外观相似的构造具有相似的外观含义,反之亦然。例如,所有在内部利用封装容器的可扩展数据始终都使用 ordinal: 进行呈现。

layout {
    ordinal: name type;
};

我们会尽量减少功能和规则的数量,并力求通过组合功能来实现使用场景。在实践中,在考虑新功能时,我们应先尝试调整或推广其他现有功能,而不是引入新功能。例如,虽然可以为可扩展的方法参数(和返回值)设计特殊语法(如 RFC-0044:可扩展的方法参数中所述),但我们更倾向于使用 table 和常规语法。

有人可能会认为,我们甚至应该要求方法请求和响应使用匿名 struct 布局,而不是使用从大多数编程语言借用的参数语法糖。不过,另一个设计注意事项是帮助库作者在总体上实现一致性:在 enum 布局声明中,我们更倾向于使用语法糖,而不是明确选择封装类型,因为合理的默认值可为各个 FIDL 库中的枚举提供更大的一致性。这反过来又提供了一个迁移路径,以便日后切换枚举,例如,如果某个库定义了通用 ErrorStatus 枚举,则可以稍后将其替换为另一个“更优”的通用 ErrorStatusV2

设计

类型

类型遵循以下一般形式:

Name<Param1, Param2, ...>:<Constraint1, Constraint2, ...>

空类型参数化必须省略 <>,即 uint32(而非 uint32<>)。

没有约束条件的类型必须同时省略 : 分隔符和 <>,即 uint32(而非 uint32:<>uint32:)。

具有单个约束条件的类型可以忽略 <>,即允许使用 vector<uint32>:5vector<uint32>:<5>,并且二者等效。

内置

支持以下基元类型

  • 布尔值 bool
  • 有符号整数 int8int16int32int64
  • 无符号整数 uint8uint16uint32uint64
  • IEEE 754 浮点 float32float64

固定大小的重复值

array<T, N>

这可以视为一个类型为 TN 元素的 struct

大小可变的重复值

vector<T>
vector<T>:N

即可以省略大小 N

大小可变的 UTF-8 字符串

string
string:N

即可以省略大小 N

对内核对象(即句柄)的引用

handle
handle:S

其中,子类型 S 可以是 btibufferchanneldebuglogeventeventpairexceptionfifoguestinterruptiommujobpagerpcidevicepmtportprocessprofileresourcesocketsuspendtokenthreadtimervcpuvmarvmo 中的一种。

RFC-0028:手柄权限中引入了具有权限的手柄:

handle:<S, R>

其中,权限 R 是权限值或权限表达式。

对协议对象(即目标用途的频道句柄)的引用

client_end:P
server_end:P

例如 client_end:fuchsia.media.AudioCoreserver_end:fuchsia.ui.scenic.Session

具体而言,单独引用协议是不合法的:协议声明不会引入类型,而只会引入可被视为一种客户端或服务器端的项。传输通用化部分对此进行了详细讨论。

布局

除了内置布局之外,我们还有 5 种布局可配置为引入新类型:

  • enum
  • bits
  • struct
  • table
  • union

有限布局

enumbits 布局的表示方式类似:

layout : WrappedType {
    MEMBER = expression;
    ...;
};

其中 : WrappedType 是可选的 [^2],如果省略,则默认为 uint32

示例 enum

enum {
    OTHER = 1;
    AUDIO = 2;
    VIDEO = 3;
    ...
};

示例 bits

bits : uint64 {
    TOTAL_BYTES = 0x1;
    USED_BYTES  = 0x2;
    TOTAL_NODES = 0x4;
    ...
};

灵活布局

tableunion 布局的表达方式类似:

layout {
    ordinal: member_name type;
    ...;
};

在这里,ordinal: 可视为用于描述 envelope<type> 的语法糖。

对于表,成员通常称为字段。对于联合体,成员通常称为变体。此外,成员可能还会被预留:

layout {
    ordinal: reserved;
    ...
};

固定布局

唯一的固定布局 struct 的表达方式与灵活布局类似,但不使用灵活的表示法:

layout {
    member_name type;
    ...;
};

对于结构体,成员通常称为字段。

属性

布局可能在该布局的属性之前:

[MaxBytes = "64"] struct {
    x uint32;
    y uint32;
};

这样,您就可以将属性明确地附加到布局的成员和该成员的类型:

table {
    [OnMember = "origin"]
    1: origin [OnLayout] struct {
        x uint32;
        y uint32;
    };
};

如果引入了布局这一新类型,则新引入的类型上的属性有两种可能的放置位置:

  • 新类型:[Attr] type MyStruct = struct { ... }
  • 在布局上:type MyStruct = [Attr] struct { ... }

fidlc 会将这两者视为等效,如果同时在两个位置指定属性,则会引发错误。

无论使用哪种展示位置来指定属性,这些属性在概念上都附加到布局本身,而不是整个类型诗节。下面是一个实际应用示例:在任何 IR 中,首选是将类型诗节中的属性向下移至布局,而不是将布局中的属性向上移至类型诗节。

命名上下文和布局的使用

布局本身没有名称,从某种意义上讲,所有布局都是“匿名的”。 而是一种布局的特定用法,它决定了布局在目标语言中的名称。

例如,布局最常见的用途是引入新的顶级类型:

library fuchsia.mem;

type Buffer = struct {
    vmo handle:vmo;
    size uint64;
};

在这里,结构体布局用于顶级库中的“新类型”声明。

在介绍如何表达可扩展的方法参数的备注中,我们介绍了在匿名上下文中的使用示例:

library fuchsia.bluetooth.le;

protocol Peripheral {
    StartAdvertising(table {
        1: data AdvertisingData;
        2: scan_response AdvertisingData;
        3: mode_hint AdvertisingModeHint;
        4: connectable bool;
        5: handle server_end:AdvertisingHandle;
    }) -> () error PeripheralError;
};

在这里,表格布局在 StartAdvertising 方法的请求中使用,位于 Peripheral 协议声明中。

我们将名称列表(从最不具体到最具体)称为“命名上下文”,用于标识布局的用途。在上述两个示例中,我们分别使用 fuchsia.mem/Bufferfuchsia.bluetooth.le/Peripheral, StartAdvertising, request 作为两个命名上下文。

在 JSON IR 中,布局声明将包含其命名上下文,即上面所述的层次结构名称列表。

命名上下文

在库 some.library 中,type Name = 声明会为 some.library/Name 引入命名上下文。

在请求(或响应)中使用 Protocol 中的 Method 会引入 some.library/Protocol, Method, request/response 的命名上下文

在布局中使用会将字段名称(或变体名称)添加到命名上下文。例如:

type Outer = struct {
    inner struct {
        ...
    };
};

第一个外部结构体布局的命名上下文为 some.library/Outer,第二个内部结构体布局的命名上下文为 some.library/Outer, inner

生成的扁平化名称

许多目标语言都可以以分层方式表示命名上下文。例如,在 C++ 中,可以在封闭类型中定义类型。不过,某些目标语言不具备此能力,因此我们必须考虑因扁平化命名上下文而导致的名称冲突。

例如,请考虑命名上下文 some.library/Protocol, Method, request。在 Go 中,这可能会扁平化为 some.library/MethodRequestOfProtocool。如果某个其他定义恰好使用了命名上下文 some.library/MethodRequestOfProtocool,那么 Go 绑定就会遇到一个难题:必须重命名这两个声明中的某一个。最糟糕的是,如果一个包含一个声明(没有名称冲突)的库演变为包含两个声明(存在名称冲突)的库,那么 Go 绑定必须与之前生成的内容保持一致,以避免发生破坏源代码的更改。

我们的经验表明,最好将这些决策交给核心 FIDL 编译器,而不是将其委托给工具链中的 FIDL 绑定。因此,我们将计算并保证一个稳定的扁平化名称。

在 JSON IR 中,命名上下文将包含生成的扁平化名称,编译器保证该名称在全局范围内是唯一的,即前端编译器负责生成扁平化名称,并验证扁平化名称不会与其他声明(无论是其他扁平化名称还是顶级声明)冲突。

以前面的示例为例,如果库作者添加的声明 type MethodRequestOfProtocool = ... 与另一个声明生成的扁平化名称冲突,则编译将会失败。

通过绑定使用命名上下文

绑定大致可分为两类:

  1. 能够在目标语言中表示命名上下文作用域,例如 C++ 语言的绑定;
  2. 无法表示命名上下文,并回退到使用生成的扁平化名称,例如 Go 语言的绑定。

这比目前的情况有所改进,因为我们至少可以保证绑定之间的一致性,并在前端提供编译器帮助。目前,我们必须在游戏后期(在后端)生成一些名称,这是一种危险且容易出错的方法。

例如,请考虑以下定义:

type BinOp = union {
    add struct {
        left uint32;
        right uint32;
    };
};

在 C++ 绑定中,我们最终可能会:

class BinOp {
    class Add {
        ...
    };
};

变体 add 的访问器为:

BinOp.add();

且不会与类定义冲突。

或者,在 Go 中,使用扁平化名称:

type BinOp struct { ... };
type BinOpAdd struct { ... };

如果库作者稍后决定引入名为 BinOpAdd 的顶级声明,前端编译器会捕获此声明并将其报告为错误。库作者可以控制并仔细考虑这项更改的影响,并可以选择决定是否为了引入此新声明而破坏源代码兼容性。同样,这比当前情况有所改进,当前情况是,此类源代码兼容性问题被发现的时间较晚,距离决策制定的时间较远。

类型别名和新类型

RFC-0052:类型别名和新类型中,我们改进了类型别名和新类型声明。

别名声明如下:

alias NewName = AliasedType;

即与 RFC-0052 中提议的语法保持不变。

新类型的声明方式如下:

type NewType = WrappedType;

也就是说,无论封装的类型是另一种现有类型(封装)还是某种布局(新的顶级类型),新类型的语法都是相同的。这与 RFC-0052 中最初提议的语法不同。

可选性

某些类型本身就是可选的:vectorsstringsenvelopes 以及使用此类结构的布局,例如 table(一种封装容器的矢量)和 union(一个标记和一个封装容器)。因此,这些类型是可选类型还是必需类型是一个约束条件,可以演变为(通过放宽约束条件而变为可为 null)或演变出(通过收紧约束条件而变为必需)这些类型。

另一方面,int8struct 布局等类型本身无法作为可选项。为了实现可选性,需要引入间接引用,例如在结构体情况下通过间接引用。因此,与本质上是可选的类型不同,此类类型没有任何演变路径。

为了区分这两种情况,并遵循将 ABI 问题“放在左侧”且“可演化问题放在右侧”的原则,请执行以下操作:

当然是可选的 并非自然可选
string:optional box<struct>
vector:optional
union:optional

在命名方面,我们更倾向于使用“可选”“必需”“存在”“不存在”等字词。 (我们应避免使用“可为 null”“不可为 null”“null 字段”等字词。)为了遵循这种命名偏好,我们选择了 box<T>,而不是 pointer<T>box 默认是可选结构,即新语法中的 box<struct> 等同于旧语法中的 struct?,而 box<struct>:optional 是多余的,可能会触发编译器或 lint 工具发出警告。这是为了更好地匹配我们预期的用例:用户通常封装结构体是为了实现可选性,而不是添加间接性。

常量

常量声明如下:

const NAME type = expression;

约束条件排序

基于布局和约束条件对类型进行参数化时,对于给定类型,这些参数的顺序是固定的。此 RFC 定义了以下约束条件顺序(目前没有任何类型具有多个布局参数):

  • 手柄:子类型、权限、可选性。
  • 协议 client/server_end:协议,可选性。
  • 矢量:大小、可选性。
  • 联合:可选性。

作为指导原则,可选性始终放在最后,对于句柄,子类型位于权限之前。

例如,请考虑以下结构体,其中对其成员定义了所有可能的约束条件:

type Foo = struct {
  h1 zx.handle,
  h2 zx.handle:optional,
  h3 zx.handle:VMO,
  h4 zx.handle:<VMO,optional>,
  h5 zx.handle:<VMO,zx.READ>,
  h6 zx.handle:<VMO,zx.READ,optional>,
  p1 client_end:MyProtocol,
  p2 client_end:<MyProtocol,optional>,
  r1 server_end:P,
  r2 server_end:<MyProtocol,optional>,
  s1 MyStruct,
  s2 box<MyStruct>,
  u1 MyUnion,
  u2 MyUnion:optional,
  v1 vector<bool>,
  v2 vector<bool>:optional,
  v3 vector<bool>:16,
  v4 vector<bool>:<16,optional>,
};

未来方向

除了对现有功能的语法进行更改之外,我们还会研究并确定即将推出的功能的方向。本文重点介绍了预期的表达能力及其语法呈现(而非精确的语义,这需要单独的 RFC)。例如,在介绍传输通用化时,我们不会讨论各种棘手的设计问题(例如可配置性程度、JSON IR 中的表示法)。

本部分也应被视为指南,而非未来规范。随着新功能的引入,我们会评估相应的语法以及这些功能的确切运作方式。

内容相关名称解析

例如

const A_OR_B MyBits = MyBits.A | MyBits.B;

会简化为:

const A_OR_B MyBits = A | B;

例如

zx.handle:<zx.VMO, zx.rights.READ_ONLY>

会简化为:

zx.handle:<VMO, READ_ONLY>

约束条件

声明网站限制

type CircleCoordinates = struct {
    x int32;
    y int32;
}:x^2 + y^2 < 100;

使用网站约束条件

type Small = struct {
    content fuchsia.mem.Buffer:vmo.size < 1024;
};

独立约束条件

constraint Circular : Coordinates {
    x^2 + y^2 < 100
};

对信封的限制

表和可扩展联合体的语法会隐藏封装容器的使用:

  • tablevector<envelope<...>>,且
  • unionstruct { tag uint64; variant envelope<...>; }

目前,封装容器仅存在于 tableunion 声明中显示的 ordinal: 中,因此将此语法视为封装容器的“糖衣”引入会很有帮助。从本质上讲,我们可以按如下方式去糖:

对表格和灵活联合体进行脱糖
table ExampleTable {
    1: name string;
    2: size uint32;
};
table ExampleTable {
    @1 name envelope;
    @2 size envelope;
};
union ExampleUnion {
    1: name string;
    2: size uint32;
};
union ExampleUnion {
    @1 name envelope;
    @2 size envelope;
};

如果我们想对 envelope 进行约束,例如将某个元素的 require 设为 1,则可以将此约束条件放在序数 ordinal:C 上,例如:

对表格和灵活联合体进行脱糖
table ExampleTable {
    1:C1 name string:C2;
    2:C size uint32;
};
table ExampleTable {
    @1 name envelope<string:C2>:C1;
    @2 size envelope:C;
};
union ExampleUnion {
    1:C1 name string:C2;
    2:C size uint32;
};
union ExampleUnion {
    @1 name envelope<string:C2>:C1;
    @2 size envelope:C;
};

属性

FIDL 的类型系统已经包含约束条件的概念。我们使用 vector<uint8>:8 表示矢量最多有 8 个元素,使用 string:optional 表示放宽可选性约束条件,允许字符串为可选项。

各种需求促使我们开发更具表现力的约束条件,并对如何统一和处理这些约束条件形成了主观看法。

例如,fuchsia.mem/Buffer 指出“此大小不得大于 VMO 的实际大小”。我们正在努力引入 RFC-0028:句柄权限,即限制句柄。或者,考虑要求使用表格字段,即限制在原本可选的封装容器中使用表格字段。

目前,无法描述要操控的值或实体的运行时属性。虽然 string 值具有大小,但无法为其命名。虽然 handle 具有关联的权限,但也无法为这些权限命名。

为了妥善解决与受限类型相关的表达能力问题,我们必须先将值的运行时方面与 FIDL 对这些值的有限视图联系起来。我们计划引入 **属性 **,这可以被视为附加到值的虚拟字段。属性对线格格式没有影响,它们只是语言级构造,并会显示在 JSON IR 中,以便为绑定赋予运行时含义。属性的存在目的仅在于表达对它们的约束条件。每个属性都需要为绑定所知,就像绑定知道内置项一样。

继续使用上面的示例,string 值可以具有 uint32 size 属性,句柄可以具有 zx.rights rights 属性。

例如:

layout name {
    properties {
        size uint32;
    };
};

运输泛化

声明新的传输层至少需要定义一个新名称,为传输层支持的消息指定约束条件(例如“无句柄”“无表格”),并为协议指定约束条件(例如仅使用“火花式方法”“无事件”)。

预期的语法类似于使用无类型 FIDL 文本表示的配置:

transport ipc = {
    methods: {
        fire_and_forget: true,
        request_response: true,
    },
    allowed_resources: [handle],
};

然后使用以下方式:

protocol SomeProtocol over zx.ipc {
    ...
};

处理泛化

目前,句柄完全是 Fuchsia 特有的概念:它们直接与 Zircon 内核相关联,映射到 zx_handle_t(或 C 以外其他语言中的等效项),并且其类型仅限于内核公开的对象,例如 portvmofifo 等。

考虑其他情况(例如进程通信)时,一个理想的扩展点是能够直接在 FIDL 中定义句柄,而不是将其作为语言定义的一部分。

例如,定义 Zircon 句柄:

library zx;

resource handle : uint32 {
    properties {
        subtype handle_subtype;
        rights rights;
    };
};

type handle_subtype = enum {
    PROCESS = 1;
    THREAD = 2;
    VMO = 3;
    CHANNEL = 4;
};

type rights = bits {
    READ = ...;
    WRIE = ...;
};

这将允许 handlehandle:VMO(或在其他库 zx.handle:zx.handle.VMO 中)。

有一个实验性实现,将用于打破 Zircon 和 FIDL 之间的循环依赖项(在此更改之前,Zircon 的 API 在 FIDL 中进行了描述,但 FIDL 在一定程度上是根据 Zircon 的 API 定义的)。

实施策略

系统会在所有 .fidl 文件的开头添加临时的“版本声明”,以供 fidlc 使用,以检测 .fidl 文件是采用旧语法还是新语法。

此令牌将位于库语句的紧前面:

// Copyright notice...

deprecated_syntax;

library fidl.test;
...

建议使用显式标记,以简化 fidlc 在检测语法中的角色并提高可读性。检测语法时遇到的挑战的一个示例是,将表达式解读为任一语法都会导致编译错误。在这些情况下,需要使用启发词语来决定使用旧语法还是新语法,这可能会导致意外结果。

此外,此令牌会添加到之前语法(而非新语法,例如 new_syntax;")中的所有文件中,以便让用户了解即将进行的迁移 - FIDL 文件的读者会意识到语法即将发生变化,并可以通过其他渠道(例如文档、邮寄名单)寻求更多背景信息。

我们将添加一个新的 fidlconv 主机工具,该工具可以将旧格式的 FIDL 文件转换为新格式的文件(在本部分中称为 .fidl_new)。虽然此工具与 fidlc 是分开的,但它需要利用编译器的内部表示法才能正确执行此转换。例如,只有当类型 Foo 是协议时,才需要将其转换为 client_end:Foo,以确定 fidlconv 情形是否会先利用 fidlc 编译 FIDL 库。

FIDL 前端编译器 fidlc 以及格式设置工具和 lint 工具等随附工具将根据上面定义的标记扩展为支持任一语法。

添加此功能后,构建流水线将会如下扩展:

可视化:构建流水线策略

即:

  • fidlconv 工具会将采用旧版语法的 FIDL 文件转换为采用新版语法的文件。
  • fidlc 编译器将通过编译旧语法输出 .json
  • 此外,fidlc 编译器将通过编译新语法输出 .json IR。
  • fidlfmt 格式化程序将设置生成的新库文件 .fidl_new 的格式。

对于测试和验证:

  • 系统会比较这两个 JSON IR,并验证它们是否匹配(除了 span 信息外)。
  • 系统将验证新库文件格式的幂等性,以检查使用新语法的 fidlc 编译器和 fidlfmt 格式设置的输出。

在此实现过程中,FIDL 团队还将编码表后端移至独立二进制文件(与其他后端相同),并通过生成最后一次使用情况并在 fuchsia.git 树代码库中检查这些使用情况来废弃并删除 C 绑定后端。

工效学设计

本文档介绍了人体工学。

我们愿意让熟悉当前语法的开发者在重新学习使用这种经过修改的语法时,短期内效率有所下降,因为我们坚信,未来将有更多开发者使用 FIDL,他们将因此受益匪浅。

文档和示例

这需要更改以下内容:

向后兼容性

此更改不向后兼容。如需了解转换计划,请参阅“实现”部分。

性能

此更改对性能没有影响。

安全

此项更改对安全性没有影响。

测试

如需了解转换计划以及如何验证其正确性,请参阅“实现”部分。

缺点、替代方案和未知

使用英文冒号将名称与类型分隔开来

由于我们将类型移到了第二位,因此我们还可以考虑使用非常常见的 : 分隔符,就像类型理论、Rust、Kotlin、ML 语言(SML、Haskell、OCaml)、Scala、Nim、Python、TypeScript 等众多语言中所做的那样:

    field: int32 rather than the proposed field int32

此提案拒绝了这种方法。

: 分隔符主要用于将布局与约束条件分隔开来。它还用于指明 enumbits 声明的“封装类型”。最后,它用于在 tableunion 声明中表示信封。进一步重载 : 分隔符(尤其是在语法上与其主要用途相近的情况下),会导致混淆(例如表成员 1: name: string:128;)。

省略分号

我们已讨论过,要努力省略结束声明(无论是成员、const 还是其他)的分号。

本提案选择不探讨这种简化。

移除英文分号对 FIDL 作者的语法影响不大。这也不是一项关键的更改,如果我们日后想要探索这一点,可以轻松进行修改(例如,Go 移除英文分号的方法)。

不过,使用英文分号来终止成员和声明,可以更轻松地保证语法规则不含歧义,尤其是在我们探索约束条件(使用位置和声明位置)时。例如,使用声明式网站布局约束条件 (C) 时(例如 struct Example { ... }:C;),我们可以在 : 分隔符和 ; 终止符之间清晰地划定约束条件。

统一枚举和联合体

从类型理论的角度来看,枚举表示单元类型的总和,而联合表示任何类型的总和。因此,人们很容易试图将这两个概念合二为一。支持 ADT 的编程语言(例如 ML 或 Rust)采用的就是这种方法。

不过,从布局的角度来看,仅包含单元类型(枚举)的总和类型的表示方式比可扩展的对应项(联合体)更高效。虽然这两种结构体在添加新成员时都支持扩展,但只有联合体支持从单元类型(例如 struct {})扩展到任何类型。这种可扩展性需要使用内嵌信封。

我们选择了一种实用的方法,在使用两个结构的复杂性与特殊大小写枚举的性能优势之间取得了平衡。

参考文档

语法简介

可扩展的方法参数

类型别名和命名类型

Footnote2

虽然更看重语法简洁性而非明确选择封装类型可能看起来很奇怪,但拥有合理的默认值可为各个 FIDL 库中的枚举提供更大的一致性。这为日后切换枚举提供了迁移路径,例如,如果某个库定义了通用的 ErrorStatus 枚举,则可以稍后将其替换为另一个“更好”的通用 ErrorStatusV2


  1. 或者至少,在充分了解接口格式并谨慎操作的情况下,例如: https://fxrev.dev/360015