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 主要关注 Application Binary Interface (ABI) 问题,其次关注 Application Programming Interface (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>

这可以视为具有 NT 类型元素的 struct

可变大小的重复值

vector<T>
vector<T>:N

也就是说,可以省略大小 N

可变大小的 UTF-8 字符串

string
string:N

也就是说,可以省略大小 N

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

handle
handle:S

其中,子类型 Sbtibufferchanneldebuglogeventeventpairexceptionfifoguestinterruptiommujobpagerpcidevicepmtportprocessprofileresourcesocketsuspendtokenthreadtimervcpuvmarvmo 之一。

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

handle:<S, R>

其中,权限 R 可以是权限值,也可以是权限表达式。

对协议对象的引用,即目标用途的渠道句柄

client_end:P
server_end:P

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

具体来说,单独引用协议是不合法的:协议声明不会引入类型,只会引入可视为某种客户端或服务器端的内容。如需详细了解相关内容,请参阅传输泛化部分。

布局

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

  • 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 会将这两种情况视为等效,如果在这两个位置都指定了属性,则会引发错误。

无论使用哪种放置位置来指定属性,从概念上讲,属性都是附加到布局本身,而不是整个类型 stanza。一个实际应用示例是,在任何 IR 中,最好将类型 stanza 上的属性降至布局,而不是将布局上的属性提升至类型 stanza。

命名上下文和布局的使用

布局本身不带名称,从某种意义上说,所有布局都是“匿名”的。 相反,它是一种布局的特定用法,用于确定其在目标语言中的名称。

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

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;
};

在此示例中,表格布局在 Peripheral 协议声明中用于 StartAdvertising 方法的请求中。

我们将从最不具体到最具体的名称列表称为布局的使用“命名上下文”。在上述两个示例中,我们分别将 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: 是信封存在的唯一位置,将此语法视为信封的“语法糖”引入很有用。从本质上讲,我们可以按如下方式进行去糖化:

Desugaring 表和灵活的联合
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 元素,则应将此限制条件放在序号 ordinal:C 上,如下所示:

Desugaring 表和灵活的联合
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;
    };
};

交通概化

声明新传输至少需要定义新名称、指定传输支持的消息的限制(例如“无句柄”“无表”)以及指定协议的限制(例如仅“fire-and-forget 方法”“无事件”)。

设想的语法类似于以无类型 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 以及格式化程序和代码检查器等随附工具将得到扩展,以支持基于上述标记的任一语法。

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

可视化:构建流水线策略

即:

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

对于测试和验证:

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

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

工效学设计

此 RFC 主要关注的是人体工程学。

我们愿意让熟悉当前语法的开发者在重新训练以使用此修改后的语法时,暂时损失一些效率,因为我们坚信,未来使用 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;)。

省略分号

有人讨论过要努力省略以分号结尾的声明(无论是成员、常量还是其他)。

此提案选择不探讨此简化。

对于 FIDL 作者来说,移除分号在语法上几乎没有区别。这也不是一项关键变更,如果我们将来想探索此功能,可以轻松进行修改(例如 Go 的移除分号方法)。

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

统一枚举和联合

从类型理论的角度来看,枚举表示单位类型的总和,而联合表示任意类型的总和。因此,人们很想将这两个概念统一为一个概念。这是支持 ADT 的编程语言(例如 ML 或 Rust)所采用的方法。

不过,从布局的角度来看,仅包含单位类型的总和类型(枚举)比可扩展的对应类型(联合)的表示效率要高得多。虽然两者都可以在添加新成员时提供可扩展性,但只有联合类型可以提供从单元类型(例如 struct {})到任何类型的可扩展性。这种可扩展性是以内嵌信封为代价实现的。

我们选择了一种务实的方法,即在拥有两种构造的复杂性与特殊处理枚举的性能优势之间取得平衡。

参考文档

语法方面

可扩展方法实参

关于类型别名和命名类型

Footnote2

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


  1. 或者至少,在不充分了解线框格式的情况下,不能这样做,例如 https://fxrev.dev/360015