component-model

wit格式(The wit format)

Wasm接口类型(Wasm Interface Type, WIT)格式是一种IDL,它以两种主要方式为WebAssembly组件模型提供工具:

WIT包是在同一目录下以wit为扩展名的文件集合,其定义了interfaceworld,例如foo.wit。文件编码为有效的utf-8字节。类型可以使用无限制的名称从包的接口(interface)导入,还可以使用限定的命名空间(namespace)和包(package)名从其他包导入。

本文档将介绍WIT文档句法结构的用途、非正式语法规范(pseudo-formal grammar specification),以及适合分发的WIT包的包格式(package format)规范。

包名(Package Names)

所有WIT包均分配一个包名。包名类似于foo:bar@1.0.0,包含三个字段:

🪺 使用“嵌套命名空间和包”,包名称类似于foo:bar:baz/quux,其中barfoo的嵌套命名空间、quuxbaz的嵌套包。有关更多详细信息,请参阅[包声明][package-declaration]部分。

在WIT文件顶部通过package声明指定包名:

package wasi:clocks;

或者

package wasi:clocks@1.2.0;

WIT包可以由一组文件定义,且至少有一个文件须指定包名。多个文件可以指定package,但它们必须统一包名。

或者,如果使用“显示的(explicit)”包表示法,可以在一个或多个文件中连续声明多个包:

package local:a {
  interface foo {}
}

package local:b {
  interface bar {}
}

包名用于生成组件模型中表示接口(interface)和[世界(world)]的导入名和导出名,具体描述如下

WIT接口(WIT Interfaces)

“接口(interface)”的概念是WIT的核心,它是函数(function)类型(type)的集合。接口可以被视为WebAssembly组件模型中的一个实例,例如,从宿主导入或由组件实现供宿主消费的功能单元。所有函数和类型都属于接口。

接口示例:

package local:demo;

interface host {
  log: func(msg: string);
}

表示一个名为host的接口,它提供一个函数log,该函数接受一个string参数。如果将其导入到组件中,则它将对应于:

(component
  (import "local:demo/host" (instance $host
    (export "log" (func (param "msg" string)))
  ))
  ;; ...
)

一个接口interface可以包含use语句, 类型定义和函数定义。例如:

package wasi:filesystem;

interface types {
  use wasi:clocks/wall-clock.{datetime};

  record stat {
    ino: u64,
    size: u64,
    mtime: datetime,
    // ...
  }

  stat-file: func(path: string) -> result<stat>;
}

有关use类型的更多信息将在下文中介绍,但此为interface内部项集合的示例。interface中定义的所有项(包括use),均被视为接口的导出。这意味着此interface的类型可被其他interface使用。接口具有单个命名空间,这意味着定义的名称都不会发生冲突。

WIT包可以包含任意数量的接口(interface),这些接口在顶级(top-level)列出且顺序任意。WIT验证器将确保接口之间的所有引用都是格式正确且无环的。

WIT世界(WIT Worlds)

除了interface定义之外,WIT包还可以在顶级(top-level)包含world定义。world是组件导入和导出的完整描述。world可以被视为组件模型中component类型的等价物。例如:

package local:demo;

world my-world {
  import host: interface {
    log: func(param: string);
  }

  export run: func();
}

可以视为如下组件类型(component type):

(type $my-world (component
  (import "host" (instance
    (export "log" (func (param "param" string)))
  ))
  (export "run" (func))
))

世界描述了一个具体的组件,是生成绑定的基础。来宾语言将使用world来确定导入并命名哪些函数、导出哪些函数及其名称。

世界可以包含任意数量的导入和导出,并且可以为函数(function)或接口(interface)。

package local:demo;

world command {
  import wasi:filesystem/filesystem;
  import wasi:random/random;
  import wasi:clocks/monotonic-clock;
  // ...

  export main: func(args: list<string>);
}

有关wasi:random/random语法的更多信息,请参阅下方use描述。

导入或导出接口对应于组件模型中的导入或导出实例。函数相当于裸组件函数(bare component functions)。此外,接口可以用显式的纯名称(plain name)内联定义,从而避免了外联定义需要。

package local:demo;

interface out-of-line {
  the-function: func();
}

world your-world {
  import out-of-line;
  // ... 大致相当于 ...
  import out-of-line: interface {
    the-function: func();
  }
}

importexport语句的纯名称用作最终组件importexport定义的纯名称。

在组件模型中,导入组件可以使用纯名称(plain name)或接口名称(interface name),在WIT中对应的语法:

package local:demo;

interface my-interface {
  // ..
}

world command {
  // 生成接口名称`local:demo/my-interface`的导入
  import my-interface;

  // 生成接口名称`wasi:filesystem/types`的导入
  import wasi:filesystem/types;

  // 生成纯名称`foo`的导入
  import foo: func();

  // 生成纯名称`bar`的导入
  import bar: interface {
    // ...
  }
}

每个名称在声明的范围内必须是唯一的(不区分大小写)。在world中,所有导入的名称都在同一范围内,但区分于所有导出的名称,因此同一个名称不能导入两次,但能够同时导出并导出。

通过include合并多个世界(Union of Worlds with include

可以通过合并两个或多个world来创建一个world。此操作允许从较小的world构建更大的world。

下面是一个world包含另外两个world的简单示例。

package local:demo;

// 省略了a、b、c、foo、bar、baz的定义

world my-world-a {
    import a;
    import b;
    export c;
}

world my-world-b {
    import foo;
    import bar;
    export baz;
}

world union-my-world {
     include my-world-a;
     include my-world-b;
}

include语句用于将另一个world的导入和导出引入当前world。它表示新的world应能运行其包含world的全部组件及更多。

上面定义的union-my-world等同于下面的world:

world union-my-world {
    import a;
    import b;
    export c;
    import foo;
    import bar;
    export baz;
}

接口去重(De-duplication of interfaces)

如果两个world共享一个导入或导出接口名称,则两个world的并集将仅包含该导入或导出名称的一个副本。例如,下面的两个世界union-my-world-aunion-my-world-b是等效的:

package local:demo;

world my-world-a {
    import a1;
    import b1;
}

world my-world-b {
    import a1;
    import b1;
}

world union-my-world-a {
    include my-world-a;
    include my-world-b;
}

world union-my-world-b {
    import a1;
    import b1;
}

名称冲突及with(Name Conflicts and with

当包含两个或更多world的导入或导出的纯名称(plain name)同名时,不能使用自动重复数据删除(因为两个同名的导入/导出在不同的world中可能有不同的含义),因此必须使用关键字with手动解决冲突。

以下示例说明如何解决union-my-world-aunion-my-world-b等效的名称冲突:

package local:demo;

world world-one { import a: func(); }
world world-two { import a: func(); }

world union-my-world-a {
    include world-one;
    include world-two with { a as b }
}

world union-my-world-b {
  import a: func();
  import b: func();
}

但是with不能用于重命名接口名称,因此以下world将是错误的:

package local:demo;

interface a {
    foo: func();
}

world world-using-a {
    import a;
}

world invalid-union-world {
    include my-using-a with { a as b }  // 错误:'a'是'local:demo/a'的缩写,为接口名称
}

关于子类型的注释(A Note on Subtyping)

未来,当支持optional导出时,world的作者可能会显示地标记导出为可选,以使一个目标为包含world的组件成为联合世界(union World)的子类型。

目前,我们不遵循该include语句的子类型规则。也就是说,该include语句不隐含任何其包含世界与联合世界之间的子类型关系。

WIT包和use(WIT Packages and use

WIT包表示分发单元,例如,可以发布到注册表并由其他WIT包使用。WIT包是*.wit文件中定义的一系列接口(interface)和世界(world)的集合。目前的惯例是,项目都会有一个wit文件夹,其中所有的wit/*.wit文件联合起来描述一个完整的包。

use语句的目的是接口之间共享类型,即使它们在当前包之外的依赖项中定义。use语句可以在interface和world中使用,也可以用在WIT文件的顶级(top-level)。

接口、世界、和use(Interfaces, worlds, and use)

interfaceworld块内的use语句可用于导入类型:

package local:demo;

interface types {
  enum errno { /* ... */ }

  type size = u32;
}

interface my-host-functions {
  use types.{errno, size};
}

use的目标是types,在包的范围内会被解析为接口,在本例中是预先定义的。然后,提供了用use语句导入的类型列表。接口type在文本上可以出现在接口use指令之后或之前。与use关联的接口必须是无环的。

通过use导入的名称可以在导入时重命名:

package local:demo;

interface my-host-functions {
  use types.{errno as my-errno};
}

这种形式的use是使用单个标识符作为导入目标,在本例中为types。首先在当前文件范围中查找名称types,但它同时会查重包的命名空间。这意味着如果接口定义在同级文件中时上述语法仍然有效:

// types.wit
interface types {
  enum errno { /* ... */ }

  type size = u32;
}

// host.wit
package local:demo;

interface my-host-functions {
  use types.{errno, size};
}

此处的types接口未定义在host.wit中但会找到它,因为它在同一个包中定义,只是在不同的文件中。由于文件没有排序,但组件模型中的类型定义是有序且无环的,因此WIT解析器将对所有解析的WIT定义进行隐式拓扑排序,以找到无环定义顺序(如果没有则报错)

world中导入或导出interface,使用importexport指令的相同语法:

// a.wit
package local:demo;

world my-world {
  import host;

  export another-interface;
}

interface host {
  // ...
}

// b.wit
interface another-interface {
  // ...
}

引用接口时,可以使用完全限定的接口名称(interface name)。 例如,在此WIT文档:

package local:demo;

world my-world {
  import wasi:clocks/monotonic-clock;
}

wasi:clocksmonotonic-clock接口被导入。 同样的语法可以用于use

package local:demo;

interface my-interface {
  use wasi:http/types.{request, response};
}

顶层(Top-level)use

如果引用的包有版本号,那么使用上述语法到目前为止可能会有点重复:

package local:demo;

interface my-interface {
  use wasi:http/types@1.0.0.{request, response};
}

world my-world {
  import wasi:http/handler@1.0.0;
  export wasi:http/handler@1.0.0;
}

为了减少重复并可能有助于避免命名冲突,use语句可以在文件顶层用于文件自身范围内的接口重命名。例如,上面的代码可以重写为:

package local:demo;

use wasi:http/types@1.0.0;
use wasi:http/handler@1.0.0;

interface my-interface {
  use types.{request, response};
}

world my-world {
  import handler;
  export handler;
}

这与之前世界的含义相同,use纯粹是为了方便开发人员在必要时提供较小的名字。

use引用的接口是在当前文件范围内定义的名称:

package local:demo;

use wasi:http/types;   // 定义名称`types`
use wasi:http/handler; // 定义名称`handler`

与接口级use类似,关键字as可以用于重命名推断名称:

package local:demo;

use wasi:http/types as http-types;
use wasi:http/handler as http-handler;

注意这些都可以组合使用以导入多版本包并重命名为不同的WIT标识符。

package local:demo;

use wasi:http/types@1.0.0 as http-types1;
use wasi:http/types@2.0.0 as http-types2;

// ...

传递导入和世界(Transitive imports and worlds)

use语句的实现不是通过复制类型信息,而是保留对其他地方定义的类型的引用。这种表示一直贯穿到最终组件,这意味着use类型会影响最终生成的组件结构。

例如此文档:

package local:demo;

interface shared {
  record metadata {
    // ...
  }
}

world my-world {
  import host: interface {
    use shared.{metadata};

    get: func() -> metadata;
  }
}

将生成此组件:

(component
  (import "local:demo/shared" (instance $shared
    (type $metadata (record (; ... ;)))
    (export "metadata" (type (eq $metadata)))
  ))
  (alias export $shared "metadata" (type $metadata_from_shared))
  (import "host" (instance $host
    (export $metadata_in_host "metadata" (type (eq $metadata_from_shared)))
    (export "get" (func (result $metadata_in_host)))
  ))
)

此处可以看出尽管组件world仅列出host作为导入,但组件额外导入了local:demo/shared接口。这是因为use shared.{ ... }隐式地需要shared导入到组件中。

注意此处"local:demo/shared"名字是由interface加上包名local:demo组成。

对于export接口,任何可传递的use接口都被视为导入,除非明确将其列为导出。例如,这里w1相当于w2

interface a {
  resource r;
}
interface b {
  use a.{r};
  foo: func() -> r;
}

world w1 {
  export b;
}
world w2 {
  import a;
  export b;
}

注意:未来计划使用“高级用户语法”来更细粒度地配置导出,例如能够配置某个use接口是特定的导入还是特定的导出。

WIT函数(WIT Functions)

函数定义于interface,或在world中列为importexport。函数参数必须全部命名,并且名称在不区分大小写的情况下是唯一的:

package local:demo;

interface foo {
  a1: func();
  a2: func(x: u32);
  a3: func(y: u64, z: f32);
}

函数最多可以返回一个未命名类型:

package local:demo;

interface foo {
  a1: func() -> u32;
  a2: func() -> string;
}

并且函数还可以通过命名来返回多种类型:

package local:demo;

interface foo {
  a: func() -> (a: u32, b: f32);
}

请注意,从函数返回多个值并不等同于从函数返回一组值。这些选项在组件二进制格式中以不同的方式表示。

WIT类型(WIT Types)

目前,WIT文件只能在interface中定义类型。WIT中支持的类型与组件模型本身支持的类型相同:

package local:demo;

interface foo {
  // "命名字段包(package of named fields)"
  record r {
    a: u32,
    b: string,
  }

  // 此类型的值将是指定的情况之一
  variant human {
    baby,
    child(u32), // 可选类型载荷(payload)
    adult,
  }

  // 类似于`variant`,但没有类型载荷
  enum errno {
    too-big,
    too-small,
    too-fast,
    too-slow,
  }

  // 位标志(bitflags)类型
  flags permissions {
    read,
    write,
    exec,
  }

  // 基本类型允许使用类型别名,另外这里还有一些其他类型示例
  type t1 = u32;
  type t2 = tuple<u32, u64>;
  type t3 = string;
  type t4 = option<u32>;
  type t5 = result<_, errno>;           // 无"ok"类型
  type t6 = result<string>;             // 无"err"类型
  type t7 = result<char, errno>;        // 两种类型指定("ok"或"err")
  type t8 = result;                     // 无"ok"或"err"类型
  type t9 = list<string>;
  type t10 = t9;
}

记录(record)变量(variant)枚举(enum)、和标志(flags)类型都必须有与之关联的名称。列表(list)可选项(option)结果(result)元组(tuple)和原始类型(primitive type)无需名称且可在任何上下文中提及。此限制是为了帮助在所有语言生成代码,尽可能地利用语言的内置类型,同时也适应哪些需要在美中语言中单独定义的类型。

WIT标识符(WIT Identifiers)

WIT中的标识符可以使用两种不同的格式定义。第一种是组件模型文本格式中的烤串命名法(kebab-case)label

foo: func(bar: u32);

red-green-blue: func(r: u32, g: u32, b: u32);

resource XML { ... }
parse-XML-document: func(s: string) -> XML;

这种格式在词汇上不能表示WIT关键字,因此第二种形式与第一种形式具有相同的语法和相同的限制,但以“%”为前缀:

%foo: func(%bar: u32);

%red-green-blue: func(%r: u32, %g: u32, %b: u32);

// 此表单还支持标识符,否则将是关键字。
%variant: func(%enum: s32);

词汇结构(Lexical structure)

wit格式是基于花括号的格式,其中空白是可选的(但建议使用)。wit文档被解析为unicode字符串,且当被存储在文件中时,预期会被编码为utf-8。

此外,wit文件必须不包含任何双向覆盖标量值,除换行符、回车符和水平制表符之外的控制代码或Unicode官方弃用或强烈不推荐的代码点。

目前的标记结构如下:

token ::= whitespace
        | operator
        | keyword
        | integer
        | identifier

解析此处其他地方定义的结构时,空格和注释将被忽略。

空白(Whitespace)

wit中的whitespace标记可以是空格、换行符、回车符、制表符、或注释:

whitespace ::= ' ' | '\n' | '\r' | '\t' | comment

注释(Comments)

wit中的comment标记要么是以//开头、换行符(\n)结尾的行注释,要么是以/*开头、*/结尾的块注释。请注意,块注释可以嵌套且其分隔符必须匹配。

comment ::= '//' character-that-isnt-a-newline*
          | '/*' any-unicode-character* '*/'

运算符(Operators)

wit的词法结构中,有一些常见的运算符用于各种构造。需要注意的是,像{(这样的定界符必须是配对的。

operator ::= '=' | ',' | ':' | ';' | '(' | ')' | '{' | '}' | '<' | '>' | '*' | '->' | '/' | '.' | '@'

关键字(Keywords)

某些标识符为WIT文档保留使用,不能直接用作标识符。其用于帮助解析格式,并且关键字列表目前仍在变化中,但当前的集合是:

keyword ::= 'use'
          | 'type'
          | 'resource'
          | 'func'
          | 'record'
          | 'enum'
          | 'flags'
          | 'variant'
          | 'static'
          | 'interface'
          | 'world'
          | 'import'
          | 'export'
          | 'package'
          | 'include'

整数(Integers)

整数目前仅用于包版本,是连续的数字序列:

integer ::= [0-9]+

顶级项(Top-level items)

wit文档是一系列在顶级指定的项。这些项一个接一个的出现,建议使用换行符将它们分开以提高可读性,但这不是必需的。

具体来说,wit文件的具体结构如下:

wit-file ::= explicit-package-list | implicit-package-definition

文件可以按两种方式组织。第一种是作为一系列连续的多个“显示”package ... {...}声明,包的内容在括号内。

explicit-package-list ::= explicit-package-definition*

explicit-package-definition ::= package-decl '{' package-items* '}'

或者,文件可以“隐式地”由可选package ...;声明,和随后的包项目(package items)列表组成。

implicit-package-definition ::= package-decl? package-items*

这两种结构不能混合:文件可以由显式或隐式样式写入,但不能同时使用两种样式。

wit文档中的所有其他声明都与包相关联,并定义如下。包定义由一个或多个这样的项组成:

package-items ::= toplevel-use-item | interface-item | world-item

特性门控(Feature Gates)

各种wit项可以被“门控”,以反映该项是不稳定功能的一部分,或该项是作为次要版本更新的一部分添加的,不应在针对早起次要版本时使用。

例如,以下接口有4个项目,其中3个是门控的:

interface foo {
  a: func();

  @since(version = 0.2.1)
  b: func();

  @since(version = 0.2.2, feature = fancy-foo)
  c: func();

  @unstable(feature = fancier-foo)
  d: func();
}

@since门表示bc是在0.2.10.2.2版本中添加的。因此,当构建一个目标版本为0.2.1的组件时,可以使用b,但不能使用c@since门设定的一个重要期望是,一旦将其应用到一个项目,该项目将不会向前进行不兼容的修改(根据一般的语义版本控制规则)。

相反,d上的@unstable门表示d是仍在积极开发的fancier-foo功能的一部分,因此d可能改变类型或随时移除。@unstable门设定的一个重要期望是,工具链默认不会暴露@unstable功能,除非开发者明确选择。

这两个门支持一种开发流程,在这种流程中,新功能在细节仍在讨论中时以@unstable门开始。然后,一旦功能稳定(并且,在WASI上下文中,经过投票),@unstable门会切换为@since门。为了实现平滑过渡(在此期间,生产工具链的目标版本早于 @since指定的version),@since门包含一个可选的feature字段,当该字段存在时,表示当目标版本大于等于,或者开发者明确启用了特性(feature)名称时,启用该特性。因此,如果版本是0.2.2或更高,或者开发者明确启用了fancy-foo特性,c就会被启用。一旦生产工具链更新了他们的默认版本以默认启用该特性,就可以移除特性字段。

具体来说,特性门控的语法是:

gate ::= unstable-gate
       | since-gate
unstable-gate ::= '@unstable' '(' feature-field ')'
feature-field ::= 'feature' '=' id
since-gate ::= '@since' '(' 'version' '=' <valid semver> ( ',' feature-field )? ')'

作为WIT验证的一部分,任何项必须进行门控以兼容所引用的另一个门控项项。例如,这是一个错误:

interface i {
  @since(version = 1.0.1)
  type t1 = u32;

  type t2 = t1; // 错误
}

此外,如果某项包含在门控项中,则该项也必须兼容门控。例如,这是一个错误:

@since(version = 1.0.2)
interface i {
  foo: func();  // 错误: 没有门控

  @since(version = 1.0.1)
  bar: func();  // 同样错误: 宽松门控
}

包声明(Package declaration)

WIT文件可以选择以定义包名称的包声明开头。

package-decl        ::= 'package' ( id ':' )+ id ( '/' id )* ('@' valid-semver)?  ';'

valid-semver项按语义版本2.0(Semantic Versioning 2.0)定义并且是可选的。

项:toplevel-use(Item: toplevel-use

文件顶级(top-level)的use语句可以用于将接口引入当前文件的范围,并/或为了方便在本地重命名接口:

toplevel-use-item ::= 'use' use-path ('as' id)? ';'

use-path ::= id
           | id ':' id '/' id ('@' valid-semver)?
           | ( id ':' )+ id ( '/' id )+ ('@' valid-semver)? 🪺

此处的use-path接口名称(interface name)。裸形式id指的是在当前包内定义的接口,而完全形式则指的是在包依赖中的接口。

as语法可以选择性地用来指定应赋予接口的名称。否则,名称将从use-path中推断而来。

作为未来的扩展,WIT、组件和组件注册表可能允许嵌套命名空间和包,这将会使得use-path的语法更加通用,如 🪺 后缀规则所示。

项:world(Item: world

世界定义了一个组件类型(componenttype),它是一系列可以进行控制的导入和导出的集合。

具体来说,world的结构如下:

world-item ::= gate 'world' id '{' world-items* '}'

world-items ::= gate world-definition

world-definition ::= export-item
                   | import-item
                   | use-item
                   | typedef-item
                   | include-item

export-item ::= 'export' id ':' extern-type
              | 'export' use-path ';'
import-item ::= 'import' id ':' extern-type
              | 'import' use-path ';'

extern-type ::= func-type ';' | 'interface' '{' interface-items* '}'

请注意,world可以导入类型并定义自己的类型,以便从组件的根导出并在导入和导出的函数中使用。此处interface项还定义了用于引用interface项的ID的语法。

项:include(Item: include

include语句可以将当前world与另一个world合并。include语句的结构如下:

include wasi:io/my-world-1 with { a as a1, b as b1 };
include my-world-2;
include-item ::= 'include' use-path ';'
               | 'include' use-path 'with' '{' include-names-list '}'

include-names-list ::= include-names-item
                     | include-names-list ',' include-names-item

include-names-item ::= id 'as' id

项:interface(Item: interface

接口可以在wit文件中定义。接口有一个名称和一系列可以进行控制的项目和函数。

具体来说,接口的结构如下:

注意:符号ε,也被称为Epsilon,表示一个空字符串。

interface-item ::= gate 'interface' id '{' interface-items* '}'

interface-items ::= gate interface-definition

interface-definition ::= typedef-item
                       | use-item
                       | func-item

typedef-item ::= resource-item
               | variant-items
               | record-item
               | flags-items
               | enum-items
               | type-item

func-item ::= id ':' func-type ';'

func-type ::= 'func' param-list result-list

param-list ::= '(' named-type-list ')'

result-list ::= ϵ
              | '->' ty
              | '->' '(' named-type-list ')'

named-type-list ::= ϵ
                  | named-type ( ',' named-type )*

named-type ::= id ':' ty

项:use(Item: use

use语句允许从其他wit包或接口导入类型或资源定义。use语句的结构如下:

use an-interface.{a, list, of, names}
use my:dependency/the-interface.{more, names as foo}

具体来说,其结构如下:

use-item ::= 'use' use-path '.' '{' use-names-list '}' ';'

use-names-list ::= use-names-item
                 | use-names-item ',' use-names-list?

use-names-item ::= id
                 | id 'as' id

注意:此处use-names-list?表示至少一个use-name-list术语。

项:类型(Items: type)

wit包中定义类型的方法有很多种,并且wit中所有可以定义的类型都旨在直接映射到组件模型中的类型。

项:type(别名)(Item: type (alias))

type语句在wit文档中声明一个新的命名类型。后续在使用此类型定义项时可以引用此名称。此构造类似于其他语言中的类型别名。

type my-awesome-u32 = u32;
type my-complicated-tuple = tuple<u32, s32, string>;

具体来说,其结构如下:

type-item ::= 'type' id '=' ty ';'

项:record(命名字段组)(Item: record (bag of named fields))

record语句声明一个具有命名字段的新命名结构。record类似于许多语言中的structrecord实例始终具有已定义的字段。

record pair {
    x: u32,
    y: u32,
}

record person {
    name: string,
    age: u32,
    has-lego-action-figure: bool,
}

具体来说,其结构如下:

record-item ::= 'record' id '{' record-fields '}'

record-fields ::= record-field
                | record-field ',' record-fields?

record-field ::= id ':' ty

项:flags(布尔值组)(Item: flags (bag-of-bools))

flags表示位集结构,每个位都有一个名称。该flags类型在规范ABI中表示为位标志(bit flags)表达。

flags properties {
    lego,
    marvel-superhero,
    supervillan,
}

具体来说,其结构如下:

flags-items ::= 'flags' id '{' flags-fields '}'

flags-fields ::= id
               | id ',' flags-fields?

项:variant (类型集合中的一个)(Item: variant (one of a set of types))

variant语句定义了一种新类型,该类型的实例与其列出的变体之一完全匹配。这类似于代数数据类型中的”sum”类型(或者如果你熟悉 Rust,那就是enum)。变体(variant)也可以被认为是带标签的集合。

variant的每个分支都可以有一个可选类型与之关联,当值具有该特定分支的标签时,这个类型就会出现。

所有的variant类型必须至少指定一项(variant-case)。

variant filter {
    all,
    none,
    some(list<string>),
}

具体来说,其结构如下:

variant-items ::= 'variant' id '{' variant-cases '}'

variant-cases ::= variant-case
                | variant-case ',' variant-cases?

variant-case ::= id
               | id '(' ty ')'

项:enum(无载荷的variant)(Item: enum (variant but with no payload))

enum语句定义了一种新类型,其语义等同于variant,但无有效荷载类型的情况。然而,这种情况被特殊处理,可能在语言ABI中有不同的表示形式,或者针对不同的语言生成不同的绑定。

enum color {
    red,
    green,
    blue,
    yellow,
    other,
}

具体来说,其结构如下:

enum-items ::= 'enum' id '{' enum-cases '}'

enum-cases ::= id
             | id ',' enum-cases?

项:resource(Item: resource

resource语句为资源定义了一个新的抽象类型,资源是一种具有生命周期的实体,只能通过句柄值(handle values)间接地传递。资源类型在接口(interface)中用于描述不能或不应通过值复制的事物。

例如,以下Wit定义了一种资源类型,以及一个接受并返回blob句柄的函数:

resource blob;
transform: func(blob) -> blob;

作为语法糖,resource语句也可以声明任意数量的方法(methods),此函数隐式接收一个为句柄类型的self参数。资源(resource)语句还可以包含任意数量的静态方法(static function),其没有隐式的self参数但应在词法上嵌套在资源类型的范围内。最后,资源语句最多可以包含一个构造器(constructor)函数,它是返回包含资源类型句柄的函数的语法糖。

例如,以下资源定义:

resource blob {
    constructor(init: list<u8>);
    write: func(bytes: list<u8>);
    read: func(n: u32) -> list<u8>;
    merge: static func(lhs: borrow<blob>, rhs: borrow<blob>) -> blob;
}

解析为:

resource blob;
%[constructor]blob: func(init: list<u8>) -> blob;
%[method]blob.write: func(self: borrow<blob>, bytes: list<u8>);
%[method]blob.read: func(self: borrow<blob>, n: u32) -> list<u8>;
%[static]blob.merge: func(lhs: borrow<blob>, rhs: borrow<blob>) -> blob;

这些前缀为%名称内嵌资源类型的名称,以便绑定生成器可以为目标语言生成惯用语法,或者(对于像C这样的语义)回退到带有适当前缀的自由函数名称。

当直接使用资源类型名称时(例如,当blob用作上述构造函数的返回值时),它代表“自有”句柄,当丢弃时将调用资源的析构函数。当资源类型名称被borrow<...>包裹时,它代表“借用”句柄,当丢弃时不会调用析构函数。如上所示,method(方法)总是解析为一个借用的self参数,而constructor(构造函数)总是解析为一个自有返回值。

具体来说,资源定义的语法是:

resource-item ::= 'resource' id ';'
                | 'resource' id '{' resource-method* '}'
resource-method ::= func-item
                  | id ':' 'static' func-type ';'
                  | 'constructor' param-list ';'

句柄类型的语法如下所示。

类型(Types)

如前所述,wit旨在允许定义与接口类型规范相对应的类型。上面的许多顶级项都引入了新的命名类型,但也支持“匿名(anonymous)”类型,如内置的(built-ins)。例如:

type number = u32;
type fallible-function-result = result<u32, string>;
type headers = list<string>;

具体来说,有以下类型可供选择:

ty ::= 'u8' | 'u16' | 'u32' | 'u64'
     | 's8' | 's16' | 's32' | 's64'
     | 'f32' | 'f64'
     | 'char'
     | 'bool'
     | 'string'
     | tuple
     | list
     | option
     | result
     | handle
     | id

tuple ::= 'tuple' '<' tuple-list '>'
tuple-list ::= ty
             | ty ',' tuple-list?

list ::= 'list' '<' ty '>'

option ::= 'option' '<' ty '>'

result ::= 'result' '<' ty ',' ty '>'
         | 'result' '<' '_' ',' ty '>'
         | 'result' '<' ty '>'
         | 'result'

tuple类型在语义上等同于具有数值字段的record,但其经常可以具有特定于语言的含义,所以她被视为一种一等类型。

类似地,optionresult类型在语义上等同于variant:

variant option {
    none,
    some(ty),
}

variant result {
    ok(ok-ty),
    err(err-ty),
}

这些类型经常被使用,并且经常具有特定于语言的含义,所以它们也被提供为一等类型。

最后,ty的最后一种情况就是简单的id,其目的是引用文档中定义的另一种类型或资源。请注意,这些定义可以来源于use语句,也可以在本地定义。

句柄(Handles)

Wit有两种句柄类型:“自有(owned)”句柄和“借用(borrowed)”句柄。自有句柄表示在两个组件间传递资源的唯一所有权。当自有句柄的所有者丢弃句柄时,资源会被销毁。相比之下,借用句柄表示在调用期间从调用者(caller)到被调用者(callee)的句柄的临时借用。

句柄的语法是:

handle ::= id
         | 'borrow' '<' id '>'

id表示一个自有句柄,其中id是先前的resource项。因此,资源在组件之间传递的“默认”方式是通过唯一所有权的转移。

上面定义资源方法的语法是语法糖,它扩展为单独的函数项,这些函数项接受一个名为self的第一个参数,参数的类型为borrow。例如,复合定义:

resource file {
    read: func(n: u32) -> list<u8>;
}

扩展为:

resource file
%[method]file.read: func(self: borrow<file>, n: u32) -> list<u8>;

其中%[method]file.read是方法根据组件模型的命名(name)定义的解析后的名称。

名称解析(Name resolution)

wit文档在解析(parse)后进行解析(resolve),以确保所有名称都能正确解析。例如这不是有效的wit文档:

type foo = bar;  // 错误:名称`bar`未定义

类型引用主要通过tyid产生。

此外,wit文档中的名称只能定义一次:

type foo = u32;
type foo = u64;  // 错误:名称`foo`已定义

名称不需要在使用前定义(与C或C++不同),可以在使用后定义类型:

type foo = bar;

record bar {
    age: u32,
}

但是类型不能是递归的:

type foo = foo;  // 错误:不能引用自身

record bar1 {
    a: bar2,
}

record bar2 {
    a: bar1,    // 错误:record不能引用自身
}

包格式(Package Format)

每个顶层WIT定义可以编译成单个规范的组件模型类型定义(type definition),该定义捕获上述类型解析执行的结果。这些组件模型类型可以与其他类别和导出的组件一同被导出,从而允许单个组件同时打包运行时功能和开发时WIT接口。因此,WIT不需要自己单独的包格式;WIT可以作为组件二进制打包。

以这种方式使用组件二进制文件打包WIT有几个优点:

作为第一个例子,以下WIT:

package local:demo;

interface types {
  resource file {
    read: func(off: u32, n: u32) -> list<u8>;
    write: func(off: u32, bytes: list<u8>);
  }
}

interface namespace {
  use types.{file};
  open: func(name: string) -> file;
}

可以打包成一个组件:

(component
  (type (export "types") (component
    (export "local:demo/types" (instance
      (export $file "file" (type (sub resource)))
      (export "[method]file.read" (func
        (param "self" (borrow $file)) (param "off" u32) (param "n" u32)
        (result (list u8))
      ))
      (export "[method]file.write" (func
        (param "self" (borrow $file))
        (param "bytes" (list u8))
      ))
    ))
  ))
  (type (export "namespace") (component
    (import "local:demo/types" (instance $types
      (export "file" (type (sub resource)))
    ))
    (alias export $types "file" (type $file))
    (export "local:demo/namespace" (instance
      (export "open" (func (param "name" string) (result (own $file))))
    ))
  ))
)

此示例说明了接口的基本结构:

这种编码方案的一个有用结果是每个顶层定义都是自包含的并且是有效的(根据组件模型验证规则),独立于其他定义。这允许轻松地拆分或合并包(假设结果不必是有效的包,而只是非导出类型定义的原始列表)。

另一个预期是,当包含WIT定义的组件发布到注册表时,注册表会验证组件内部的完全限定的WIT接口名称是否与注册表分配的软件包名称一致。例如,上述组件只有在发布的包名为local:demo时才有效;任何其他软件包名称都会与内部local:demo/typeslocal:demo/namespace导出的接口名称不一致。

包间引用在结构上与包内引用没有区别,除了引用的 WIT 定义不在组件中。例如,以下WIT:

package local:demo

interface foo {
  use wasi:http/types.{request};
  frob: func(r: request) -> request;
}

编码为:

(component
  (type (export "foo") (component
    (import "wasi:http/types" (instance $types
      (export "request" (type (sub resource)))
    ))
    (alias export $types "request" (type $request))
    (export "local:demo/foo" (instance
      (export "frob" (func (param "r" (own $request)) (result (own $request))))
    ))
  ))
)

世界(world)的编码与接口类似,但将内部导出的instance替换为内部导出的component。例如,此WIT:

package local:demo;

world the-world {
  export test: func();
  export run: func();
}

编码为:

(component
  (type (export "the-world") (component
    (export "local:demo/the-world" (component
      (export "test" (func))
      (export "run" (func))
    ))
  ))
)

在当前版本的WIT中,外部包装的组件类型将只包含一个export,因此仅用于将烤串命名导出与内部导出的接口名称分开,并提供与上面展示的interface的编码的一致性。

当世界(world)导入或导出接口时,为了生成有效的组件类型,接口的编译实例类型最终会被复制到组件类型中。例如,以下WIT:

package local:demo;

world the-world {
  import console;
}

interface console {
  log: func(arg: string);
}

编码为:

(component
  (type (export "the-world") (component
    (export "local:demo/the-world" (component
      (import "local:demo/console" (instance
        (export "log" (func (param "arg" string)))
      ))
    ))
  ))
  (type (export "console") (component
    (export "local:demo/console" (instance
      (export "log" (func (param "arg" string)))
    ))
  ))
)

这种重复在跨包引用或拆分包的情况下很有用,允许编译的world定义完全自包含,并且能够用于编译组件而无需额外的类型信息。

综上所述,WIT 定义如下:

// wasi-http repo

// wit/types.wit
interface types {
  resource request { ... }
  resource response { ... }
}

// wit/handler.wit
interface handler {
  use types.{request, response};
  handle: func(r: request) -> response;
}

// wit/proxy.wit
package wasi:http;

world proxy {
  import wasi:logging/logger;
  import handler;
  export handler;
}

编码为:

(component
  (type (export "types") (component
    (export "wasi:http/types" (instance
      (export "request" (type (sub resource)))
      (export "response" (type (sub resource)))
      ...
    ))
  ))
  (type (export "handler") (component
    (import "wasi:http/types" (instance $http-types
      (export "request" (type (sub resource)))
      (export "response" (type (sub resource)))
    ))
    (alias export $http-types "request" (type $request))
    (alias export $http-types "response" (type $response))
    (export "wasi:http/handler" (instance
      (export "handle" (func (param "r" (own $request)) (result (own $response))))
    ))
  ))
  (type (export "proxy") (component
    (export "wasi:http/proxy" (component
      (import "wasi:logging/logger" (instance
        ...
      ))
      (import "wasi:http/types" (instance $http-types
        (export "request" (type (sub resource)))
        (export "response" (type (sub resource)))
        ...
      ))
      (alias export $http-types "request" (type $request))
      (alias export $http-types "response" (type $response))
      (import "wasi:http/handler" (instance
        (export "handle" (func (param "r" (own $request)) (result (own $response))))
      ))
      (export "wasi:http/handler" (instance
        (export "handle" (func (param "r" (own $request)) (result (own $response))))
      ))
    ))
  ))
)

这个例子展示了,在具体world(wasi:http/proxy)的上下文中,独立的接口定义(如wasi:http/handler)不再是“参数化”形式:没有外部包装的组件类型,而是所有的use都被替换为由WIT解析过程确定的先前类型导入的直接别名。

与大多数其他WIT构造不同,@since@unstable限制不会在组件二进制文件中表示出来。相反,它们被视为“宏(macro)”构造,代替维护单个WIT文档的两个副本。具体而言,在将一组WIT文档编码为二进制时,目标版本和一组显式启用的功能名称决定了各个限制功能是否包含在编码类型中。

例如,以下WIT文档:

package ns:p@1.1.0;

interface i {
  f: func();

  @since(version = 1.1.0)
  g: func();
}

当目标版本为1.0.0时,被编码为以下组件:

(component
  (type (export "i") (component
    (export "ns:p/i@1.0.0" (instance
      (export "f" (func))
    ))
  ))
)

如果目标版本为1.1.0,则相同的WIT文档将被编码为:

(component
  (type (export "i") (component
    (export "ns:p/i@1.1.0" (instance
      (export "f" (func))
      (export "g" (func))
    ))
  ))
)

因此,@since@unstable限制不是组件运行时语义的一部分,而只是用于生成组件的源级工具的一部分。