Design: 提议:添加跨语言类型绑定

创建于 2019-05-01  ·  61评论  ·  资料来源: WebAssembly/design

WebAssembly 目前非常擅长从给定的(通常是 JS)解释器执行用任意语言编写的代码,但在将多种任意语言组合在一起时,它缺乏几个关键特性。

这些功能之一是与语言无关的类型系统。 我想建议将一个或多个这样的系统添加到 WebAssembly。

顺便说一句,在之前的功能讨论中,一些贡献者表示语言互操作性不应该是 WebAssembly 的设计目标。 虽然我同意它不一定是一个高优先级的目标,但我认为这是一个长期努力的目标。 因此,在进入设计目标之前,我将阐述我认为语言互操作性值得付出努力的原因。

为什么要关心语言互操作性?

较低的语言间障碍的好处包括:

  • 为 wasm 用户提供更多库:这是不言而喻的,但提高语言互操作性意味着用户可以更频繁地使用现有库,即使库是用与他们使用的语言不同的语言编写的。

  • 更容易采用小型语言:在当前的市场中,没有企业支持的语言通常很难获得吸引力。 新语言(甚至像 D 这样经过多年改进的语言)必须与拥有庞大生态系统的语言竞争,并且它们自身缺乏库。 语言互操作性将允许他们使用现有的生态系统,如 Python 或 Java 的。

  • 更好的与语言无关的工具链:现在,大多数语言都有自己的库加载方案和包管理器(或者,在 C/C++ 的情况下,有几个非官方的)。 编写与语言无关的项目构建器很困难,因为这些语言通常具有微妙的依赖关系和 ABI 不兼容性,需要一个整体的项目范围的解决方案来解决。 强大的跨语言类型系统将使项目更容易拆分为更小的模块,这些模块可以由类似 npm 的解决方案处理。

总的来说,我认为第一点是最重要的。 更好的类型系统意味着更好地访问其他语言,这意味着有更多机会重用代码而不是从头开始编写代码。 我不能夸大它的重要性。

要求

考虑到这一点,我想概述跨语言类型系统需要通过的要求。

我的写作假设类型系统将严格用于注释模块之间传递的函数,并且不会以任何方式检查语言如何使用自己的线性或托管内存。

为了在 wasm 环境中真正有用,这样的类型系统需要:

1 - 安全

  • 类型安全:被调用者必须只能访问调用者指定的数据,对象能力风格。

    • 在通话结束时应该“忘记”记忆。 被调用者不应该能够访问调用者的数据,返回,然后以任何形式再次访问该数据。

2 - 开销

  • 开发人员应该习惯于定期进行模块间调用,例如在渲染循环中。

    • 零拷贝:类型系统应该具有足够的表达能力,以允许解释器根据需要实现零拷贝策略,并且具有足够的表达能力让这些实现者知道何时零拷贝是最佳的。

3 - 结构图

  • 类型系统应包括结构、可选指针、变长数组、切片等。

    • 理想情况下,调用者应该能够在遵守要求 1 和 2 的同时发送分散在内存中的对象图。

4 - 引用类型

  • 模块应该能够交换嵌套在结构图中深处的引用类型。

5 - 内存布局之间的桥梁

  • 这是非常重要的一点。 不同类别的语言有不同的要求。 依赖线性内存的语言希望传递内存片,而依赖 GC 的语言希望传递 GC 引用。

    • 理想的类型系统应该表达语义类型,并让语言决定如何在内存中解释它们。 虽然在内存布局不兼容的语言之间传递数据总是会产生一些开销,但在相似语言之间传递数据最好是便宜的(例如,如果 memcpy 可以完成相同的工作,嵌入器应该避免序列化-反序列化步骤)。

    • 额外的绑定也可能允许缓存和其他优化策略。

    • 只要语义类型兼容,在两个模块之间传递数据时的转换工作应该对开发人员透明。

6 - 编译时错误处理

  • 任何与无效函数调用参数相关的错误都应该在编译时被检测和表达,不像在,例如,JS 中,当尝试评估参数时,在运行时抛出 TypeErrors。
  • 理想情况下,语言编译器本身应该在导入 wasm 模块时检测类型错误,并向用户输出富有表现力的惯用错误。 需要在工具约定存储库中详细说明此错误检查应采用的形式。
  • 这意味着具有现有转换器到其他语言的 IDL 将是一个加分项。

7 - 为跨语言交互提供谢林点

  • 这说起来容易做起来难,但我认为 wasm 应该向所有编译器编写者发出一个信号,即语言之间互操作的标准方式是 X。出于显而易见的原因,对于语言互操作性而言,拥有多个相互竞争的标准是不可取的。

建议实施

我建议将@kentonv绑定到内容添加到 Webassembly。

它们将以类似于 WebIDL 绑定的方式工作:wasm 模块将导出函数,并使用特殊指令将它们绑定到类型签名; 其他模块会导入这些签名,并将它们绑定到自己的函数。

以下伪语法旨在让您了解这些绑定的外观; 它是近似的并且深受 WebIDL 提案的启发,并且更多地关注技术挑战而不是提供详尽的说明列表。

Capnproto 绑定指令将全部存储在新的Cap'n'proto 绑定部分中。

Cap'n'proto 类型

该标准需要capnproto 的模式语言的内部表示。 例如,以下 Capnproto 类型:

```Cap'n Proto
结构人{
名称@0 :文本;
生日@3 :日期;

电子邮件@1 :文本;
电话@2 :List(电话号码);

结构电话号码{
数字@0 :文本;
类型@1 :类型;

enum Type {
  mobile @0;
  home @1;
  work @2;
}

}
}

结构日期{
@0 :Int16;
月份@1 :UInt8;
@2天:UInt8;
}

might be represented as

```wasm
(<strong i="32">@capnproto</strong> type $Date (struct
    (field "year" Int16)
    (field "month" UInt8)
    (field "day" UInt8)
))
(<strong i="33">@capnproto</strong> type $Person_PhoneNumber_Type (enum 0 1 2))
(<strong i="34">@capnproto</strong> type $Person_PhoneNumber (struct
    (field "number" Text)
    (field "type" $Person_PhoneNumber_Type)
))
(<strong i="35">@capnproto</strong> type $Person (struct
    (field "name" Text)
    (field "email" Text)
    (field "phones" (generic List $Person_PhoneNumber))
    (field "birthdate" $Data)
))

从线性内存序列化

Capnproto 消息传递两种类型的数据:段(原始字节)和功能。

这些大致映射到 WebAssembly 的线性内存和表格。 因此,webassembly 创建 capnproto 消息的最简单可能的方法是将偏移量和长度传递给段的线性内存,并将偏移量和长度传递给功能表。

(可以为功能设计更好的方法,以避免运行时类型检查。)

请注意,实际的序列化计算将在粘合代码中进行(如果有的话)(请参阅生成粘合代码)。

绑定运算符

| 操作员 | 立即数 | 儿童 | 说明 |
| :--- | :--- | :--- | :--- |
| | 关闭idx
len-idx | | 取源元组的第off-idxlen-idx的 wasm 值,它们都必须是i32 s,作为线性内存切片的偏移量和长度其中存储了一个段。 |
| 可捕获| 关闭idx
len-idx | | 取源元组的第off-idxlen-idx的 wasm 值,它们都必须是i32 s,作为表的一个切片的偏移量和长度存储能力表。 |
| 留言| capnprototype
能力表| 细分| 使用提供的功能表和段创建格式capnproto-type的 capnproto 消息。 |

从托管内存序列化

在 GC 提案落地之前很难确定具体的行为。 但是一般的实现是 capnproto 绑定将使用单个转换运算符从 GC 类型获取 capnproto 类型。

低级类型的转换规则相当简单:i8 转换为 Int8、UInt8 和 bool,i16 转换为 Int16 等。高级类型将转换为它们的 capnproto 等价物:结构和数组引用转换为指针、不透明引用转化为能力。

更完整的提案需要为枚举和联合定义策略。

绑定运算符

| 操作员 | 立即数 | 儿童 | 说明 |
| :--- | :--- | :--- | :--- |
| 作为| capnprototype
idx | | 获取源元组的idx 'th wasm 值,它必须是一个引用,并产生capnproto-type的 capnproto 值。 |

反序列化为线性内存

反序列化为线性内存与从线性内存中序列化大致相似,但有一个额外的警告:wasm 代码通常事先不知道 capnproto 类型将占用多少内存,并且需要为主机提供某种动态内存管理方法.

在 WebIDL 绑定提议中,提议的解决方案是将分配器回调传递给主机函数。 对于 capnproto 绑定,这种方法是不够的,因为动态分配需要在调用方和被调用方都发生。

另一种解决方案是允许传入绑定映射绑定到两个传入绑定表达式(以及两个函数):一个为 capnproto 数据分配内存,一个实际获取数据。

反序列化到托管内存

反序列化到托管内存将使用与相反方向相同类型的转换运算符。

生成胶水代码

当将两个 wasm 模块链接在一起时(无论是静态还是动态),嵌入器应该列出两个模块共有的所有 capnproto 类型、函数类型和 capnproto 类型之间的绑定,并在每对不同的函数类型之间生成胶水代码。

粘合代码将取决于绑定数据的类型。 线性内存绑定之间的粘合代码将归结为 memcpy 调用。 托管内存绑定之间的粘合代码将归结为传递引用。 另一方面,线性和托管内存之间的粘合代码将涉及更复杂的嵌套转换操作。

例如,Java 模块可以导出一个函数,将参数作为 GC 类型,并将该函数绑定到类型化签名; 解释器应该允许 Python 模块和 C++ 导入该类型签名; C++ 绑定将从线性内存中传递数据,而 Python 绑定将从 GC 内存中传递数据。 必要的转换对 Java、Python 和 C++ 编译器来说是透明的。

替代解决方案

在本节中,我将研究交换数据的替代方法,以及它们如何对需求部分中定义的指标进行评分。

交换 JSON 消息

这是蛮力解决方案。 我不会在那个上花太多时间,因为它的缺陷相当明显。 它不符合要求 2、4 和 6。

发送以序列化格式编码的原始字节

这是一个部分的解决方案。 为 wasm 模块定义一种将线性内存和表切片传递给其他模块的方法,然后模块编写者可以使用序列化格式(capnproto、protobuff 或其他)将结构化图编码为字节序列,传递字节,并使用相同的格式对其进行解码。

它传递 1 和 3,并且可以通过一些调整传递 2 和 4(例如,将引用作为索引传递给表)。 如果用户确保将序列化类型导出为调用者语言中的类型定义,则它可以传递 6。

但是,它无法满足要求 5 和 7。在两个 GC 实现之间进行绑定时这是不切实际的; 例如,通过 Protobuf 调用 Java 库的 Python 模块需要将字典序列化为线性内存,传递该内存片,然后将其反序列化为 Java 对象,而不是进行一些可以优化的哈希表查找JIT 实现。

它鼓励每个库编写者使用自己的序列化格式(JSON、Protobuf、FlatBuffer、Cap'n Proto、SBE),这对于互操作性来说并不理想; 尽管这可以通过在tool-conventions 中定义规范的序列化格式来缓解。

然而,增加传递任意线性内存切片的可能性将是一个很好的第一步。

发送 GC 对象

依靠模块相互发送GC 对象是可能的。

该解决方案有一些优点:GC提案已经在进行中; 它通过 1、3、4 和 7。GC 收集的数据分配成本高,但传递成本低。

但是,该解决方案对于类 C 语言并不理想。 例如,将数据传递给 Rust 模块的 D 模块需要将其数据序列化为 GC 图,将图传递给 Rust 函数,该函数会将其反序列化到其线性内存中。 这个过程会分配立即丢弃的 GC 节点,从而产生大量不必要的开销。

除此之外,当前的 GC 提案没有对枚举和联合的内置支持; 错误处理将在链接时或运行时而不是编译时进行,除非编译器可以读取和理解 wasm GC 类型。

使用其他编码

任何定义类型系统的序列化库都可以用于 wasm。

Capnproto 似乎最合适,因为它强调零复制,并且其内置的对象功能可以巧妙地映射到引用类型。

剩下的工作

需要充实以下概念,才能将这个基本提案变成可以提交给社区小组的文件。

  • 绑定运算符
  • GC 类型等价物
  • 对象能力
  • 布尔数组
  • 数组
  • 常数
  • 泛型
  • 类型演变
  • 添加第三个“getter 和 setter”绑定类型。
  • 可能的缓存策略
  • 支持多表和线性存储器

同时,欢迎对我已经写的内容提出任何反馈意见。 这里的范围非常广泛,所以我很感激帮助缩小该提案需要回答的问题。

最有用的评论

我们可以为每个 IR 类型添加一些绑定以覆盖绝大多数语言。

这是我认为根本不正确的关键基本假设。 我的经验是(至少!)与语言实现一样多的表示选择。 它们可以任意复杂。

以 V8 为例,它本身就有几十个(!)字符串表示,包括不同的编码、异构绳索等。

Haskell 的情况比您描述的要复杂得多,因为 Haskell 中的列表是惰性的,这意味着对于字符串中的每个单个字符,您可能需要调用 thunk。

其他语言对字符串的长度使用有趣的表示,或者不显式存储它但要求计算它。

这两个示例已经表明声明式数据布局并没有削减它,您通常需要能够调用运行时代码,而运行时代码又可能有自己的调用约定。

这只是字符串,在概念上是一种相当简单的数据类型。 我什至不想考虑语言表示产品类型(元组/结构/对象)的无数种方式。

然后是接收方,您必须能够创建所有这些数据结构!

因此,我认为我们甚至可以远程支持“绝大多数语言”是完全不现实的。 相反,我们会开始给予少数人特权,同时已经发展了一个包含任意东西的大型动物园。 这在多个层面上似乎是致命的。

所有61条评论

这真的很有趣! 我只是快速通读了一遍,并且只是有一些初步的想法,但我的首要问题是问为什么大多数语言已经提供/使用的现有 FFI 机制对 WebAssembly 来说是不够的。 几乎我熟悉的每种语言都有某种形式的 C FFI,因此今天已经能够进行互操作。 其中许多语言也能够基于这些绑定进行静态类型检查。 此外,已经有大量围绕这些接口的工具(例如,用于 Rust 的bindgen crate,用于 Erlang/BEAM 的erl_nif等)。 C FFI 已经解决了最重要的要求,并且具有已经在实践中得到广泛验证和使用的关键优势。

5 - 内存布局之间的桥梁

理想的类型系统应该表达语义类型,并让语言决定如何在内存中解释它们。 虽然在内存布局不兼容的语言之间传递数据总是会产生一些开销,但在相似语言之间传递数据最好是便宜的(例如,如果 memcpy 可以完成相同的工作,嵌入器应该避免序列化-反序列化步骤)。

只要语义类型兼容,在两个模块之间传递数据时的转换工作应该对开发人员透明。

在通过 FFI 屏障传递数据时,将一个布局透明地转换为另一个布局对我来说确实像是编译器后端或语言运行时的一项工作,并且在 C/C++/Rust/等对性能敏感的语言中可能根本不需要。 特别是,对于您计划在 FFI 之间来回传递的内容,在我看来,最好使用通用 ABI,而不是进行任何类型的翻译,因为翻译可能会产生过高的成本。 选择平台通用 ABI 以外的布局的好处不太值得,但我很乐意承认我可能误解了您所说的替代布局的意思。

顺便说一句,将可靠的 FFI 工具的负担放在编译器/运行时上还有一个额外的好处,因为所做的任何改进都适用于其他平台,反之亦然,因为对非 Wasm 平台的 FFI 改进有利于 Wasm。 我认为这个论点必须非常引人注目,从根本上开始并建立一个新的 FFI 机制。

抱歉,如果我误解了提案的目的,或者错过了一些关键的东西,正如我上面提到的,我需要更仔细地再读一遍,但我觉得我需要在有时间的时候提出我最初的问题。

Apache Arrow 也为此而存在,但更侧重于高性能应用程序。

我想我同意这里的总体动机,它基本上与我们就Web IDL 绑定在未来如何推广的讨论一致。 事实上,解释器的早期草稿包含一个 FAQ 条目,提到了这个跨语言用例。

我的主要关注点(以及省略该 FAQ 条目的原因)是范围:绑定 N 种语言的一般问题似乎可能会产生大量开放式(并且可能是非终止式)讨论,特别是考虑到没有人已经这样做了(这当然是先有鸡还是先有蛋的问题)。 相比之下,Web IDL 绑定所解决的问题相当具体,并且在今天的 Rust/C++ 中很容易证明,这使我们能够激发(非平凡的)标准化/实现的努力,并热切地原型/验证所提出的解决方案。

但我希望 Web IDL 绑定能够让我们打破这个鸡与蛋的问题,并开始获得一些跨语言绑定的经验,这可能会激发下一波扩展或一些新的而不是特定于 Web IDL 的东西。 (请注意,正如目前提出的那样,如果使用兼容的 Web IDL 绑定的两个 wasm 模块相互调用,则优化 impl 可以执行您在此处提到的优化;只是没有 Cap'n Proto 的完整表达能力。)

我应该事先声明,我还没有时间完全理解该提案。
原因是我认为这项任务是不可能的。 这有两个根本原因:
一种。 不同的语言具有不同的语义,这些语义不一定包含在类型注释中。 举个例子,Prolog 的评估与 C++ 的评估完全不同:语言本质上是不可互操作的。 (对于 Prolog,您可以替换许多其他语言)

湾根据定义,任何类型系统的 LCD 都不能保证捕获所有给定语言的类型语言。 这给语言实现者留下了一个非常不舒服的选择:支持他们自己的语言或放弃他们语言类型系统的好处。 举个例子:Haskell 有“类型类”。 任何不支持类型类的 Haskell 实现都会有效地破坏它并使其无法使用。
另一个例子:C++ 对泛型的支持需要在编译时消除泛型; 另一方面,ML、Java(以及许多其他语言)使用一种通用表示形式——这与 C++ 所采用的方法不兼容。

另一方面,导出/导入类型的两个表达式似乎带来了它自己的问题:语言系统是否应该验证这两个表达式在某种意义上是一致的? 做这项工作是谁的责任?

@lukewagner感谢您的链接! 我很高兴有机会阅读该文件!

在我看来,在这个特定的讨论中,有两件事有点混合在一起——下面的一些内容被写出来,所以我可以再次检查我的理解,所以请随时指出我可能误解或遗漏的任何内容:

  1. 高效的主机绑定

    • 基本上,WebIDL 旨在解决的问题,至少对于浏览器环境而言——一种从模块->主机和主机->模块映射的接口描述,本质上将从一个到另一个的转换工作委托给主机引擎。 这种转换不一定是理想的,甚至根本没有优化,但优化引擎可以利用它来做到这一点。 然而,即使优化,翻译仍然在一定程度上执行,但这是可以接受的,因为替代方案仍然是翻译,只是速度较慢。

  2. 高效的异构模块到模块绑定。

    • 换句话说,给定两个模块,一个用source编写,另一个用dest编写,它们之间共享类型,从 source->dest 和/或 dest->source 调用

    • 如果没有通用的 FFI 可用,并且给定类似 WebIDL 的东西,即搭载 1,则未优化的路径将是在跨语言障碍调用时通过宿主环境提供的某些公分母类型进行翻译,例如source type -> common type -> dest type .



      • 优化引擎理论上可以使这种从source直接转换为dest而无需中间人,但仍然会增加翻译开销。



    • 如果有一个通用的 FFI,即sourcedest共享一个 ABI(例如 C ABI),那么sourcedest可以直接互相调用通过 FFI,完全没有开销。 这可能是实践中最有可能的场景。

所以我的看法是,利用 WebIDL 或类似的东西(即支持更广泛的主机 API/环境集的超集)肯定有好处,但它实际上只是对 1 中概述的问题的解决方案,子集共 2 个,它处理没有 FFI 可用的语言间绑定。 FFI _is_ 可用的 2 子集显然比替代方案更可取,因为它本身不会产生开销。

即使可以选择 FFI,是否有充分的理由使用 IDL? 明确地说,我绝对同意将 IDL 用于提到的其他用例,但我特别是在语言互操作性的上下文中提出要求,而不是主机绑定。

如果同时使用/存在 C FFI(例如,因为它最常见)和 IDL,我还有几个问题:

  • 如果sourcedest语言根据它们的公共 ABI(例如,可变长度数组的公共表示)为具有相同底层内存中表示的共享类型提供不同的类型定义) - 主机引擎是否会仅仅因为存在 IDL 指令而尝试在这些类型之间执行转换,即使它们可以使用标准 FFI 安全地相互调用?

    • 如果不是,并且选择加入,这似乎是理想的方案,因为您可以添加 IDL 以支持与没有 FFI 的语言的互操作,同时支持具有 FFI 的语言。 我不确定主机引擎将如何使其工作。 我还没有完全考虑清楚,所以我可能遗漏了一些东西

    • 如果是这样,宿主引擎如何统一类型?:



      • 如果引擎只关心布局,那么静态分析如何检测调用者何时向被调用者提供了错误的参数类型? 如果这种分析不是目标,那么 IDL 似乎真的只适合主机绑定,而不是跨语言。


      • 如果引擎关心的不仅仅是布局,换句话说,类型系统需要名义和结构兼容性:





        • 谁为某些函数定义了权威类型? 我什至如何从某种语言中引用权威类型? 例如,假设我正在调用一个用另一种语言编写的共享库,它定义了一个add/2函数,并且add/2需要某种类型的两个参数size_t 。 我的语言不一定知道size_t名义上,它有自己的机器宽度无符号整数的 ABI 兼容表示, usize ,所以我的语言中该函数的 FFI 绑定使用我的语言类型。 鉴于此,我的编译器如何知道生成将usize映射到size_t IDL。






  • 是否有用于在程序中的模块之间调用的 IDL 接口示例,其中 FFI 可用但明确未使用以支持 IDL 描述的接口? 特别是一些不是 WebAssembly 的东西,主要有兴趣研究这些情况下的好处。

我承认我仍在努力挖掘 WebIDL 及其前身的全部细节,所有这些如何适应不同的主机(浏览器与非浏览器)等等,如果我忽略了,请务必让我知道某物。

@位行者

这真的很有趣!

很开心你喜欢!

但我的首要问题是问为什么大多数语言已经提供/使用的现有 FFI 机制不足以用于 WebAssembly。

C 类型系统作为语言间 IDL 存在一些问题:

  • 它在共享地址空间的假设下运行,这是不安全的,并且故意不保留在 WebAssembly 中。 (我自己在 JS-to-C FFI 方面的经验表明,实现往往只是为了速度而牺牲安全性)

  • 它没有对动态长度数组、标记联合、默认值、泛型等的原生支持。

  • 没有直接等价于引用类型。

C++ 解决了其中一些问题(不是最大的问题,共享地址空间),但添加了一堆在 IPC 中并没有真正有用的概念。 当然,您始终可以使用 C 的超集或 C++ 的子集作为您的 IDL,然后围绕它设计绑定规则,但此时您几乎无法从现有代码中获得任何好处,因此您也可以使用现有代码IDL。

特别是,对于您计划在 FFI 之间来回传递的事物

我不太明白你的意思,但要明确一点:我认为在一般情况下不可能在模块之间来回传递可变数据。 该提案试图概述一种在模块之间发送不可变数据并获得不可变数据作为回报的方法,模块之间没有任何关于其他如何存储其数据的信息。

选择平台通用 ABI 以外的布局的好处不太值得,但我很乐意承认我可能误解了您所说的替代布局的意思。

现在的问题是,常见的 ABI 是存储在线性内存中的字节片。 但是在未来,当 GC 提案实施时,一些语言(Java、C#、Python)将在线性内存中存储很少甚至没有。 相反,它们会将所有数据存储在 GC 结构中。 如果其中两种语言尝试通信,将这些结构序列化为字节流只是为了立即反序列化它们将是不必要的开销。


@KronicDeth谢谢,我会研究一下。

虽然,从浏览文档来看,这似乎是 Flatbuffers 的超集,专门用于提高性能? 无论哪种方式,与 Flatbuffers 或 Capnproto 相比,它的哪些品质可以独特地帮助 WebAssembly 模块互操作性?


@卢克瓦格纳

但我希望 Web IDL Bindings 能让我们打破这个鸡与蛋的问题,并开始获得一些跨语言绑定的经验,这可能会激发下一波扩展或一些新的而不是特定于 Web IDL 的东西。

同意。 我在编写这个提案时的假设是,任何 capnproto 绑定的实现都将基于来自实现 WebIDL 提案的反馈。

我的主要关注点(以及省略该 FAQ 条目的原因)是范围:绑定 N 种语言的一般问题似乎可能会产生大量开放式(并且可能是非终止式)讨论,特别是考虑到没有人已经这样做了(这当然是先有鸡还是先有蛋的问题)。

我认为讨论 capnproto 实现确实有价值,尽管如此,即使是这么早。

特别是,我试图概述实现应该/可以尝试满足的要求。 我认为列出跨语言类型系统可能尝试解决的常见用例也很有用。

关于 N 对 N 问题,我专注于以下解决方案:

  • 只需要担心 RPC 式的数据传输。 不要试图传递共享的可变数据、类、指针生命周期或任何其他类型的信息,这些信息比“一个向量具有三个字段:'x'、'y' 和 'z',它们都是浮点数”更复杂。

  • 尝试将语言和用例分组到数据处理策略的“集群”中。 在这些集群的中心制定战略; 语言编译器绑定到给定的策略,解释器完成 NxN 的其余工作。


@fgmccabe

原因是我认为这项任务是不可能的。 这有两个根本原因:
一种。 不同的语言具有不同的语义,这些语义不一定包含在类型注释中。 举个例子,Prolog 的评估与 C++ 的评估完全不同:语言本质上是不可互操作的。 (对于 Prolog,您可以替换许多其他语言)

任何不支持类型类的 Haskell 实现都会有效地破坏它并使其无法使用。

是的,这个想法并不是要定义一个完美的“易于与所有语言兼容”的抽象。

也就是说,我认为大多数语言在如何构建数据方面有一些相似之处(例如,他们有一种方式说“每个人都有姓名、电子邮件和年龄”,或者“每个组都有一个人的名单任意大小”)。

我认为可以利用这些相似性来显着减少模块之间的摩擦。 (另见我对卢克瓦格纳的回答)

湾根据定义,任何类型系统的 LCD 都不能保证捕获所有给定语言的类型语言。 这给语言实现者留下了一个非常不舒服的选择:支持他们自己的语言或放弃他们语言类型系统的好处。

是的。 我认为这里的经验法则是“如果它是共享库边界,则使其成为 capnproto 类型,否则,使用您的本机类型”。

另一方面,导出/导入类型的两个表达式似乎带来了它自己的问题:语言系统是否应该验证这两个表达式在某种意义上是一致的? 做这项工作是谁的责任?

是的,我最初想包括一个关于不变检查的部分,另一个关于类型兼容性的部分,但我失去了勇气。

“谁的责任”的答案通常是“被调用者”(因为他们必须假设他们收到的任何数据都是可疑的),但如果解释器可以证明调用者尊重类型不变量,则可以省略检查。

C 类型系统作为语言间 IDL 存在一些问题

明确地说,我并不是建议将其作为 IDL。 相反,我建议二进制接口(C ABI)已经存在,定义良好,并且已经具有广泛的语言支持。 这意味着 WebAssembly 不需要提供另一个解决方案,除非正在解决的问题超出跨语言互操作性。

它在共享地址空间的假设下运行,这是不安全的,并且故意不保留在 WebAssembly 中。

所以我想我在这里看到了部分误解。 我们在这里讨论的 FFI 有两类,一类涉及共享线性内存(更传统的共享内存 FFI),另一类不涉及(更传统的 IPC/RPC)。 我一直在谈论前者,我认为你更关注后者。

当您控制模块时在模块之间共享内存(例如将多个独立模块链接在一起作为整个应用程序的一部分的情况)对于效率来说是可取的,但会牺牲安全性。 另一方面,可以专门为 FFI 共享指定的线性内存,尽管我不知道使用当今的默认工具有多实用。

_不_使用共享内存 FFI 的跨模块互操作,即 IPC/RPC,绝对看起来很适合 WebIDL、capnproto 或该领域的其他建议之一,因为这是他们的基本要素。

我不确定的部分是如何以不牺牲任何一种方式的好处的方式混合这两个类别,因为选择一种方式或另一种方式在很大程度上取决于用例。 至少如前所述,似乎我们只能拥有其中一个,如果可以同时支持两者,我认为那将是理想的。

它没有对动态长度数组、标记联合、默认值、泛型等的原生支持。

我认为现在我意识到这可能无关紧要,因为我意识到我们在谈论两种不同的事情,但只是为了后代:ABI 当然有一个 _representation_ 用于可变长度数组和标记联合,但你是对的,因为 C 确实有一个弱类型系统,但这并不是重点,语言并不是针对 C 类型系统的 C FFI。 C ABI 之所以有用,是因为它提供了一个共同点,语言能够使用该共同点与其他可能不知道与之交互的类型系统概念的语言进行通信。 缺乏更高级别的类型系统功能并不理想,并且限制了您可以通过 FFI 表达的东西的种类,但这些限制也是它在所做的事情上如此成功的部分原因,几乎任何语言都可以找到方法表示通过该接口暴露给它的事物,反之亦然。

C++ 解决了其中一些问题(不是最大的问题,共享地址空间),但添加了一堆在 IPC 中并没有真正有用的概念。 当然,您始终可以使用 C 的超集或 C++ 的子集作为您的 IDL,然后围绕它设计绑定规则,但此时您几乎无法从现有代码中获得任何好处,因此您也可以使用现有代码IDL。

同意,对于 IPC/RPC,C 是一种用于定义接口的糟糕语言。

现在的问题是,常见的 ABI 是存储在线性内存中的字节片。

这当然是我们正在使用的原语,但 C ABI 在此之上定义了很多。

但是在未来,当 GC 提案实施时,一些语言(Java、C#、Python)将在线性内存中存储很少甚至没有。 相反,它们会将所有数据存储在 GC 结构中。 如果其中两种语言尝试通信,将这些结构序列化为字节流只是为了立即反序列化它们将是不必要的开销。

我不相信那些语言会跳到将 GC 推迟到主机,但这只是我的猜测。 在任何情况下,理解主机 GC 管理结构的语言可以使用 C ABI 来决定这些结构的通用表示,就像使用 capnproto 表示它们一样容易,唯一的区别是该表示的规范所在的位置。 也就是说,我对 GC 提案的细节以及它与主机绑定提案的关系只有非常微弱的了解,所以如果我在这里偏离目标,请随意忽略。

TL;DR:我认为我们同意不使用共享线性内存的模块互操作。 但我认为支持共享内存 _is_ 很重要,由于现有的语言支持,C ABI 是该用例的最明智选择。 我希望这个提案随着它的发展而支持两者。

我们需要的只是一种交换字节缓冲区的最有效方式,以及一种语言就格式达成一致的方式。 无需将其修复到一个特定的序列化系统。 如果 Cap'n Proto 最适合此目的,它可以有机地作为常见的默认值出现,而不是由 wasm 强制执行。

我当然有偏见,因为我制作了FlatBuffers ,它在效率上类似于 Cap'n Proto,但更灵活,支持更广泛。 但是,我也不建议 wasm 强制使用这种格式。

给定某些用例,还有许多其他格式可能比这两种格式更可取。

请注意,Cap'n Proto 和 FlatBuffers 都是零复制、随机访问,并且在嵌套格式方面很有效(意味着封装在另一种格式中的效率不低于未封装的格式),这是跨语言要考虑的真实属性沟通。 您可以想象一个 IDL,它允许您为缓冲区指定非常精确的字节布局,包括“以下字节是 Cap'n Proto 模式 X”。

虽然我在巧妙地自我推销,但我可能会向人们指出FlexBuffers ,它有点像无模式的 FlatBuffers。 它具有同样理想的零拷贝、随机访问和廉价的嵌套属性,但可以允许语言在不就架构达成一致的情况下进行通信,无需进行代码生成,类似于使用 JSON 的方式。

@aardappel

我们需要的只是一种交换字节缓冲区的最有效方式,以及一种语言就格式达成一致的方式。 无需将其修复到一个特定的序列化系统。 如果 Cap'n Proto 最适合此目的,它可以有机地作为常见的默认值出现,而不是由 wasm 强制执行。

我理解隐含的一点,wasm 不应该被用作向其他竞争对手强加一个标准的方式,而且我个人对选择哪个 IDL 并不关心。

也就是说,当一切都完成后,橡胶需要在某个时候与道路相遇。 如果 wasm 想要促进跨语言交流(当然,这不是每个人都共享的假设),那么它需要一种标准格式,可以表达的不仅仅是“这些字节组成数字”。 这种格式可以是 capnproto、C 结构、flatbuffers 甚至是特定于 wasm 的东西,但由于@fgmccabe概述的原因,它不能同时是所有这些的子集。

虽然我巧妙地自我推销,但我可能会向人们指出 FlexBuffers,它有点像无模式的 FlatBuffers。 它具有同样理想的零拷贝、随机访问和廉价的嵌套属性,但可以允许语言在不就架构达成一致的情况下进行通信,无需进行代码生成,类似于使用 JSON 的方式。

我看到了吸引力,我认为在编写库时,大多数时候这不是您想要的。 JSON 的问题(除了糟糕的解析时间)是,当您在代码中的某处编写 import JSON 对象时,您最终会在使用数据之前编写大量清理代码,例如:

assert(myObj.foo);
assert(isJsonObject(myObj.foo));
assert(myObj.foo.bar);
assert(isString(myObj.foo.bar));
loadUrl(myObj.foo.bar);

如果您不这样做,则可能存在潜在的安全漏洞。

另请参阅上面的6 - 编译时错误处理


@位行者

是的,我并没有真正考虑共享线性内存的可能性。 我需要一个比我更熟悉 webassembly 设计的人( @lukewagner ?)来讨论它的可行性,以及它是否是实现模块间调用的好方法; 它还取决于 FFI 所依赖的假设有多少被 wasm 的内存布局无效。

例如,FFI 通常依赖于他们的宿主语言使用 C 库这一事实,并让本地库直接访问 malloc 函数。 在两个相互怀疑的模块的背景下,该策略如何转化为 wasm?

我想我应该在这个帖子上说些什么,作为 Cap'n Proto 的创造者,但奇怪的是,我没有发现我有太多的意见。 让我表达一些相邻的想法,这些想法可能有趣也可能不有趣。

我也是 Cloudflare Workers 的技术负责人,这是一个运行 JavaScript 和 WASM 的“无服务器”环境。

我们一直在考虑支持 Cap'n Proto RPC 作为工作人员相互交谈的协议。 目前,它们仅限于 HTTP,因此门槛设置得很低。 :)

在 Workers 中,当一个 Worker 调用另一个 Worker 时,很常见的情况是两者都运行在同一台机器上,甚至在同一进程中。 出于这个原因,像 Cap'n Proto 这样的零拷贝序列化显然很有意义,尤其是对于 WASM Workers,因为它们在线性内存上运行,理论上可以在它们之间物理共享。

我们认为这很合适的第二个鲜为人知的原因是 RPC 系统。 Cap'n Proto 具有完整的对象能力 RPC 协议和承诺流水线,以 CapTP 为模型。 这使得以安全和高效的方式表达丰富的、面向对象的交互变得容易。 Cap'n Proto RPC 不仅仅是一个点对点协议,而是对任意数量的联网方之间的交互进行建模,我们认为这将是一件非常重要的事情。

与此同时,在 WASM 领域,WASI 正在引入基于能力的 API。 似乎这里可能有一些有趣的“协同作用”。

尽管如此,Cap'n Proto 的几个设计目标对于 FFI 的特定用例可能没有意义:

  • Cap'n Proto 消息被设计为与位置无关且连续的,以便它们可以在地址空间之间传输和共享。 指针是相对的,消息中的所有对象都需要分配在连续的内存中,或者至少是少量的段。 与本机对象相比,这使使用模型显着复杂化。 在同一线性内存空间中使用 FFI 时,这种开销被浪费了,因为您可以将本机指针传递给松散的堆对象就好了。
  • Cap'n Proto 消息旨在在模式版本之间向前和向后兼容,包括在不知道模式的情况下无损复制对象和子树的能力。 这需要一些光类型信息直接存储在内容中,Cap'n Proto 将其编码为每个指针的元数据。 如果同时编译通过 FFI 通信的两个模块,则不需要此元数据。
  • 当调用方和被调用方之间存在不可忽略的延迟时,Cap'n Proto RPC 的承诺流水线、路径缩短和排序保证是有意义的。 单个 CPU 上的 FFI 没有这样的延迟,在这种情况下,承诺流水线机制可能只是浪费周期。

简而言之,我认为当您在单独的沙箱中独立部署的模块相互交谈时,Cap'n Proto 非常有意义。 但是对于在单个沙箱中同时部署的模块来说,这可能有点过头了。

感谢您的反馈!

指针是相对的,消息中的所有对象都需要分配在连续的内存中,或者至少是少量的段。 与本机对象相比,这使使用模型显着复杂化。 在同一线性内存空间中使用 FFI 时,这种开销被浪费了,因为您可以将本机指针传递给松散的堆对象就好了。

我不知道共享线性内存方法对于 wasm 有多可行(见上文)。

也就是说,无论哪种方式,我都不认为相对指针的开销会那么糟糕。 WebAssembly 已经使用相对于线性内存开始的偏移量,并且在大多数情况下(我认为)实现有优化ADD指令的技巧,因此使用相对指针的开销也可能被优化掉。

Cap'n Proto 消息旨在在模式版本之间向前和向后兼容,包括在不知道模式的情况下无损复制对象和子树的能力。 [...] 如果同时编译通过 FFI 通信的两个模块,则不需要此元数据。

我不认为这是真的。 为模块提供一种在其边界定义向后兼容类型的方法允许 wasm 使用依赖树模型,同时主要避免 Haskell 的依赖菱形问题。

更大的无意义开销来源是 capnproto xor其变量与默认值进行对比的方式,这在压缩零字节时很有用,但在零复制工作流程中会适得其反。

我不知道共享线性内存方法对于 wasm 有多可行(见上文)。

啊,TBH 我认为我没有足够的背景来理解讨论的那部分。 如果您没有共享地址空间,那么是的,Cap'n Proto 开始变得很有意义。

我很高兴就如何设计这样的格式提供建议。 FWIW 如果我不关心与今天已经存在的应用程序的兼容性,我会在 Cap'n Proto 中更改一些小东西……不过,它主要是低级指针编码细节。

更大的无意义开销来源是 capnproto 将其变量与其默认值进行异端运算的方式,这在压缩零字节时很有用,但在零复制工作流程中适得其反。

有点离题,但 XOR 是一种优化,而不是开销,即使在零拷贝的情况下也是如此。 它确保所有结构都是零初始化的,这意味着如果缓冲区已经为零(无论如何通常都是如此),您不必对对象分配进行任何初始化。 对编译时常量的 XOR 可能会花费 1 个周期,而任何类型的内存访问都会花费更多。

@lukewagner关于“共享线性记忆”部分的任何想法?

我认为共享和不共享线性内存都有用例,最终工具需要支持两者:

在今天的本地应用程序使用静态或动态链接的情况下,共享是有意义的:当所有被组合的代码都是完全可信的,并且它们的组合都使用相同的工具链或使用严格定义的 ABI 时。 不过,它更像是一个更脆弱的软件组合模型。

对于松散耦合的模块集合,不共享内存是有意义的,在这种情况下,经典的 Unix 风格设计会将代码放入由管道连接的单独进程中。 就个人而言,我认为这是一个更组合的软件生态系统更令人兴奋/未来主义的方向,因此我主张将其作为任何旨在通过ESM 集成参与 ESM/npm 生态系统的工具链的默认设置(事实上,今天 Rust 的 wasm-pack/wasm-bindgen 就是这种情况)。 在 Web IDL Bindings 或您提议的扩展附近使用一种机制对我来说很有意义,因为它是一种高效、符合人体工程学、类型化(同步或异步)RPC 的形式。

终于完整阅读了这篇文章,这听起来很像我在这个领域的想法(这个评论框太短了,无法包含?)。

特别是我一直在考虑使用模式最好地描述模块间通信问题。 也就是说,我们不需要 Cap'nProto 序列化格式,我们可以直接使用 schema。 目前我对 Cap'nProto 的模式语言没有任何意见。

从 WASI/ESM+npm 的角度来看,这种形式的解决方案对我来说最有意义。 它是对 ABI 的抽象,不依赖于共享的 ABI。 它本质上允许人们使用 schema-lang API 来描述接口,并在两端使用看似原生的 ABI 跨越这些语言边界进行调用,让主机处理翻译。

特别是,这不包含与另一个模块进行更多协调的用例:如果您确定可以共享 ABI,那么实际上您可以只使用 ABI,任何 ABI,无论是 C 还是 Haskell。 如果你控制和编译所有有问题的 wasm,那么解决这个问题就容易多了。 只有当您进入 npm 情况下加载任意未知代码并且不知道其源语言时,模块之间的模式级互操作才会变得非常有吸引力。 因为我们可以使用 wasm 本身的 LCD——我预测它将遵循与本机库类似的弧线,并使用 C ABI——或者我们可以使用语言的 LCD,以模式语言编码。 通过使需求 2) 成为软需求,模式可以更加灵活,例如,应该可以有效地从 C 转换为 Rust 再到 Nim,但是 C 到 Haskell 的开销更多并不是一个交易破坏者。

特别是我一直在考虑使用模式最好地描述模块间通信问题。 也就是说,我们不需要 [a] 序列化格式,我们可以使用模式。

我倾向于同意前者,但我不确定后者是否遵循。 谁来实现模式? 即使主机进行传输,在某些时候您也必须定义两端实际消耗/产生的 Wasm 值/字节,并且每个模块必须将自己的数据转换为主机可以理解的形式。 甚至可能有多种形式可用,但这仍然与序列化格式没有什么不同,只是更高级一些。

应该可以有效地从 C 转换到 Rust 到 Nim,C 到 Haskell 有更多的开销并不是一个交易破坏者。

也许不是,但你必须意识到其中的含义。 特权类 C 语言意味着 Haskell 不会将这种抽象用于 Haskell 模块,因为会产生开销。 这反过来意味着它不会为自己的库参与相同的“npm”生态系统。

这里的“Haskell”只是几乎所有高级语言的替代品。 绝大多数语言都不像 C 语言。

我并不声称有更好的解决方案,但我认为我们必须对任何单个 ABI 或模式抽象对于一般语言群体的效率和吸引力保持现实,超越通常的 FFI 风格的单向互操作性。 特别是,我不相信泛语言包生态系统是一个过于现实的结果。

特权类 C 语言意味着 Haskell 不会将这种抽象用于 Haskell 模块,因为会产生开销。 这反过来意味着它不会为自己的库参与相同的“npm”生态系统。

这里的“Haskell”只是几乎所有高级语言的替代品。 绝大多数语言都不像 C 语言。

能不能给出一些具体的用例? 理想情况下,Haskell 或其他语言中的现有库很难转换为序列化模式?

我怀疑它主要归结为实用程序库与业务库。 例如,依赖于语言泛型的容器、排序算法和其他实用程序不能很好地转换为 wasm,但解析器、gui 小部件和文件系统工具可以。

@PoignardAzur ,翻译它们并不难,但需要它们复制(序列化/反序列化)每个跨模块调用两端的所有参数/结果。 显然,您不想为每个语言内部库调用支付该费用。

特别是在 Haskell 中,您还有一个额外的问题,即复制与懒惰的语义不兼容。 在其他语言中,它可能与有状态数据不兼容。

谁来实现模式? 即使主机进行传输,在某些时候您也必须定义两端实际消耗/产生的 Wasm 值/字节,并且每个模块必须将自己的数据转换为主机可以理解的形式。 甚至可能有多种形式可用,但这仍然与序列化格式没有什么不同,只是更高级一些。

主机实现模式。 模式根本不描述字节,让它成为一个实现细节。 这是借鉴了 WebIDL 绑定提案的设计,其中有趣的一点在于从 C 结构到 WebIDL 类型的转换。 这种设计使用 Wasm 抽象接口类型(我建议缩写:WAIT)而不是 WebIDL 类型。 在 WebIDL 提案中,当数据被“转换为 WebIDL”时,我们不需要或不想强制要求数据的二进制表示,因为我们希望能够直接从 wasm 转到浏览器 API,而不会在中间停止。

特权类 C 语言意味着 Haskell 不会将这种抽象用于 Haskell 模块,因为会产生开销。

哦,100%同意。 我应该完成这个示例以更清楚地说明这一点:同时,Haskell 到 Elm 到 C# 可能同样有效(假设它们使用 wasm gc 类型),但 C# 到 Rust 可能有开销。 我认为在跨越语言范式时没有办法避免开销。

我认为您的观察是正确的,我们需要尝试避免授予任何语言的特权,因为如果我们不能对给定的语言进行足够的人体工学 + 高性能,他们将不会看到使用界面的价值,因此不会参与生态系统.

我相信通过抽象类型而不指定有线格式,我们能够为主机提供更多的优化余地。 我认为一个非目标是说“C 风格的字符串是有效的”,但目标是说“[想要] 推理 C 风格字符串的语言可以有效地做到这一点”。 或者,不应该祝福任何一种格式,但某些兼容的调用链应该是有效的,并且所有调用链都应该是可能的。

通过调用链,我的意思是:

  1. C -> Rust -> Zig -> Fortran,高效
  2. Haskell -> C# -> Haskell,高效
  3. C -> Haskell -> Rust -> Scheme,效率低下
  4. Java -> Rust,效率低下

这里的“Haskell”只是几乎所有高级语言的替代品。 绝大多数语言都不像 C 语言。

是的,这就是我使用 Haskell 作为具体语言的意图。 (虽然 Nim 可能是类 C 语言的一个不好的例子,因为它也大量使用了 GC)

——

我一直在考虑抽象类型的另一种方式是作为 IR。 与 LLVM 描述多对多关系(多语言 -> 一个 IR -> 多目标)的方式相同,wasm 抽象类型可以调解多对多映射,语言 + 主机 ->语言+主机。 这个设计空间中的某些东西将 N^2 映射问题变成了 N+N 映射问题。

主机实现模式。

好吧,这还不够,每个模块都必须实现一些东西,以便主机可以找到数据。 如果主机需要 C 布局,那么您必须定义此 C 布局,并且每个客户端都必须在内部对其进行编组/解组。 这与序列化格式并没有什么不同。

即使我们这样做了,定义序列化格式仍然很有用,例如,对于需要在单个引擎之间传输数据的应用程序,例如通过网络或基于文件的持久性。

好吧,这还不够,每个模块都必须实现一些东西,以便主机可以找到数据。 如果宿主需要 C 布局,那么你必须定义这个 C 布局

主人不应该期待任何事情,但需要支持一切。 更具体地说,以 webidl-bindings 提案为例,我们有utf8-cstrutf8-str ,分别取i32 (ptr)i32 (ptr), i32 (len) 。 没有必要在规范中强制要求“主机内部将其表示为 C 字符串”,以便能够在它们之间具体映射。
所以,每个模块都实现了一些东西,是的,但是数据的表示不需要在抽象数据/模式层中表达,这就是为什么这为我们提供了对该数据布局进行抽象的属性。
此外,这在具体 wasm 类型和抽象中间类型之间映射的绑定层是可扩展的。 要添加 Haskell 支持(将字符串建模为字符的 cons 列表和字符数组),我们可以添加utf8-cons-strutf8-array-str绑定,它们期望(并验证)wasm 类型(使用当前gc 提案语法) (type $haskellString (struct (field i8) (field (ref $haskellString))))(type $haskellText (array i8))

也就是说,每个模块决定了数据的来源。 抽象类型 + 绑定允许在模块查看相同数据的方式之间进行转换,而不会将单个表示视为某种规范。

抽象类型(子集)的序列化格式会很有用,但可以作为模式格式的使用者来实现,我认为这是一个正交问题。 FIDL 我相信对于可以通过网络传输的类型子集具有序列化格式,不允许物化不透明句柄,同时允许不透明句柄在系统内传输(IPC 是,RPC 否)。

您所描述的内容与我的想法非常接近,但有一个很大的警告:模式必须具有少量固定数量的可能表示。 不同表示之间的桥接是一个 N*N 问题,这意味着表示的数量应该保持很少,以避免让 VM 编写者负担过重。

因此,添加 Haskell 支持需要使用现有绑定,而不是添加自定义绑定。

一些可能的表示:

  • C 风格的结构和指针。
  • 实际的 capnproto 字节。
  • GC 类。
  • 用作 getter 和 setter 的闭包。
  • Python 风格的字典。

这个想法是,虽然每种语言都不同,并且存在一些极端的异常值,但您可以将相当多的语言放入相当少的类别中。

因此,添加 Haskell 支持需要使用现有绑定,而不是添加自定义绑定。

取决于您正在考虑的现有绑定的粒度级别。 编码每个可能绑定的 N<->N 种语言是 2*N*N,但 N<->IR 是 2*N,进一步如果你说 N<->[通用绑定样式]<->IR,其中的数字常见格式是 k,你说的是 2*k,其中 k < N。

特别是,使用我描述的方案,您可以免费获得 Scheme(它会重复使用utf8-cons-str )。 如果 Java 也将字符串建模为字符数组,那就是utf8-array-str绑定。 如果 Nim 在幕后使用 string_views, utf8-str 。 如果 Zig 符合 C ABI,则utf8-cstr 。 (我不知道 Java/Nim/Zig 的 ABI,所以我之前没有提到它们作为具体示例)

所以,是的,我们不想为每种可能的语言添加绑定,但我们可以为每个 IR 类型添加一些绑定以覆盖绝大多数语言。 我认为这里存在分歧的空间是,“几个”绑定有多少,最佳点是什么,我们是否支持一种语言的 ABI 的标准应该有多严格?
我对这些问题没有具体的答案。 我试图给出很多具体的例子来更好地说明设计空间。

此外,我会断言我们绝对希望为每个抽象类型指定多个绑定,以避免授予任何一种数据样式的特权。 如果我们暴露给字符串的唯一绑定是utf8-cstr ,那么所有非 C-ABI 的语言都必​​须处理这种不匹配。 我对增加 VM 编写复杂性的一些不小的因素感到满意。
生态系统中的总工作量是 O(VM 工作量 + 语言实现工作量),这两个术语都以某种方式扩展,N = 语言数量。 设 M=嵌入器数量,k=绑定数量,a=给定语言需要实现的平均绑定数量,a<=k。 至少我们有 M+N 个单独的 wasm 实现。
天真的方法,每个 N 语言独立实现 ABI FFI 与其他 N 语言,是 O(M + N*N)。 这就是我们在原生系统上所拥有的,这是一个强烈的信号,任何 O(N*N) 都会导致与原生系统没有区别的结果。
第二种简单的方法,其中每个 VM 都需要实现所有 N*N 绑定:O(M*N*N + N),这显然更糟。
我们试图提出的是,我们有 k 个绑定在抽象语言之间映射,映射回所有语言。 这意味着每个 VM 都有 k 个工作。 对于每种语言,我们只需要实现绑定的一个子集。 总工作量是 M*k + N*a,也就是 O(M*k + N*k)。 请注意,在 k=N 的情况下,VM 端“仅”M*N,因此对于任何给定的 VM,它“仅”与语言数量呈线性关系。 显然,尽管我们想要 k << N,否则这仍然是 O(N*N),并不比第一个解决方案好。
尽管如此,O(M*k + N*k) 更可口。 如果 k 是 O(1),那么整个生态系统的实现数量是线性的,这是我们所涉及的工作量的下限。 更可能的界限是 k 为 O(log(N)),我对此仍然非常满意。

这是一个很长的说法,我完全可以通过一些恒定因素增加此功能的 VM 复杂性。

我们可以为每个 IR 类型添加一些绑定以覆盖绝大多数语言。

这是我认为根本不正确的关键基本假设。 我的经验是(至少!)与语言实现一样多的表示选择。 它们可以任意复杂。

以 V8 为例,它本身就有几十个(!)字符串表示,包括不同的编码、异构绳索等。

Haskell 的情况比您描述的要复杂得多,因为 Haskell 中的列表是惰性的,这意味着对于字符串中的每个单个字符,您可能需要调用 thunk。

其他语言对字符串的长度使用有趣的表示,或者不显式存储它但要求计算它。

这两个示例已经表明声明式数据布局并没有削减它,您通常需要能够调用运行时代码,而运行时代码又可能有自己的调用约定。

这只是字符串,在概念上是一种相当简单的数据类型。 我什至不想考虑语言表示产品类型(元组/结构/对象)的无数种方式。

然后是接收方,您必须能够创建所有这些数据结构!

因此,我认为我们甚至可以远程支持“绝大多数语言”是完全不现实的。 相反,我们会开始给予少数人特权,同时已经发展了一个包含任意东西的大型动物园。 这在多个层面上似乎是致命的。

我的经验是(至少!)与语言实现一样多的表示选择。 它们可以任意复杂。

我完全同意。 我认为试图设计以某种方式覆盖大多数语言内部数据表示的类型根本不容易处理,并且会使生态系统变得过于复杂。

最后,在数据方面,语言之间只有一个最小的公分母:“缓冲区”的公分母。 所有语言都可以读取和构建这些。 它们高效而简单。 是的,他们偏爱能够直接处理其内容的语言,但我不认为这是通过以某种方式将(懒惰的)cons 单元提升到相同支持级别来解决的不平等问题。

事实上,您可以只使用一种数据类型:指针 + len 对。 然后你只需要一个“模式”来说明这些字节中的内容。 它是否承诺符合 UTF-8? 最后一个字节是否保证始终为 0? 前 4/8 字节是长度/容量字段吗? 所有这些字节都是可以直接发送到 WebGL 的小端浮点数吗? 这些字节可能是现有序列化格式的模式 X? 等等?

我提出了一个非常简单的模式规范,可以回答所有这些问题(不是现有的序列化格式,而是更底层、更简单和特定于 wasm 的东西)。 然后,以指定的格式有效地读取和写入这些缓冲区成为每种语言的负担。 然后,中间的层可以通过复制或在可能的情况下通过引用/视图在不处理的情况下盲目地传递缓冲区。

这是我认为根本不正确的关键基本假设。 我的经验是(至少!)与语言实现一样多的表示选择。 它们可以任意复杂。

我同意这是关键的基本假设。 我不同意这不是真的,尽管我认为由于语义上的细微差别,我认为我没有说清楚。

绑定并不意味着完美地映射到所有语言表示,它们只需要足够好地映射到所有语言。

这是一个至关重要的潜在假设,无论编码如何,它都可以轻松应对。 @aardappel提出的另一个方向并将字节实际具体化为可解码的缓冲区的提议也建立在假设它是任何给定程序的语义的有损编码的基础上,有些比其他

Haskell 的情况比您描述的要复杂得多,因为 Haskell 中的列表是惰性的,这意味着对于字符串中的每个单个字符,您可能需要调用 thunk。

我实际上已经忘记了这一点,但我认为这不重要。 目标不是在表示 Haskell 字符串的同时保留它们跨模块边界的所有语义细微差别。 目标是按值将 Haskell 字符串转换为 IR 字符串。 这必然涉及计算整个字符串。

这两个示例已经表明声明式数据布局并没有削减它,您通常需要能够调用运行时代码,而运行时代码又可能有自己的调用约定。

无论我们如何指定绑定(甚至如果我们为绑定指定任何内容),建模的方法都是在用户空间中处理它。 如果一种语言的类型表示没有直接映射到绑定,则需要转换为可以映射的表示。 例如,如果 Haskell 的字符串确实表示为(type $haskellString (struct (field i8) (field (func (result (ref $haskellString)))))) ,则需要转换为严格字符串并使用类似 Scheme 的绑定,或转换为 Text 数组并使用类似 Java 的绑定,或者转换为CFFIString 并使用类似 C 的绑定。 拥有多个不完美绑定类型的价值主张是,其中一些对于 Haskell 来说比其他类型更不笨拙,并且可以在不需要修改编译器的情况下构造 Wasm-FFI 类型。

这只是字符串,在概念上是一种相当简单的数据类型。 我什至不想考虑语言表示产品类型(元组/结构/对象)的无数种方式。
然后是接收方,您必须能够创建所有这些数据结构!

我很困惑,我认为“语言之间的绑定是完全不可能的,所以我们根本不应该尝试”,但我相信你所说的更像是“我不相信此处描述的方法易于处理”,这似乎更合理。 我特别反对这一论点,因为它没有描述前进的道路。 鉴于这个问题“非常难”,我们该怎么办?

相反,我们会开始给予一些特权

几乎可以肯定。 问题之一是,少数获得最佳支持的语言在多大程度上享有特权? 弱势语言在寻找符合人体工程学的解决方案方面有多少回旋余地?

同时已经发展了一个包含任意东西的大型动物园。

我不确定你的意思。 我对任意性的解释是“我们支持哪些语言”,但这与“给予少数特权”相同,这会重复计算。 因此,这只会在那个层面上存在致命缺陷,而不是多个 :D

@aardappel的简短版本是,如果声明性抽象方法失败,那是我的备用计划:走完全相反的方向并描述序列化格式。 据观察,Web 本身几乎完全建立在 Text 上,因为它是一个极低的公分母。 所有工具都可以轻松理解文本,因此在它之上可以实现诸如 Web 之类的东西。

我对可能使该方法难以处理的数据缓冲区的最大担忧是我们如何处理引用类型? 我最好的想法是共享表并将索引序列化到它们中,但我没有全面了解它的实际工作情况。

@jgravelle-google 也许引用类型应该分开? 因此,给定的函数原始签名可能是ref ref i32 i32 i32 i32 ,它实际上是 2 个 anyref 后跟 2 个特定类型的缓冲区(在上面的假设模式中指定)。

(顺便说一句,我不熟悉 Haskell,但是将字符串作为惰性字符列表的想法让我大吃一惊。什么时候字节链表是做任何事情的最有效或最方便的方式?我知道 Haskell 需要一切是不可变的,并且链表允许廉价的前置,但是您可以在不使用链表的情况下获取和操作不可变的字符串。)

我对可能使该方法难以处理的数据缓冲区的最大担忧是我们如何处理引用类型? 我最好的想法是共享表并将索引序列化到它们中,但我没有全面了解它的实际工作情况。

这就是我建议使用 capnproto 作为编码的原因之一。 参考表或多或少是内置的。

在任何情况下,我们都希望我们选择的任何格式将引用类型作为一等公民,可以放置在数据图中的任何位置。 (例如在可选、数组、变体等中)

感谢大家的反馈。

我认为我们已经开始到了这样的地步:我们主要是在几乎没有变化的情况下再次重读相同的论点,所以我将更新提案并尝试解决每个人的担忧。 一旦我写完更新的提案,我可能会重新开始一个新问题。

总结到目前为止的反馈:

  • 关于哪种序列化格式(如果有)最适合 wasm 几乎没有共识。 替代方案包括 FlatBuffers、带有原始指针的 C ABI 结构图、定制的 wasm IDL 格式或上述的某种组合。

  • 提案需要更强的负空间。 多个读者对提案的范围以及它旨在促进哪些用例(静态与动态链接、模块到主机与主机到主机、共享可变数据与传递不可变消息)感到困惑。

  • @lukewagner对连接互不信任的模块的模块系统与 ESM 集成相结合的潜力表达了一些热情。 提案的下一次迭代应该扩大这一潜力; 特别是,我相信拥有向后兼容的类型系统将允许 wasm 使用类似 npm 的依赖树模型,同时避免依赖钻石问题的冲击。

  • 关于功能主题的反馈很少,即可以返回和传递但不能从原始数据创建的不透明值。 我认为这表明下一次迭代应该更加重视它们。

  • 一些读者对跨语言类型系统的可行性表示担忧。 这些担忧有些模糊且难以定义,部分原因是主题非常抽象,部分原因是迄今为止的提案本身非常模糊,这与@lukewagner的先有鸡还是先有蛋的问题相呼应。 具体的故障状态包括:

    • 过分关注高度可见的语言,而将更多的小众语言抛在脑后。
    • 有一个泄漏的抽象,试图过于笼统,但不能方便或有效地涵盖任何人的用例。
    • 覆盖太多情况,创建一个臃肿的 N*N 实现仍然存在上述问题。

下一次提案迭代需要以某种方式解决这些问题。

特别是,我认为讨论会从一些稻草人的例子中受益匪浅。 这些示例将包括至少两个库,用不同的语言编写,具有不同的数据布局(例如 C++ 和 Java),由至少两个不同语言(例如 Rust 和 Python)的最小程序使用,以说明 n*n 问题和解决它的策略。

此外,正如读者所指出的,该提案目前将类型模式的思想与其表示的思想纠缠在一起。 虽然该提案在布置表示格式的要求方面做得很好,但它首先需要布置抽象类型模式的要求。

无论如何,再次感谢迄今为止参与此讨论的所有人。 我会尽快提出更彻底的建议。 如果这里有人有兴趣帮我写它,一定要给我发电子邮件!

@jgravelle-谷歌:

无论我们如何指定绑定(甚至如果我们为绑定指定任何内容),建模的方法都是在用户空间中处理它。

是的,我同意,我的论点与@aardappel的类似:如果这是我们通常必须做的事情,那么我们应该简单地接受这一点,不要尝试临时的事情来改善一些奇怪的情况。 Userland 是转换所属的地方,本着 Wasm 中其他一切的精神。

我很困惑,我看到有句话说“语言之间的绑定是完全不可能的,所以我们根本不应该尝试,”

我认为为语言之间的数据互操作定义 DDL(类型方案)是完全可取的。 我只是认为将转换构建为 Wasm 并不容易。 转换需要在用户空间中实现。 绑定层只是规定了用户代码必须生成/使用的格式。

同时已经发展了一个包含任意东西的大型动物园。
我不确定你的意思。 我对任意性的解释是“我们支持哪些语言”,但这与“给予少数特权”相同,这会重复计算。

抱歉,我的意思是我怀疑这些转换不会有任何非常规范的内容。 所以他们的选择是“任意的”和他们各自的语义。

我们如何处理引用类型?

啊,这是个好问题。 FWIW,我们现在正试图为 Dfinity 平台的 IDL/DDL 解决这个问题。 只要只有anyref,解决方案就相当简单:序列化格式定义了两部分,一个是投影透明数据的内存片,一个是投影包含的引用的表片。 多个引用类型相应地需要多个表切片。 棘手的问题是一旦 ref 类型集不再是有限的(例如,使用类型化函数引用)该怎么办。

一旦我们有了 GC 类型,就应该有一种替代方法来提供数据,即作为 GC 值。 在这种情况下,引用不是问题,因为它们可以自由混合。

@PoignardAzur

顺便说一句,我不熟悉 Haskell,但是将字符串作为惰性字符列表的想法让我大吃一惊。

是的,我相信现在人们普遍认为这是一个错误。 但它展示了即使对于“简单”数据类型也有多少多样性。

@罗斯伯格

我认为为语言之间的数据互操作定义 DDL(类型方案)是完全可取的。 我只是认为将转换构建为 Wasm 并不容易。 转换需要在用户空间中实现。

我同意,并补充说:我对为此向 wasm 规范添加一些内容持怀疑态度,因为我不认为 wasm 比其他平台更需要跨语言解决方案,而且我不认为 wasm比其他平台有更大的能力来实现这样的解决方案。 这里的 wasm 对我来说没有什么明显的特别之处,所以我不确定为什么我们可以在这方面做得比标准解决方案更好,例如@aardappel提到的缓冲区。 (但我确实认为用户空间中的实验非常有趣,因为它在所有平台上都是如此!)

至少在 Web 上,wasm 拥有的一件特别的事情是用于字符串和数组等的 JavaScript/Web API 类型。 能够与他们互动显然很重要。

我不认为 wasm 比其他平台更需要跨语言解决方案

我认为确实如此。 默认情况下在网络上使用意味着代码可以并且将在不同的上下文中运行。 就像<script src="http://some.other.site/jquery.js"> ,我很乐意看到人们以一种跨源的方式组合 wasm 库。 由于 Web 提供的短暂性和可组合性特性,能够与外部模块交互的附加值比在本机系统上更高。

我不认为 wasm 比其他平台更有能力实现这样的解决方案。

我认为确实如此。 由于 wasm 由嵌入器/在主机中运行,因此代码生成被有效地抽象化。 因此,VM 拥有更多的工具和余地来支持本机系统上无法实现的更高级别的构造。

所以我认为这个领域的某些东西比其他系统更有价值,更有可能,这就是为什么 wasm 在这种情况下很特别。 对我来说,JS 互操作是更普遍概念的一个特例,即 wasm 模块需要能够与具有截然不同世界观的外部事物对话。

为此,一条前进的道路是暂时将其完全推入工具级互操作中,并推迟标准化,直到我们有一个成功的格式。 因此,如果目标是让主要的 wasm 包管理器的生态系统使用给定的接口格式(是 NPM 或 WAPM 还是一些尚未创建的包管理器?)那么这可以独立于标准化而发生。 理论上,我们可以标准化人们已经在做的事情,以获得更好的性能,但人体工程学可以在用户空间中实现。 存在的风险是获胜的中间语言格式不利于优化,我们最终会得到一个次优的事实上的标准。 如果我们可以设计一种格式,以便以后标准化(自定义部分中的声明式样式基本上就足够了?),可以消除这种风险,但也会延迟任何性能改进。 对我来说,表现是拥有这种东西的不那么令人兴奋的动力之一,所以我对此很满意,尽管其他人可能不同意。

(那是 NPM 或 WAPM 还是一些尚未创建的包管理器?)

我认为 WAPM 成为可行的包管理器还为时过早。 在 wasm 包管理器变得可行之前,我们需要将 ESM 集成、WASI 和某种形式的语言间绑定等功能标准化。

事实上,我认为 WAPM 甚至没有依赖管理。

我不认为 wasm 比其他平台更需要跨语言解决方案

我认为确实如此。 默认情况下在网络上使用意味着代码可以并且将在不同的上下文中运行。 以同样的方式,一个人可能

转换需要在用户空间中实现。 绑定层只是规定了用户代码必须生成/使用的格式。

这对我来说是一个很好的总结。

在我写新草稿时,我有一个悬而未决的问题要问这个线程上的每个人:

是否有一些现有的库,如果将其编译为 WebAssembly,您希望能够从任何语言使用?

我本质上是在寻找潜在的用例来作为设计的基础。 我有我自己的(特别是 React、Bullet 引擎和插件系统),但我想要更多的例子来使用。

@PoignardAzur C 中的许多语言使用相同的 Perl 兼容正则表达式 (PCRE) 库,但在浏览器嵌入中,它们可能应该使用 JS 的 Regex API。

@PoignardAzur BoringSSL 和 libsodium 浮现在脑海中。

还有 Cap'n Proto RPC 实现,但这是一个奇怪的实现:Cap'n Proto 的 _serialization_ 层实际上必须在每种语言中独立实现,因为它大部分是一个宽而浅的 API 层,需要惯用和内联-友好的。 RPC 层 OTOH 很窄但很深。 原则上,通过在 FFI 边界上传递 capnp 编码的字节数组引用,应该可以在任何任意语言的序列化实现背后使用 C++ RPC 实现......

我认为做最终提议的事情需要对 WebAssembly 本身进行一些相当侵入性的改变,因为它已经存在——但可以说是值得的。

我要指出,SmallTalk 世界在这样的努力中获得了一些积极的经验,这些经验可以为他们开发状态复制协议 (SRP) 提供信息,这是一种高效的序列化协议,可以相当有效地表示任何类型的任何大小。 我曾考虑将其作为 VM 甚至 FPGA 的本机内存布局,但还没有开始尝试。 我知道它至少被移植到了另一种语言 Squeak,并且效果很好。 由于与本提案的问题、挑战和经验有很强的重叠,因此值得一读。

我理解为什么 Web IDL 是作为绑定语言的默认提议:它是 Web 的历史性且以某种方式成熟的绑定语言。 我非常支持这个决定,而且我很可能也会做出同样的决定。 尽管如此,我们可能会认识到它几乎不适合其他上下文(理解,其他主机/语言)。 Wasm 被设计为与主机无关,或与语言/平台无关。 我喜欢使用成熟的 Web 技术并为非 Web 场景找到用例的想法,但在 Web IDL 的情况下,它似乎真的与 Web 相关联。 这就是我非常密切关注这些对话的原因。

我打开了https://github.com/WebAssembly/webidl-bindings/issues/40 ,这让我在这里问了一个问题,因为我没有看到它(或者我错过了)。

在整个绑定故事中,不清楚“谁”负责生成绑定:

  • 它是编译器(将程序转换为 Wasm 模块)吗?
  • 它是程序作者吗(因此,绑定是手写的)?

我认为两者都有效。 而在 Web IDL 的情况下,它似乎显示出一些限制(参见上面的链接)。 也许我只是错过了过程中的一个重要步骤,因此,请考虑忘记我的信息。

即使目标是“重新聚焦”Web IDL 以减少以 Web 为中心,但现在,它_是_非常以 Web 为中心的。 并且提议提出替代方案,因此出现此线程。 因此,我担心潜在的碎片化。 理想情况下(这就是目前 Wasm 的设计方式),给定一个包含其绑定的 Wasm 模块,可以按原样运行它。 对于用 Web IDL、Cap'n' Proto、FlatBuffers 编写的绑定,我很确定并非所有编译器或程序作者都会以不同的语法编写相同的绑定以实现真正的跨平台。 有趣的是,这是支持手写绑定的一个论点:人们可以通过为平台 P 编写绑定来为程序做出贡献。但是我们承认这根本不是理想的。

总结一下:我担心 Web 和非 Web 绑定之间可能存在的碎片。 如果持有一种非 Web 绑定语言,它会被 Web 浏览器实际实现吗? 他们必须编写绑定“Wasm ⟶ 绑定语言 B ⟶ Web IDL”。 请注意,这是所有主机的相同场景:Wasm ⟶ 绑定语言 B ⟶ 主机 API。

对于那些好奇的人,我在 Wasmer 工作,我是PHP-Python-Ruby-和 Go - Wasm 集成的作者。 我们开始有一个很好的游乐场来为非常不同的主机破解不同的绑定。 如果有人希望我整​​合不同的解决方案、收集反馈或尝试进行实验,我们都愿意合作并投入更多资源。

“webIDL 绑定”的当前方向可能远离 webIDL
本身。 然而,困境是这样的:

用于表达模块间和模块宿主的“自然”语言
互操作性比 WASM 的自然语言要丰富得多。 这个
意味着任何可用的 IDL 等价物看起来都相当随意
WASM 的观点。

另一方面,对于那些从 C/C++ 的镜头看世界的人
(以及 Rust 及其同类)任何比 WASM 模型更丰富的东西都有可能被
无法使用。 我们已经可以通过整合 ref 的难度看到这一点
类型进入工具链。

此外,WASM 的直接任务不是支持一般
语言间的互操作性。 也不应该是 IMO。

(有一个更有限的跨语言互操作性版本,我
相信不仅可以支持,而且很重要:它存在于我们所有的
能力提供者和能力使用者可以满足的兴趣
摩擦最小。 (能力是管理说的,但我有
没有找到更好的术语。)这需要不同风格的 IDL 和一个
这比完全跨语言所需的更容易实现
互操作。)

底线:有一个与 IDL 等效的案例,我们需要它
支持跨所有权边界的互操作。 结果如何
目前还不清楚。

2019 年 6 月 24 日星期一上午 7:02 Ivan Enderlin通知@ github.com
写道:

我理解为什么 Web IDL 是作为绑定语言的默认提议:
它是用于 Web 的历史悠久且以某种方式成熟的绑定语言。 一世
非常支持这个决定,而且我很可能会做出
相同的。 尽管如此,我们可能会认识到它几乎不适合其他上下文
(理解,其他主机/语言)。 Wasm 被设计为与主机无关,
或语言/平台无关。 我喜欢使用成熟网络的想法
技术并为非 Web 场景找到用例,但在这种情况下
Web IDL,它似乎真的与 Web 相关联。 这就是我非常喜欢的原因
密切关注此处的这些对话。

我打开了 WebAssembly/webidl-bindings#40
https://github.com/WebAssembly/webidl-bindings/issues/40 ,这导致了我
在这里问一个问题,因为我没有看到提到它(或者我错过了)。

在整个绑定故事中,不清楚“谁”对
生成绑定:

  • 它是编译器(将程序转换为 Wasm 模块)吗?
  • 它是程序作者吗(因此,绑定是手写的)?

我认为两者都有效。 而在 Web IDL 的情况下,它似乎显示了一些
限制(见上面的链接)。 也许我只是错过了重要的一步
过程,所以,考虑忘记我的信息。

即使目标是“重新聚焦”Web IDL 以减少以 Web 为中心,现在,
非常Web为中心。 并且提议提出替代方案,
因此这个线程。 因此,我担心潜在的碎片化。
理想情况下(这就是目前 Wasm 的设计方式),给定一个 Wasm 模块
包括它的绑定,可以按原样在任何地方运行它。 和
用 Web IDL、Cap'n' Proto、FlatBuffers 编写的绑定,不管怎样,我是
很确定并非所有编译器或程序作者都会编写相同的代码
不同语法的绑定是真正的跨平台。 有趣的是,这是
支持手写绑定的论点:人们可以为
通过为平台 P 编写绑定来编写程序。但是让我们承认这不会是
完全理想。

总结一下:我担心 Web 和
非 Web 绑定。 如果持有非 Web 绑定语言,会不会是
由 Web 浏览器实际实现? 他们将不得不写
绑定“Wasm ⟶ 绑定语言 B ⟶ Web IDL”。 请注意,这是相同的
适用于所有主机的场景:Wasm ⟶ 绑定语言 B ⟶ 主机 API。

对于那些好奇的人,我在 Wasmer 工作并且我是 PHP 的作者-
https://github.com/wasmerio/php-ext-wasm ,Python-
https://github.com/wasmerio/python-ext-wasm ,Ruby-
https://github.com/wasmerio/ruby-ext-wasm和 Go-
https://github.com/wasmerio/go-ext-wasm Wasm 集成。 我们开始
有一个很好的游乐场来破解不同的绑定
主机。 如果有人要我整合不同的解决方案,收集
反馈,或尝试尝试,我们都很开放并准备好合作
并投入更多资源。


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/WebAssembly/design/issues/1274?email_source=notifications&email_token=AAQAXUD6WA22DDUS7PYQ6F3P4DHYRA5CNFSM4HJUHVG2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW6ZLODNXP020000000000000000000003
或静音线程
https://github.com/notifications/unsubscribe-auth/AAQAXUGM66AWN7ZCIVBTXVTP4DHYRANCNFSM4HJUHVGQ
.

——
弗朗西斯·麦凯布
瑞典

我们已经可以通过将 ref 类型集成到工具链中的困难看到这一点。

我不会说其他语言,但 Rust + wasm-bindgen 已经支持:

所以我很好奇:你指的是什么困难?

我对困难的理解更多是在 C++ 端。 Rust 有足够强大的元编程来使这在语言端更合理,但用户级 C++ 有一个更难的推理时间,例如 anyrefs。

我很想在这里听到更多关于 C++ 特定问题的信息。 (它们是 C++ 特定的还是 LLVM 特定的?)

C++ 不知道 ref 类型是什么。 所以你不能把它放在一个
任意对象。 不是语言的真正组成部分; 更像是一个文件
描述符。 一个字符串的有趣的地方。

在 2019 年 6 月 24 日星期一下午 3:07,Alon Zakai通知@ github.com 写道:

我很想在这里听到更多关于 C++ 特定问题的信息。 (他们是
C++ 特定的还是 LLVM 特定的?)


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/WebAssembly/design/issues/1274?email_source=notifications&email_token=AAQAXUDW237MUBBUUJLKS6LP4FARJA5CNFSM4HJUHVG2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LOLDNX59JW63LOLVWSQ520000000000000000000000000000000001001001
或静音线程
https://github.com/notifications/unsubscribe-auth/AAQAXUB4O3ZX4LRQSQRL763P4FARJANCNFSM4HJUHVGQ
.

——
弗朗西斯·麦凯布
瑞典

@fgmccabe离线交谈,C++ 和 Rust 都不能直接将 ref 类型存储在结构中,因为该结构将存储在线性内存中。 C++ 和 Rust 当然都可以间接处理 ref 类型,就像它们处理文件描述符、OpenGL 纹理等一样——使用整数句柄。 我认为他的观点是,这两种语言都不能“很好地”/“本机地”处理 ref 类型(如果我错了,请纠正我!)我同意 - 这些语言在 ref 类型上总是处于劣势操作,与 GC 语言相比。

我仍然很好奇这里是否有任何特定于 C++ 的内容。 我不认为有?

我对 C++ 困难的理解是,如果您说:

struct Anyref; // opaque declaration
void console_log(Anyref* item); // declaration of ref-taking external JS API
Anyref* document_getElementById(const char* str);

void wellBehaved() {
  // This should work
  Anyref* elem = document_getElementById("foo");
  console_log(elem);
}

void notSoWellBehaved() {
  // ????
  Anyref* elem = document_getElementById("bar");
  Anyref* what = (Anyref*)((unsigned int)elem + 1);
  console_log(what);
}

好消息是,我相信后一个例子是 UB(无效的指针一旦创建就是 UB),但是我们如何尝试在 LLVM IR 中对其进行建模?

@jgravelle-google 我认为即使struct Anyref;预先假定它在线性记忆中有意义。 相反,为什么不使用前面提到的整数句柄对其进行建模,例如 OpenGL 纹理、文件句柄等?

using Anyref = uint32_t; // handle declaration
void console_log(Anyref item); // declaration of ref-taking external JS API
Anyref document_getElementById(const char* str);

void wellBehaved() {
  // This should work
  Anyref elem = document_getElementById("foo");
  console_log(elem);
}

整数句柄在使用时必须在表中查找——同样,这只是使用线性内存的语言(如 C++ 和 Rust)的一个缺点。 但是,它肯定可以至少在本地进行优化——如果不是通过 LLVM,那么在 wasm 级别。

这会起作用,但是您需要确保调用table_free(elem)否则您将永远保留对它的引用。 当然,这对 C++ 来说并不奇怪。

这是一个奇怪的映射,因为我认为它不能很好地分层? 就像一个像 OpenGL 一样的库,但它依赖于编译器魔法来提供 - 我认为即使使用内联wasm,你也不能在 C++ 中构建anyref.h ,如果你依赖于声明一个单独的桌子。

无论如何,我认为这一切都是可行的/易于处理的,但并非直截了当。

@kripken确实,“适当的”本机anyref支持需要对 LLVM(以及 rustc)进行一些更改,但这实际上并不是障碍。

wasm-bindgen 将真正的 wasm anyref在一个 wasm 表中,然后在线性内存中存储一​​个整数索引到表中。 因此它可以使用 wasm table.get指令访问anyref

在实现 wasm-gc 之前,GC 语言将需要使用完全相同的策略,因此 Rust(等人)不会错过。

那么 LLVM 中的原生anyref支持会给我们带来什么? 好吧,它可以直接从函数传递/返回anyref ,而不需要通过 wasm 表间接传递anyref 。 这会很有用,是的,但这只是性能优化,它实际上并不能阻止使用anyref

@Pauan

wasm-bindgen 将真正的 wasm anyrefs 存储在一个 wasm 表中,然后在线性内存中存储一​​个整数索引到表中。 因此它可以使用 wasm table.get 指令访问 anyref。

没错,没错,这就是我所指的模型。

在实现 wasm-gc 之前,GC 语言将需要使用完全相同的策略,因此 Rust(等人)不会错过。

是的,现在 GC 语言没有优势,因为我们没有原生的 wasm GC。 但希望这会改变! :) 最终我希望 GC 语言在这里有明显的优势,至少如果我们正确地进行 GC。

那么 LLVM 中的原生 anyref 支持会给我们带来什么? 好吧,它可以直接从函数传递/返回 anyref,而不是需要通过 wasm 表间接传递 anyref。 那会很有用,是的,但这只是性能优化,实际上并不能阻止使用 anyref。

同意,是的,这只会是 GC 语言(最终)优于 C++ 和 Rust 等的性能优势。它不会阻止使用。

然而,循环对于 C++ 和 Rust 来说是一个更大的问题,因为表是根。 也许我们可以有一个跟踪 API 或“影子对象”,基本上是某种方式来映射 C++/Rust 内部 GC 链接的结构,以便外部 GC 可以理解它们。 但我认为目前还没有针对其中任何一项的实际提案。

最终我希望 GC 语言在这里有明显的优势,至少如果我们正确地进行 GC。

我可能是错的,但如果是这种情况,我会感到惊讶:GC 语言必须分配一个 wasm GC 结构,然后 wasm 引擎必须在它流经程序时对其进行跟踪。

相比之下,Rust 不需要分配(只分配给一个表),只需要存储一个整数,而 wasm 引擎只需要跟踪 1 个静态不动的表用于 GC 目的。

我想anyref访问可能可以针对 GC 语言进行优化,因为它不需要使用table.get ,但是我希望table.get会非常快。

那么你能解释一下你如何期望 wasm-gc 程序比使用 wasm 表的程序性能更好吗?

PS 这开始变得相当离题了,所以也许我们应该把这个讨论转移到一个新的话题上?

真的就是这样:避免table.get/table.set 。 使用 GC 你应该有原始指针,保存间接。 但是是的,您是对的,Rust 和 C++ 只需要存储一个整数,而且它们总体上非常快,因此任何 GC 优势最终都可能无关紧要!

我同意我们可能会跑题,是的。 我认为主题是@fgmccabe的观点,即 ref 类型在线性内存使用语言中并不自然地适合。 这可能会以某种方式影响我们的绑定(特别是循环令人担忧,因为 C++ 和 Rust 无法处理它们,但也许绑定可以忽略这一点?),所以我想只是需要小心 - 两者都试图使事情适用于尽可能多的语言,并且不受任何特定语言限制的过度影响。

@kentonv

Cap'n Proto 的序列化层实际上必须在每种语言中独立实现,因为它大部分是一个宽而浅的 API 层,需要惯用和内联友好。 RPC 层 OTOH 很窄但很深

是哪个文件夹?

@PoignardAzur对不起,我不明白你的问题。

@kentonv我正在浏览 capnproto Github 存储库。 序列化层在哪里?

@PoignardAzur所以,这又回到了我的观点。 真的没有一个地方可以指向并说“那是序列化层”。 大多数情况下,Cap'n Proto 的“序列化”只是围绕加载/存储到底层缓冲区的指针算法。 给定一个架构文件,您可以使用代码生成器生成一个头文件,该文件定义了对架构中定义的特定字段执行正确指针运算的内联方法。 应用程序代码需要在每次读取或写入任何字段时调用这些生成的访问器。

这就是为什么尝试调用用不同语言编写的实现是没有意义的。 为每个字段访问使用原始 FFI 将非常麻烦,因此毫无疑问,您最终会编写一个代码生成器,将 FFI 包装在更漂亮的东西中(并且特定于您的架构)。 但是生成的代码至少与 Cap'n Proto 已经实现的代码一样复杂——可能更复杂(而且速度要慢得多!)。 所以直接为目标语言编写代码生成器更有意义。

Cap'n Proto 实现中可能有一些可以共享的内部辅助函数。 具体来说, layout.c++ / layout.h包含解释 capnp 的指针编码、执行边界检查等的所有代码。生成的代码访问器在读取/写入指针字段时调用该代码。 所以我可以想象把那部分包装在一个 FFI 中,以便从多种语言中调用; 但我仍然希望用每种目标语言编写代码生成器和一定数量的运行时支持库。

是的,对不起,我的意思是相反的^^(RPC层)

@PoignardAzur Ohhh,我想您对查看接口特别感兴趣,因为您正在考虑如何将它们包装在 FFI 中。 那么你想要:

  • capability.h :用于表示能力和 RPC 调用的抽象接口,理论上可以由各种实现支持。 (这是最重要的部分。)
  • rpc.h :通过网络实现 RPC。
  • rpc-twoparty.h :通过简单连接的 RPC 传输适配器。

这个提议现在被#1291:OCAP 绑定所取代。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

ghost picture ghost  ·  7评论

beriberikix picture beriberikix  ·  7评论

frehberg picture frehberg  ·  6评论

bobOnGitHub picture bobOnGitHub  ·  6评论

JimmyVV picture JimmyVV  ·  4评论