Design: call_indirect 与抽象

创建于 2020-05-07  ·  36评论  ·  资料来源: WebAssembly/design

call_indirect对 WebAssembly 来说是一个非常有用的特性。 然而,指令的效率和良好行为隐含地依赖于 wasm 类型系统的简单性。 特别是,每个 wasm 值都有它所属的一种(静态)类型。 此属性方便地避免了类型化语言中非类型化函数调用的许多已知问题。 但是现在 wasm 已经超越了数字类型,它已经到了我们需要理解这些问题并牢记这些问题的地步。

call_indirect从根本上通过将调用者的预期签名与被调用者定义的签名进行比较来工作。 对于仅数字类型,WebAssembly 具有以下属性:当且仅当对相应funcref引用的函数的直接调用会进行类型检查时,这些签名才相等。 但有两个原因很快就会不成立:

  1. 使用子类型,只要实际函数的定义签名是预期签名的“子签名”,直接调用就可以工作,这意味着所有输入类型都是函数参数类型的子类型,所有输出类型都是函数的超类型结果类型。 这意味着间接调用的预期签名和函数定义的签名之间的相等性检查将陷入许多完全安全的情况,这对于支持大量使用子类型和间接调用的语言可能是有问题的(正如在讨论中提出的那样)推迟子类型)。 这也意味着,如果一个模块故意导出一个签名比函数定义时更弱的函数,那么call_indirect可用于访问具有其私有定义签名的函数,而不仅仅是其较弱的公共签名(一个刚刚发现的问题,因此尚未讨论)。
  2. 通过类型导入,模块可以在不导出类型定义的情况下导出类型,从而提供像 WASI 这样的系统计划严重依赖的抽象。 该抽象防止其他模块在编译时依赖其特定定义。 但是在运行时抽象导出类型只是简单地替换为其定义。 例如,这对于使call_indirect能够在其导出签名引用该导出类型的导出函数上正常工作非常重要。 但是,如果恶意模块知道该导出类型的定义是什么,他们可以使用call_indirect在导出类型与其预期秘密定义之间来回转换,因为call_indirect仅在运行时比较签名,当两种类型确实相同时。 因此,恶意模块可以使用call_indirect访问旨在由导出类型抽象的秘密,并且可以使用call_indirect来伪造导出类型的值,这些值可能违反未捕获的安全关键不变量类型本身的定义。

在上述两种情况下, call_indirect可用于绕过模块导出签名的抽象。 正如我提到的,到目前为止这还不是一个问题,因为 wasm 只有数字类型。 最初我认为,通过推迟子类型,所有关于call_indirect问题也被有效地推迟了。 但我最近意识到,通过删除子类型,“新”类型(在 https://github.com/WebAssembly/reference-types/pull/87 中名为externref )实际上是一个替代品对于抽象类型导入。 如果这就是人们希望的样子,那么不幸的是,我们需要考虑call_indirect和类型导入之间的上述交互。

现在有许多可能的方法来解决call_indirect的上述问题,但每种方法都有其权衡,而且设计空间太大而无法快速做出决定。 所以我不是建议我们现在就解决这个问题。 相反,目前要做出的决定是是否花时间正确解决externref 。 特别是,如果我们现在call_indirectfunc.ref为仅在关联签名完全是数字时进行类型检查,那么我们将服务于间接调用的所有 core-wasm 用例,并在同时为上述问题的所有潜在解决方案留出空间。 但是,我不知道这个限制是否实用,无论是在实施工作方面,还是在是否阻碍人们期待的externref的应用方面。 另一种方法是保留call_indirectfunc.ref原样。 这可能意味着,根据我们得出的解决方案, externref可能无法像真正的类型导入那样实例化,和/或externref可能(具有讽刺意味的是)不会能够拥有任何超类型(例如,如果我们最终决定添加anyref可能无法成为anyref的子类型)。

我,仅代表我自己,认为这两种选择都是可控的。 虽然我确实有偏好,但我并没有强烈推动决定采取一种方式或另一种方式,而且我相信你们都可以更好地获得必要的信息,以做出明智的决定。 我只是想让你们都知道有一个决定要做出,同时我想通过call_indirect建立对总体问题的认识。 如果您想对该问题进行比上述摘要更详尽的解释,请阅读以下内容。

call_indirect与抽象,详细

我将使用符号call_indirect[ti*->to*](func, args) ,其中[ti*] -> [to*]是函数的预期签名, func只是一个 funcref (而不是一个 funcref 表和一个索引),并且args是传递给函数的to*值。 同样,我将使用call($foo, args)直接调用函数,索引$foo传递参数args

现在假设$foo是具有声明输入类型ti*和输出类型to*的函数的索引。 您可能期望call_indirect[ti*->to*](ref.func($foo), args)等价于call($foo, args) 。 确实,现在就是这种情况。 但目前尚不清楚我们能否保持这种行为。

call_indirect和子类型

在讨论子类型时出现了一个潜在问题示例。 假设如下:

  • tsubtsuper的子类型
  • 模块实例 IA 导出定义为[] -> [tsub]类型的函数$fsub [] -> [tsub]
  • 模块 MB 导入一个函数$fsuper ,类型[] -> [tsuper]
  • 模块实例 IB 是用 IA 的$fsub实例化的模块 MB 为$fsuper (这是可行的——即使现在不可能,这个问题是关于潜在的即将出现的问题)

现在考虑如果 IB 执行call_indirect[ -> tsuper](ref.func($fsuper))会发生什么。 以下是看起来最合理的两种结果:

  1. 调用成功,因为预期的签名和定义的签名是兼容的。
  2. 因为两个签名不同,所以调用陷入困境。

如果我们要选择结果 1,请意识到我们可能需要采用以下两种技术之一来实现这一点:

  1. 对于导入的函数,将call_indirect与导入签名而不是定义签名进行比较。
  2. 对预期签名和定义签名的子类型兼容性进行至少线性时间运行时检查。

如果您更喜欢技术 1,请意识到一旦我们添加类型化函数引用(带有变体子类型),它就不会起作用。 也就是说, func.ref($fsub)将是一个ref ([] -> [tsub])也是一个ref ([] -> [tsuper]) ,但是技术 1 不足以防止call_indirect[ -> super](ref.func($fsub))被捕获。 这意味着结果 1 可能需要技术 2,这对性能有影响。

因此,让我们多考虑一下结果 2。 这里的实现技术是检查IB中call_indirect的预期签名是否等于IA中$fsub定义的签名。 起初,这种技术的主要缺点似乎是它会捕获许多可以安全执行的调用。 然而,另一个缺点是它可能会为 IA 引入安全漏洞。

为了看看如何,让我们稍微改变一下我们的例子并假设,虽然实例 IA 内部定义$fsub具有类型[] -> [tsub] ,但实例 IA 仅它的类型[] -> [tsuper] 。 使用结果 2 的技术,实例 IB 可以(恶意)执行call_indirect[ -> tsub]($fsuper)并且调用将成功。 也就是说,IB 可以使用call_indirect来规避 IA 对其函数签名所做的缩小。 充其量,这意味着 IB 依赖于 IA 的签名不能保证的方面。 在最坏的情况下,IB 可以使用它来访问 IA 可能有意隐藏的内部状态。

call_indirect和类型导入

现在让我们把子类型放在一边,考虑类型导入。 为方便起见,我将讨论类型导入,而不仅仅是引用类型导入,但这些细节无关紧要。 对于此处的运行示例,假设如下:

  • 模块实例IC定义类型capability和出口型,但不是它的定义$handle
  • 模块实例 IC 导出一个函数$do_stuff ,该函数定义为[capability] -> []类型,但导出为[$handle] -> []
  • 模块 MD 导入类型$extern和类型[$extern] -> []的函数$run [$extern] -> []
  • 模块实例 ID 是模块 MD 实例化的,IA 的导出$handle$extern ,IA 导出的$do_stuff$run

这个例子设置的是两个模块,其中一个模块在不知道或不被允许知道这些值是什么的情况下处理另一个模块的值。 例如,此模式是与 WASI 交互的计划基础。

现在让我们假设实例 ID 已设法获得$extern类型的值e $extern并执行call_indirect[$extern -> ](ref.func($run), e) 。 以下是看起来最合理的两种结果:

  1. 调用成功,因为预期的签名和定义的签名是兼容的。
  2. 因为两个签名不同,所以调用陷入困境。

结果 2 使得call_indirect对于导入类型几乎毫无用处。 因此,对于结果 1,意识到输入类型$extern不是$do_stuff的定义输入类型(而是capability ),因此我们可能需要使用其中之一弥补这一差距的两种技术:

  1. 对于导入的函数,将call_indirect与导入签名而不是定义签名进行比较。
  2. 认识到在运行时实例 ID 中的类型$extern表示capability

如果您更喜欢技术 1,请意识到一旦我们添加类型化函数引用它再次不起作用。 (根本原因与子类型相同,但需要更多的文字来说明这里的模拟。)

这给我们留下了技术 2。不幸的是,这再次带来了潜在的安全问题。 要了解原因,假设 ID 是恶意的,并且想要获取 IC 保密的$handle的内容。 进一步假设 ID 可以很好地猜测$handle真正代表什么,即capability 。 ID 可以定义类型[capability] -> [capability]的标识函数$id_capability [capability] -> [capability] 。 给出的值e类型$extern ,ID就可以执行call_indirect[$extern -> capability](ref.func($id_capability), e) 。 使用技术 2,此间接调用将成功,因为$extern在运行时表示capability ,而 ID 将返回e表示的原始capability 。 同样,给定capability类型的值c ,ID 可以执行call_indirect[capability -> $extern](ref.func($id_capability), c)c伪造$extern

结论

希望我已经明确表示call_indirect有许多重要的即将到来的性能、语义和/或安全/抽象问题——WebAssembly 到目前为止幸运地避免了这些问题。 不幸的是,由于call_indirect是核心 WebAssembly 的一部分,这些问题贯穿了许多正在进行的提案。 目前,我认为最好将重点放在最紧迫的此类提案上,即引用类型,我们需要决定是否将call_indirectfunc.ref为仅用于数字类型call_indirect的总体问题。

(抱歉,这篇长文章。我尽力解释跨模块编译时输入满足运行时输入功能的复杂交互,并尽可能简洁地展示这些交互的重要性。)

最有用的评论

另一方面,您已经证明了 castable anyref 可用于规避静态抽象机制。

在具有动态类型转换的语言中,静态类型抽象是不够的。 因为静态抽象依赖于参数化,而强制转换则打破了这一点。 这不是什么新鲜事,已经有论文讨论过。 在这样的上下文中需要其他抽象机制。

试图通过限制抽象类型的使用来解决这个问题违背了它们的目的。 考虑 WASI 用例。 WASI 模块及其导出的任何类型是由主机实现还是在 Wasm 中实现都无关紧要。 如果您任意限制用户定义的抽象类型,那么通常来说,Wasm 实现将不再可与主机实现互换。

  1. 它无助于使 call_indirect 尊重子类型(我认为您已经明确说过)

嗯? 它是子类型规则的一部分,根据定义也是如此。

  1. 它不会阻止 call_indirect 被用于使用带有其定义签名的导出函数而不是其导出签名。

我没说有。 我说这不是 call_indirect 本身的问题,而是为具有强制转换的语言选择合适的类型抽象机制的问题。

顺便说一句,编译 OCaml(或任何类似语言)需要引入变体类型没有令人信服的理由。 即使理论上这可能会稍微快一点(我怀疑在当前的引擎中会是这种情况,更有可能相反),变体类型是一个重大的并发症,对 MVP 来说是不必要的。 我不太同意你对过早复杂的胃口。 ;)

重新定义函数:有些语言(例如 Haskell 或 SML)不支持这种功能,因此可能会直接从 func refs 中受益。 OCaml 为结构相等而抛出,并显式具有物理实现定义的行为。 是否允许始终为函数返回 false 或 throwing 是开放的,但在实践中可能就足够了,并且在承诺昂贵的额外包装之前值得探索。

[作为元评论,如果你能淡化你的演讲,也许考虑到这个世界,也许有能力的人不是单身,而且以前偶尔会应用大脑的痕迹,我真的很感激。]

所有36条评论

谢谢你写的这么详细,罗斯! 我有一个小问题:在你写的“ call_indirect和类型导入”部分,

如果您更喜欢技术 1,请意识到一旦我们添加类型化函数引用它再次不起作用。

这是否也受到上一节中的警告,即只有在我们向类型化函数引用添加变体子类型时才会出现问题?

它不是。 子类型部分中的所有问题都与类型导入无关,类型导入部分中的所有问题都与子类型无关。 关于您询问的特定问题,请考虑导出函数可以将ref ([] -> [capability])类型的值作为ref ([] -> [$handle])类型的值返回,然后可以将其转换为funcref并间接调用到。 与导出的函数不同,值的这种透视变化发生在运行时而不是链接时,因此我们无法通过与导入签名进行比较来解决它,因为函数引用本身从未被导入。

module instance IC defines a type capability and exports the type but not its definition as $handle
这将如何运作? 需要有连接capability$handle以便 IC 知道如何处理它?
同样基于https://github.com/WebAssembly/proposal-type-imports/blob/master/proposals/type-imports/Overview.md#exports ,导入的类型是完全抽象的。 所以即使导出了$capability也是抽象的。 也许我误解了一些东西。

导出module instance IC exports a function $do_stuff that was defined with type [capability] -> [] but exported with type [$handle] -> []类似问题。

我可以想象用于此的某种子类型关系,例如,如果$capability <: $handle ,那么我们可以export $capability as $handle 。 但是在本节的开头提到将子类型放在一边,所以我把它放在一边......但我也想过更多:
如果: $capability <: $handle ,我们可以export $capability as $handle ,但export ([$capability] -> []) as ([$handle] -> [])应该“失败”,因为函数在参数中是逆变的。

使用类型导出,模块指定一个签名,如type $handle; func $do_stuff_export : [$handle] -> [] ,然后实例化签名,如type $handle := capability; func $do_stuff_export := $do_stuff 。 (完全忽略特定的语法。)然后类型检查器检查“假设$handle capability在这个模块中代表func $do_stuff_export := $do_stuff在这个模块中是否有效?”。 由于$do_stuff[capability] -> [] capability ,所以在用$handle后,它的签名与$do_stuff_export签名完全一致,所以检查成功。 (这里不涉及子类型,只是变量替换。)

但是请注意,签名本身并没有说明$handle 。 这意味着其他所有人都应该将$handle视为抽象类型。 也就是说,签名有意抽象了模块实现的细节,其他人都应该尊重这种抽象。 此问题的目的是说明call_indirect可用于规避该抽象。

希望这可以澄清问题!

谢谢,这澄清了事情。 我有一个关于子类型部分的问题(抱歉跳过):

我正在遵循我们希望 IB 执行call_indirect[ -> tsuper](ref.func($fsuper))成功的场景,通过让call_indirect “与导入签名而不是定义签名进行比较”。

你补充说(由于类型化的函数引用)我们还需要

  1. 对预期签名和定义签名的子类型兼容性进行至少线性时间运行时检查。

这应该是“预期签名和导入签名”之间的兼容性吗? 由于我们假设我们已经使 call_indirect 将导入签名与预期签名进行了比较。

如果检查了预期和导入之间的兼容性,那么稍后, call_indirect[ -> tsub]($fsuper)应该会失败。

技术 1 和技术 2 以两种正交方式给出,以使间接调用起作用。 不幸的是,技术 1 与类型化函数引用不兼容,技术 2 可能太昂贵了。 所以这些似乎都不可能奏效。 因此,本节的其余部分将考虑如果我们不使用这些并且只坚持在预期和定义的签名之间进行简单的相等比较会发生什么。 对困惑感到抱歉; 没有计划好的语义意味着我必须讨论三个潜在的语义。

注意不要妄下太多结论。 ;)

我的假设是call_indirect应该保持和今天一样快,因此只需要类型等效性测试,无论我们向语言添加多少子类型。 同时,运行时检查需要与静态类型系统保持一致,即必须尊重子类型关系。

现在,这些看似矛盾的要求实际上可以很容易地调和,只要我们确保可用于call_indirect类型始终位于子类型层次结构的叶子上。

一种既定的强制执行方法是将 _exact_ 类型的概念引入类型系统。 一个确切的类型没有子类型,只有超类型,我们有(exact T) <: T

有了这个,我们可以要求call_indirect处的目标类型是一个精确类型。 此外,函数本身的类型自然已经是该函数的确切类型。

一个模块也可能需要函数导入的确切类型,如果它想确保它只能用通过预期的运行时检查成功的函数来实例化。

这就是确保对规范化函数类型进行简单指针比较的当前实现技术仍然有效所需的全部内容。 它独立于有什么其他子类型,或者我们如何制作函数子类型。 (FWIW,我不久前与卢克讨论过这个问题,并计划创建一个 PR,但它被阻止了对子类型故事的未决更改,以及现在移动到哪个提案。)

(一个缺点是,将函数定义细化为子类型不再是向后兼容的更改,至少如果它的确切类型已在任何地方使用过,则至少不是。他们。)

一些旁白:

另一种方法是保留 call_indirect 和 func.ref 原样。

AFAICS,在涉及引用类型的函数上禁止 r​​ef.func 是不可行的。 这将严重削弱许多用例,即所有涉及在externref上运行的一流函数(回调、钩子等)。

这可能意味着,根据我们得到的解决方案,externref 可能不像真正的类型导入那样可实例化,和/或 externref 可能(具有讽刺意味的是)不能有任何超类型(例如可能没有如果我们最终决定添加 anyref,则可以成为 anyref 的子类型)。

你能详细说明一下吗? 我看不到联系。

注意不要妄下太多结论。 ;)

我不确定你指的是什么结论。 我陈述的结论是, call_indirect存在许多我们需要注意并应该开始计划的问题。 您似乎在暗示这些问题无关紧要,因为您有一个解决方案。 但该解决方案尚未得到 CG 的审查或接受,我们不应在此之前对其进行计划。 我特别要求不要讨论解决方案,因为评估和比较它们需要一段时间,而且在我们有时间正确进行这些评估和比较之前,我们需要做出决定。 但是,为了防止人们形成此问题已解决的看法,从而避免做出紧迫的决定,我将花一点时间快速讨论您的解决方案。

强制执行的一种既定方法是将精确类型的概念引入类型系统。

精确类型几乎不是一个既定的解决方案。 如果有的话,确切的类型已经确定了其支持者仍在努力解决的问题。 有趣的是,这里有一个线程,其中 TypeScript 团队最初看到您提出的表单的确切类型可以解决一些问题,但后来他们最终 [意识到](https://github.com/microsoft/TypeScript/issues/12936 #issuecomment-284590083)确切类型引入的问题多于它们解决的问题。 (上下文注意:该讨论是由 Flow 的精确对象类型引发的,它实际上不是精确类型的一种形式(理论上),而是简单地禁止前缀子类型的对象模拟。)我可以想象我们重播那个线程这里。

作为 WebAssembly 会如何处理这类问题的一个例子,假设我们没有延迟子类型化。 ref.null将是exact nullref使用精确类型。 但是exact nullref不会是exact anyref的子类型。 事实上,根据精确类型的通常语义,可能没有值属于exact anyref因为可能没有值的运行时类型恰好是anyref 。 这将使call_indirect完全无法用于anyref s。

现在,您可能已经想到了一些不同版本的精确类型,但是需要一段时间来检查这个不同的版本是否以某种方式解决了精确类型的许多未解决的问题。 所以在这里我的观点是不要扔掉这个解决办法,但要认识到不是很明显,这解决方案,并不会与这种预期决定。

你能详细说明一下吗? 我看不到联系。

你引用了一个很长的句子。 您希望我详细说明其中的哪一部分? 一种猜测是您可能会遗漏call_indirect和类型导入的整体问题。 您的确切类型建议仅解决了子类型化的问题,但我们在上面确定call_indirect即使没有任何子类型化也存在问题。

这将严重削弱许多用例,即所有涉及在externref上运行的一流函数(回调、钩子等)。

是的,所以这是我希望获得更多信息的事情。 我的理解是call_indirect的主要用例是支持 C/C++ 函数指针和 C++ 虚拟方法。 我的理解也是这个用例目前仅限于数字签名。 我知道call_indirect的更多潜在用途,但正如我提到的,我建议进行临时限制,所以重要的是call_indirect当前用途是什么。 鉴于call_indirect仍然需要一个表和索引而不是简单的funcref ,它似乎不是特别适合支持回调。 我不知道那是不是因为目前它没有被用于这个目的。

你们都比我更了解针对此功能的代码库,所以如果你们现在都知道一些需要此功能的真实程序,那么在这里提供一些需要的使用模式示例会非常有帮助。 除了有助于确定我们现在是否需要支持此功能之外,如果现在需要该功能,那么这些示例将有助于告知在解决上述问题的同时如何最好地快速提供它。

@罗斯泰特

如果有的话,确切的类型已经确定了其支持者仍在努力解决的问题。 有趣的是,这里有一个线程,TypeScript 团队最初看到您提出的表单的精确类型如何解决一些问题,但后来他们最终意识到精确类型引入的问题比他们解决的问题多。 (上下文注意:该讨论是由 Flow 的精确对象类型引发的,它们实际上不是一种精确类型的形式(理论上),而是简单地禁止前缀子类型的对象模拟。)我可以想象我们重播那个线程这里。

括号是这里的关键。 我不确定他们在那个线程中到底有什么想法,但这似乎不是一回事。 否则,诸如“假定类型T & U始终可分配给T ,但如果T是精确类型则此操作失败”之类的语句将毫无意义(这不会t 失败,因为T & U将无效或底部)。 其他问题主要是关于语用学的,即程序员想在哪里使用它们(用于对象),这在我们的例子中不适用。

对于低级类型系统,即使在您自己的一些论文中,精确类型难道不是一个关键因素吗?

作为 WebAssembly 会如何处理这类问题的一个例子,假设我们没有延迟子类型化。 ref.null 的类型将是使用精确类型的精确 nullref。 但是精确的 nullref 不会是精确的 anyref 的子类型。

这里没有分歧。 没有子类型是精确类型的目的。

事实上,根据精确类型的通常语义,可能没有值属于精确 anyref,因为可能没有值的运行时类型恰好是 anyref。

是的,组合(exact anyref)不是一个有用的类型,因为 anyref 的唯一目的是成为一个超类型。 但为什么这是一个问题?

这将使 call_indirect 完全无法用于 anyrefs。

你确定你现在没有混淆级别吗? 函数类型(exact (func ... -> anyref))非常有用。 它只是与类型不兼容,例如(func ... -> (ref $T)) 。 也就是说, exact可以防止对函数类型进行非平凡的子类型化。 但这就是重点!

也许您将(exact (func ... -> anyref))(func ... -> exact anyref) ? 这些是不相关的类型。

您的确切类型建议仅解决了子类型化的问题,但我们在上面确定了 call_indirect 即使没有任何子类型化也存在问题。

您以某种方式假设您将能够导出没有定义的类型作为定义抽象数据类型的手段。 显然,这种方法在存在动态类型转换(call_indirect 或其他)的情况下不起作用。 这就是为什么我一直说我们需要 newtype 风格的类型抽象,而不是 ML 风格的类型抽象。

我的理解是 call_indirect 的主要用例是支持 C/C++ 函数指针

是的,但这不是我所指的ref.func的唯一用例,因为您将其包含在建议的限制中(可能是不必要的?)。 特别是会有call_ref ,它不涉及类型检查。

您以某种方式假设您将能够导出没有定义的类型作为定义抽象数据类型的手段。 显然,这种方法在存在动态类型转换(call_indirect 或其他)的情况下不起作用。 这就是为什么我一直说我们需要 newtype 风格的类型抽象,而不是 ML 风格的类型抽象。

好的,所以您似乎同意确切类型对于解决call_indirect和类型导入的问题没有任何作用。 但是你也说解决这个问题没有意义,因为无论如何它都会因为运行时强制转换而成为一个问题。 有一个简单的方法可以防止这个问题:不允许人们对抽象类型执行运行时转换(除非抽象类型明确说明它是可转换的)。 毕竟,它是一种不透明类型,因此我们不能假设它具有执行强制转换所需的结构。 因此,即使确切类型有可能解决子类型问题,现在忽略问题的另一半还为时过早。

正如我所说,每个解决方案都有权衡。 您似乎假设您的解决方案只有您自己确定的权衡,并且您似乎假设 CG 更喜欢您的解决方案而不是其他解决方案。 我也有这个问题的潜在解决方案。 它保证了恒定时间检查,基于已经在虚拟机中使用的技术,解决了这里的所有问题(我相信),不需要添加任何新类型,并且实际上为 WebAssembly 添加了已知应用程序的附加功能。 但是,我并不假设它按我的预期工作,并且我没有忽略一些缺点,因为您和其他人没有机会查看它。 我也不是假设 CG 会更喜欢它的权衡而不是替代选项。 相反,我试图弄清楚我们可以做些什么来给我们时间来分析选项,以便 CG,而不仅仅是我,可以就这个跨领域的主题做出明智的决定。

特别是会有call_ref ,它不涉及类型检查。

你句子中的关键词是will 。 我完全知道有应用call_indirect与非数字类型,人们希望能够获得支持。 我希望我们能够提出支持该功能并解决上述问题的设计。 但是,正如我所说,理想情况下,我们可以有一些时间来开发该设计,这样我们就不会在有机会调查这些影响之前迅速发布具有交叉影响的功能。 所以我的问题是,是否有重大的程序,需要该功能。 如果有,就没有必要假设; 只需指出一些并说明他们目前如何依赖此功能。

您以某种方式假设您将能够导出没有定义的类型作为定义抽象数据类型的手段。 显然,这种方法在存在动态类型转换(call_indirect 或其他)的情况下不起作用。 这就是为什么我一直说我们需要 newtype 风格的类型抽象,而不是 ML 风格的类型抽象。

这对我来说似乎是一个基本问题。 启用导出类型定义的机密性是类型导入提案的目标吗? 我从这个帖子中了解到@rossberg认为它目前不是一个目标。 在讨论解决方案之前,让我们先讨论并就这个问题达成一致,这样我们就可以根据相同的假设进行工作。

@罗斯泰特

好的,所以您似乎同意确切类型对于解决 call_indirect 和类型导入的问题没有任何作用。

是的,如果您的意思是如何添加用于定义抽象数据类型的功能的问题。 有多种方法可以使类型抽象始终如一地工作,但这样的功能还需要进一步研究。

你句子中的关键词是will。 我完全意识到人们希望支持具有非数字类型的 call_indirect 应用程序。

call_ref指令在函数 ref 提议中,因此相当接近,无论如何都在任何潜在的抽象数据类型机制之前。 你是建议我们把它搁置到那个时候吗?

@tlively

启用导出类型定义的机密性是类型导入提案的目标吗? 我从这个帖子中了解到@rossberg认为它目前不是一个目标。

这是一个目标,但抽象数据类型机制是一个单独的功能。 并且必须设计这样一种机制,使其不影响进口的设计。 如果是这样,那么我们就大错特错了——必须在定义站点而不是使用站点确保抽象。 幸运的是,这不是火箭科学,设计空间已经得到了很好的展示。

谢谢, @rossberg ,这是有道理的。 在类型导入和导出之后的后续提案中添加抽象原语对我来说听起来不错,但如果我们能写下我们计划如何尽快在某处做这件事的细节,那就太好了。 类型导入和导出的设计约束并通知抽象类型导入和导出的设计,因此在我们最终确定初始设计之前,我们对抽象将如何工作有一个很好的了解是很重要的。

除了详细说明该计划之外,由于call_indirect这个问题表明它会影响紧迫的决定,您能否解释为什么您似乎拒绝我的建议,即抽象类型不应可强制转换(除非明确限制为可强制转换) )? 它们是不透明的,因此该建议似乎符合抽象类型的常见做法。

@tlively ,是的,同意。 加上我打算写一段时间的其他各种事情。 一旦我解决了#69 的所有后果,就会这样做。 ;)

@RossTate ,因为这会使抽象数据类型与强制转换不兼容。 仅仅因为我想阻止其他人_通过_一个抽象类型,我不一定要阻止他们(或我自己)_to_一个抽象类型。 创建这样一个错误的二分法会破坏演员表的核心用例。 例如,我当然希望能够将抽象类型的值传递给多态函数。

@rossberg你能澄清一下你想到的这个核心用例是什么吗? 我对解释你的例子的最佳猜测是微不足道的,但也许你的意思是别的。

@RossTate ,考虑多态函数。 缺少 Wasm 泛型,当使用来自 anyref 的向上/向下强制转换编译它们时,应该可以将它们与任何其他抽象类型的值一起使用,而无需额外包装到另一个对象中。 您通常希望能够像对待任何其他类型一样对待抽象类型的值。

好的,让我们考虑多态函数,假设导入的类型是Handle

  1. Java 具有多态函数。 它的多态函数期望所有(Java 引用)值都是对象。 特别是,他们必须有一个 v 表。 使用 Handle 的 Java 模块可能会指定一个可能实现接口的 Java 类CHandle 。 这个类的实例将有一个Handle类型的(wasm 级)成员和一个 v-table,它提供指向各种类和接口方法实现的函数指针。 当给一个表面级别的多态函数时,它在 wasm 级别只是对象上的一个函数,模块可以使用它用于转换到其他类的相同机制转换为CHandle
  2. OCaml 具有多态函数。 它的多态函数期望所有 OCaml 值都支持物理相等。 因为 wasm 无法推理 OCaml 的类型安全,它的多态函数也可能需要大量使用强制转换。 专门的铸造结构可能会使这更有效。 由于这些原因中的任何一个,OCaml 模块可能会指定符合这些规范的代数数据类型或记录类型THandle并且具有类型Handle的(wasm 级别)成员。 然后,它的多态函数会将 OCaml 值转换为THandle ,就像转换为任何其他代数数据类型或记录类型一样。

换句话说,因为模块依赖关于如何表示表面级值的规范来实现多态函数之类的东西,而像 Handle 这样的抽象导入类型不满足这些规范,所以值的包装是不可避免的。 这与anyref的原始应用程序之一被接口类型取代的原因相同。 我们开发了案例研究,证明anyref对于支持多态函数来说不是必需的,甚至不是很适合。

另一方面,您已经证明了 castable anyref可用于规避静态抽象机制。 你提到的抽象机制计划是试图通过动态抽象机制来修补这个问题。 但是动态抽象机制存在许多问题。 例如,不能将您的i31ref类型导出为抽象的Handle类型,而没有其他模块使用anyref并强制转换以伪造句柄(例如,功能)的风险。 相反,如果我们只是确保标准的静态抽象,则必须跳过额外的圈套和开销,这将是不必要的。

此外,现在(我认为)我更好地理解您打算如何使用精确类型,我意识到您的意图没有解决我通过call_indirect和子类型引起注意的两个主要问题:

  1. 它无助于使call_indirect尊重子类型(我认为您已经明确说过)
  2. 它不会阻止call_indirect被用于使用带有其定义签名的导出函数而不是其导出签名。

所以这不是一个要解决的小问题。 这就是为什么,鉴于时间限制,我更愿意专注于评估如何给我们时间正确解决它。 我认为没有必要首先讨论anyref是否值得放弃静态抽象。 这是我希望避免的那种大讨论,以免进一步拖延事情。

另一方面,您已经证明了 castable anyref 可用于规避静态抽象机制。

在具有动态类型转换的语言中,静态类型抽象是不够的。 因为静态抽象依赖于参数化,而强制转换则打破了这一点。 这不是什么新鲜事,已经有论文讨论过。 在这样的上下文中需要其他抽象机制。

试图通过限制抽象类型的使用来解决这个问题违背了它们的目的。 考虑 WASI 用例。 WASI 模块及其导出的任何类型是由主机实现还是在 Wasm 中实现都无关紧要。 如果您任意限制用户定义的抽象类型,那么通常来说,Wasm 实现将不再可与主机实现互换。

  1. 它无助于使 call_indirect 尊重子类型(我认为您已经明确说过)

嗯? 它是子类型规则的一部分,根据定义也是如此。

  1. 它不会阻止 call_indirect 被用于使用带有其定义签名的导出函数而不是其导出签名。

我没说有。 我说这不是 call_indirect 本身的问题,而是为具有强制转换的语言选择合适的类型抽象机制的问题。

顺便说一句,编译 OCaml(或任何类似语言)需要引入变体类型没有令人信服的理由。 即使理论上这可能会稍微快一点(我怀疑在当前的引擎中会是这种情况,更有可能相反),变体类型是一个重大的并发症,对 MVP 来说是不必要的。 我不太同意你对过早复杂的胃口。 ;)

重新定义函数:有些语言(例如 Haskell 或 SML)不支持这种功能,因此可能会直接从 func refs 中受益。 OCaml 为结构相等而抛出,并显式具有物理实现定义的行为。 是否允许始终为函数返回 false 或 throwing 是开放的,但在实践中可能就足够了,并且在承诺昂贵的额外包装之前值得探索。

[作为元评论,如果你能淡化你的演讲,也许考虑到这个世界,也许有能力的人不是单身,而且以前偶尔会应用大脑的痕迹,我真的很感激。]

作为元评论,如果你能减少你的演讲,我真的很感激

听到。

或许考虑过这样一个想法,即在这个世界中,有能力的人可能不是单身人士,而且以前偶尔会应用大脑的痕迹。

我在这里的建议是基于与多位专家的协商。

在具有动态类型转换的语言中,静态类型抽象是不够的。 因为静态抽象依赖于参数化,而强制转换则打破了这一点。 这不是什么新鲜事,已经有论文讨论过。 在这样的上下文中需要其他抽象机制。

我咨询过的这些专家包括上述一些论文的作者。

现在,为了检查我是否正确地综合了他们的建议,我刚刚给其中一些论文的另一位作者发了电子邮件,我以前从未讨论过这个话题。 这是我问的:

假设我有一个多态函数 f(……)。 我的类型语言具有(包含)子类型和显式转换。 但是,从 t1 到 t2 的强制转换仅检查 t2 是否是 t1 的子类型。 假设默认情况下像 X 这样的类型变量没有子类型或超类型(当然除了它们自己)。 你会期望 f 是关于 X 的关系参数吗?

这是他们的回应:

是的,我认为这将是参数化的,因为它为您提供的唯一能力是在 X 上编写相当于恒等函数的强制转换,该函数已经是关系参数化的。

这符合我的建议。 现在,当然,这是手头问题的简化,但我们已经努力更具体地研究 WebAssembly 的问题,到目前为止,我们的探索表明,即使在 WebAssembly 的规模上,这种期望仍然存在除了call_indirect ,因此是这个问题。

请注意,您所指的定理适用于所有值都是可转换的语言。 这个观察结果是我们想到限制可铸性的地方。

考虑 WASI 用例。

我不明白你提出的要求。 我们已经考虑了 WASI 用例。 通过我们,我包括了多位安全专家,甚至特别是基于能力的安全专家。

作为元评论,我真的很感激不需要诉诸权威或 CG 来听取我的建议。 我建议限制强制转换即使在存在强制转换的情况下也能确保静态参数化。 你立即无视了这个建议,诉诸之前的文件来证明解雇是合理的。 然而,当我向那些论文的作者提出同样的建议时,他们立即得出了与我所做的和你可能得到的相同的结论。 在此之前,我建议评估潜在的解决方案将是一个漫长的过程。 你不理会这个建议,坚持说你(全靠你自己)已经解决了这个问题,把我们俩拉进了这场漫长的谈话。 当一个人的建议反复被如此随意地驳回时,要取得进展并避免感到沮丧是极其困难的。 (我应该澄清一下,我并不是试图将您的建议视为可能的解决方案;我试图证明它不是唯一的解决方案,因此应该与其他各种解决方案一起进行评估。)

我认为确定并审查详细设计以解决此问题中提出的问题是重要且及时的:我实际上并不认为抽象类型应该被视为一个更远的特性; WASI 现在需要它们。

我也希望exact + newtype可以解决这些问题,但我同意我们不能通过过早地承诺设计而在这个时间点简单地将农场押在这个预感上我们(很快)发布引用类型。 我们需要时间对此进行适当的讨论。

话虽如此,我没有看到在引用类型提案中允许在call_indirect签名中使用externref的危险。 是的,如果模块导出externref值(作为 const 全局变量或从函数返回它...),我们还没有确定是否可以向下转换externref 。 但是call_indirect并没有贬低externref ; 它正在向下转换funcref ,并且externref的作用与i32在 funcref-type-equality 检查中没有什么不同。 因此,在call_indirect中没有类型导入、类型导出和子类型的情况下,我看不出我们如何承诺我们尚未在 MVP 中承诺的新设计选择.

如果没有危险,也许我们可以将这种激烈的讨论降低到类型导入提案中的不那么激烈的讨论中(我仍然认为我们应该包括适当的抽象类型支持)?

当然。 我认为检查是否存在危险是个好主意。

关于 WASI,该设计仍在不断变化,但似乎仍然可行的一种选择是使用i31ref作为其“句柄”,例如因为它不需要动态内存分配。 WASI 可能会决定其他选项,但目前没有人知道这一点,现在做出的决定不影响这些决定会很好。

目前, externref是唯一可用的抽象类型,因此基于 WASI 的主机将使用i31ref (或任何 WASI“句柄”)实例化externref 。 但我的理解是,WASI 想尽可能地将其实现移到 WebAssembly 中,以减少依赖于主机的代码。 为了促进这一点,在某些时候 WASI 系统可能希望像对待任何其他类型导入一样对待externref ,并使用 WASI 的抽象导出Handle类型实例化它。 但是如果Handlei31ref ,那么上面的call_indirect需要让它跨模块边界工作,也可以用来让人们通过externref伪造句柄

所以我的问题之一,现在我注意到在我的原始帖子中没有明确说明,人们是否希望externref像其他抽象类型导入一样可实例化?

所以我的问题之一,现在我注意到在我的原始帖子中没有明确说明,人们是否希望externref像其他抽象类型导入一样可实例化?

感谢您明确提出这个问题。 FWIW,我从来没有理解externref可以从 WebAssembly 模块内部实例化。 如果 WASI 想要使用externref作为句柄,这意味着主机参与虚拟化,但这对我来说似乎没问题,或者至少看起来是一个可分离的讨论。

嗯,让我看看我是否可以澄清。 我怀疑您已经接受了以下一系列内容,但对我来说从头开始更容易。

从 wasm 模块的角度来看, externref并不意味着主机引用。 它只是一个模块一无所知的不透明类型。 相反,它是围绕externref的约定将其解释为主机引用。 例如,模块使用externref与 DOM 交互的约定在模块导入的涉及externref的函数中很明显,例如parentNode : [externref] -> [externref]childNode : [externref, i32] -> [externref] 。 模块的环境,例如宿主本身,实际上将externref为宿主引用,并且它提供了证实该解释的导入方法的实现。

然而,该模块的环境不必须是主机和externref不必是主机引用。 环境可以是另一个模块,它为某些类型提供功能,这些类型看起来像展示预期约定的主机引用。 假设模块 E 是模块 M 的环境,并且模块 M 如上所述导入parentNodechildNode 。 假设 E 想要使用模块 M 但想要限制 M 对 DOM 的访问,比如因为 E 对 M 的信任有限,或者因为 E 想要限制 M 可能存在的任何错误,并且知道 M 的需求不应超过这些限制。 E 可以做的是使用“MonitoredRef”将 M 实例化为 M 的externref 。 假设,特别是,E 想要给 M 个 DOM 节点,但要确保 M 不会沿着 DOM 树向上走。 那么 E 的 MonitoredRef 可以具体ref (struct externref externref) ,其中第二个externref (从 E 的角度来看)是 M 正在操作的 DOM 节点,但第一个externref是其祖先那个不允许 M 走过去的节点。 然后 E 可以实例化 M 的parentNode ,这样如果这两个引用相同,它就会出错。 E 本身会导入它自己的parentNodechildNode函数,使 E 有效地成为 DOM 交互的运行时监视器。

希望这足够具体,可以描绘出正确的画面,同时又不会太具体而无法在细节中迷失。 显然有很多这样的模式。 所以我想用另一种方式来表达这个问题是,我们是否希望externref只代表完全主机引用?

对我来说,唯一听起来有问题的部分是“E 可以做的是使用“MonitoredRef”将 M 实例化为 M 的externref 。 我并不认为有计划允许抽象事物在其他模块中显示为externref 。 我的理解是externref根本不是抽象工具。

我也不知道有任何这样的计划; 我也不知道有没有人考虑过这个选项。 也就是说, externref应该是“原始”类型,例如i32还是“可实例化”类型,例如导入类型?

在我原来的帖子中,我指出任何一种方式都是可以管理的。 采用“原始”解释的权衡是externref比导入类型的有用/可组合性要低得多,因为后者将支持externref的用例以及上述模式。 因此,“原始” externref似乎可能会成为残留物——仅存在于向后兼容。 但这似乎不太可能是特别有问题的,只是令人讨厌。 我能看到的最大问题是,就像call_indirect对数字类型的良好行为因为它们没有超类型而起作用一样, call_indirect可能最终取决于externref也没有超类型。

啊哈,是的,这解释了理解上的差异:我同意@tlively 的观点,即externref根本不是抽象的,并且没有“用类型实例化externref ”的概念,而且我认为我们可以对此感到非常有信心。 (由于externref是一种原始类型,而不是显式声明的类型参数,因此不清楚如何尝试在每个模块的基础上实例化它。)

在没有向下转型的情况下,这一事实使得 wasm 几乎无法实现/虚拟化 WASI API,这就是为什么 WASI 的计划是从i32句柄直接过渡到类型导入(以及为什么我提交类型导入/#6 , b/c 我们还需要一点)。

由于externref是一种原始类型,而不是显式声明的类型参数,因此不清楚如何尝试在每个模块的基础上实例化它。

当我们添加类型导入时,我们可以将没有类型导入但带有externref视为在顶部具有import type externref 。 一切都将进行类型检查,因为与其他原始类型不同, externref没有关联的原始操作(除了具有默认值)。 但是通过隐式导入,现在我们可以执行诸如虚拟化、沙盒和运行时监控之类的事情。

但在来回讨论之前,我认为衡量我们是否都在同一页面上对某事有所帮助。 如果您同意或不同意以下陈述以及原因,请告诉我:“一旦类型导入可用,模块就没有理由使用externref并且如果它们使用类型导入,则更具有可重用性/可组合性。”

如果您同意或不同意以下陈述以及原因,请告诉我:“一旦类型导入可用,模块就没有理由使用externref并且如果它们使用类型导入,则更具有可重用性/可组合性。”

我同意摘要中的这一说法。 实际上,我认为 externref 在引用外部 JS 对象的 Web 上下文中仍然很常见,因为它在实例化时不需要额外的配置。 但这只是一个预测,我不介意我是否错了,毕竟每个人都转而使用类型导入。 externref 的价值在于我们可以更快地拥有它,而不是拥有更丰富的机制,例如类型导入。 我宁愿让 externref 保持简单并看到它不再使用,也不愿在有更优雅的替代品时笨拙地将其硬塞为更强大的东西。

@tlively ,

FWIW,我从来没有理解 externref 可以从 WebAssembly 模块内部实例化。

是的,这个想法是 externref 是外部指针的“原始”类型。 要抽象引用类型的实现细节,您需要其他东西:诸如 anyref 或类型导入之类的东西。

@lukewagner ,如果可以的

@罗斯泰特

我咨询过的这些专家包括上述一些论文的作者。

优秀。 然后我假设您已经注意到您本人确实是其中几篇论文的作者,以防您正在寻找更多权威。 :)

这是我问的:

假设我有一个多态函数 f(...)。 我的类型语言具有(包含)子类型和显式转换。 但是,从 t1 到 t2 的强制转换仅检查 t2 是否是 t1 的子类型。 假设默认情况下像 X 这样的类型变量没有子类型或超类型(当然除了它们自己)。 你会期望 f 是关于 X 的关系参数吗?

叹。 对于那个具体问题,我会给出相同的答复。 但是这个问题体现了几个特定的​​假设,例如关于强制转换的性质,以及关于有界和无界量化之间相当不寻常的区别,这种区别在任何编程语言中都很少存在。 我想这是有原因的。

当我说“静态类型抽象是不够的”时,我并不是说它_技术上_不可能(当然是),而是_实际上_不够。 在实践中,您不希望在类型抽象和子类型/可转换性之间(或在参数和非参数类型之间)分叉,因为这会人为地破坏基于转换的组合。

我不明白你提出的要求。

如果您收到一个抽象类型的值,那么您可能仍然想忘记它的确切类型,例如将它放入某种联合中,然后通过向下转换恢复它。 您可能希望这样做的原因与您可能希望用于任何其他引用类型的原因相同。 类型抽象不应该妨碍某些对相同类型的常规类型有效的使用模式。

您的答案似乎是:那又怎样,将所有内容包装成各自使用站点的辅助类型,例如变体。 但这可能意味着大量的包装/展开开销,它需要更复杂的类型系统功能,并且使用起来更复杂。

我认为这就是我们的一些分歧归结为:MVP 是否应该支持引用类型的联合,或者是否应该要求引入和编码显式变体类型。 不管是好是坏,联合是典型引擎堆接口的自然匹配,它们在今天很容易且便宜地支持。 变体不是那么多,它们是一种更具研究性的方法,可能会导致额外的开销和更难预测的性能,至少在现有引擎中是这样。 我是说,作为一个类型系统的人,在其他情况下更喜欢变体而不是联合,比如面向用户的语言。 ;)

作为元评论,我真的很感激不需要诉诸权威或 CG 来听取我的建议。

我是否可以建议,如果在_假设_缺乏基于这些假设做出回答并做出广泛的断言和建议?

优秀。 然后我假设您已经注意到您本人确实是其中几篇论文的作者,以防您正在寻找更多权威。 :)

是的,即使您知道我的建议专门针对提出这些声明的条件,但当您建议有论文声称我的建议不起作用时,这会带来极大的问题。

在实践中,您不希望在类型抽象和子类型/可转换性之间(或在参数和非参数类型之间)分叉,因为这会人为地破坏基于转换的组合。

这是一个意见,而不是一个事实(让我们完全有理由不同意)。 我想说的是,对于多语言系统,没有与语言无关的行业类型汇编语言,因此不可能对实践提出要求。 这是值得彻底(单独)讨论的事情。 对于该讨论,首先提供一些详细的案例研究会对您有所帮助,以便 CG 可以比较权衡。

我是否可以建议,如果先向各自的拥护者询问不清楚的事情,例如具体的理由或未来计划(并不总是很明显或尚未写出来),然后再假设没有基于这些假设做出回答并做出广泛的断言和建议?

WebAssembly/proposal-type-imports#4、WebAssembly/proposal-type-imports#6 和 WebAssembly/proposal-type-imports#7 基本上都要求提供有关此计划的更多细节。 最后一个将问题推给了 GC,但 WebAssembly/gc#86 指出当前的 GC 提案实际上并不支持动态抽象机制。

在元层面上,我们被要求把这个讨论放在一边,专注于手头的话题。 我发现@tlively对我的问题的回答非常有帮助。 我实际上很想了解您对这个问题的具体想法。

@罗斯泰特

我发现@tlively对我的问题的回答非常有帮助。 我实际上很想了解您对这个问题的具体想法。

嗯,我以为我已经在上面评论过了。 或者你的意思是别的?

不。 我认为该评论可能暗示同意他的回应,但我想先确认一下。 谢谢!

@lukewagner ,你有什么想法?

我同意上面的externref将永远是原始类型,而不是追溯重新解释为类型参数。 我认为,鉴于此,引用类型是很好的。

我想接受@rossberg的提议,以扩大类型导入提案的范围,使其涵盖 wasm 实现抽象类型的能力。 一旦我们确定了这一点,我认为它会解开关于函数引用和子类型的进一步讨论。

惊人的。 然后我们都在同一个页面上(我也认为@tlively提供了一个很好的总结,介绍了所涉及的权衡以及做出决定的理由)。

因此externref将无法实例化,并且在该功能发布时,需要类型导入的额外灵活性的模块将有望转换为类型导入。 我突然想到,为了使过渡顺利,如果没有提供实例化,我们可能需要使(某些?)类型导入默认由externref实例化。

我也想接受扩大类型导入范围的提议。 类型导入的许多主要应用程序都需要抽象,所以在我看来,抽象成为该提案的一部分是很自然的。

在此期间,虽然我们已经解决有关紧迫的问题externref ,该怎么做更普遍约call_indirect仍然没有得到解决,尽管有一些有用的讨论如何加以解决,所以我仍将问题悬而未决。

谢谢!

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