WebAssembly JSPI 有一个新的 API
WebAssembly 的 JavaScript Promise 集成 (JSPI) API 有一个新 API,可在 Chrome M126 版本中使用。我们讨论了变化的内容、如何配合 Emscripten 使用以及 JSPI 的路线图。
JSPI 是一个允许使用 顺序 API 的 WebAssembly 应用程序访问 异步 Web API 的接口。许多 Web API 是通过 JavaScript Promise
对象设计的:它们不会立即执行请求的操作,而是返回一个 Promise
来完成这些操作。而另一方面,许多编译为 WebAssembly 的应用程序源自 C/C++ 领域,那里主要由阻塞调用者直到任务完成的 API 主导。
JSPI 钩入 Web 架构,允许在返回 Promise
时暂停 WebAssembly 应用程序,并在 Promise
被解决时恢复。
您可以在这篇博客文章和规范中了解有关 JSPI 和如何使用它的更多信息。
有哪些新的变化?
Suspender
对象的终结
2024 年 1 月,Wasm CG 的 Stacks 子组投票修改 JSPI API 的设计。具体来说,将不再使用显式的 Suspender
对象,而是使用 JavaScript/WebAssembly 边界作为确定暂停计算的分隔符。
改变虽然很小但可能意义重大:当计算需要暂停时,最近一次对一个封装的 WebAssembly 导出的调用将决定暂停的“切入点”。
这意味着使用 JSPI 的开发者对切入点的控制稍微减少了。而另一方面,不需要显式管理 Suspender
对象使整个 API 的使用显著变得更加简单。
不再需要 WebAssembly.Function
另一个变化是 API 的风格。API 不再通过 WebAssembly.Function
构造器描述 JSPI 的封装,而是提供特定的函数和构造器。
这有以下几个好处:
- 它去除了对类型反射提议的依赖。
- 它简化了 JSPI 的工具:新的 API 函数不需要显式引用函数的 WebAssembly 类型。
这一变化的实现得益于不再使用显式引用的 Suspender
对象。
不暂停直接返回
第三个变化涉及暂停调用的行为。现在调用 JavaScript 函数的暂停导入时,只在 JavaScript 函数实际返回一个 Promise
时暂停。
这一变化虽然表面上似乎违反了W3C TAG 的建议,但对于 JSPI 用户来说是安全的优化。它之所以安全,是因为 JSPI 实际上担任了调用返回 Promise
函数的调用方角色。
这一变化对大多数应用程序影响甚微,但对于某些应用程序,通过避免不必要的浏览器事件循环,可以显著受益。
新 API
此 API 非常简单:有一个函数可以接收从 WebAssembly 模块导出的函数,并将其转换为返回 Promise
的函数:
Function Webassembly.promising(Function wsFun)
注意即使参数类型定义为 JavaScript Function
,它实际上仅限于 WebAssembly 函数。
在暂停功能方面,有一个新的类 WebAssembly.Suspending
,以及一个可以接收 JavaScript 函数作为参数的构造器。在 WebIDL 中,这写作如下:
interface Suspending{
constructor (Function fun);
}
注意这个 API 带有一种不对称的感觉:有一个函数接收 WebAssembly 函数并返回一个新的 promising(有承诺的)函数;而标记暂停函数时则需要将其封装在一个 Suspending
对象中。这反映了底层发生的更深层次的现实。
导入的暂停行为本质上是对导入调用的一部分操作:即,实例化模块中的一些函数调用导入并因此暂停。
另一方面,promising
函数接收一个普通的 WebAssembly 函数并返回一个可以响应暂停并返回 Promise
的新函数。
使用新 API
如果您是 Emscripten 用户,使用新 API 通常不需要对代码进行更改。您必须使用至少 3.1.61 版本的 Emscripten,并且必须使用至少版本为 126.0.6478.17(Chrome M126)的 Chrome。
如果您在自行集成,您的代码应该会显著简化。特别是,不再需要编写存储传入的 Suspender
对象(并在调用导入时检索它)的代码。您可以在 WebAssembly 模块中简单使用常规的顺序代码。
旧 API
旧 API 至少会继续运作到 2024 年 10 月 29 日(Chrome M128)。在此之后,我们计划移除旧 API。
请注意,从 3.1.61 版本开始,Emscripten 本身将不再支持旧 API。
检测您的浏览器中启用了哪个 API
更改 API 从来不是轻率的行为。在这种情况下我们之所以可以这么做,是因为 JSPI 本身仍处于临时状态。有一个简单的方法可以测试您的浏览器中启用了哪个 API:
function oldAPI(){
return WebAssembly.Suspender!=undefined
}
function newAPI(){
return WebAssembly.Suspending!=undefined
}
oldAPI
函数在您的浏览器中启用了旧 JSPI API 时返回 true,而 newAPI
函数在启用了新 JSPI API 时返回 true。
JSPI 正在发生什么?
实现方面
我们正在进行的对 JSPI 的最大改动对大多数程序员来说实际上是不可见的:即所谓的可增长栈。
当前 JSPI 的实现基于分配固定大小的栈。实际上,分配的栈相当大。这是因为我们必须能够容纳可能需要深栈来正确处理递归的任意 WebAssembly 计算。
然而,这并不是一种可持续的策略:我们希望支持具有数百万挂起协程的应用程序;如果每个栈的大小为 1MB,这是不可能的。
可增长栈是指允许 WebAssembly 栈根据需要增长的栈分配策略。这样,对于仅需要小栈空间的应用程序,我们可以从非常小的栈开始,而在应用程序空间不足时(即所谓的栈溢出)增长栈。
实现可增长栈有几种潜在技术。其中一种我们正在研究的方法是分段栈。分段栈由一系列栈区域组成,每个栈区域固定大小,但不同的段可能大小不同。
请注意,尽管我们可能会解决协程的栈溢出问题,但我们并不计划使主栈或中央栈可增长。因此,如果您的应用程序用尽了栈空间,除非您使用 JSPI,否则可增长栈不会解决您的问题。
标准化进程
截至发布,有一个针对 JSPI 的活跃试验性功能。新 API 将在试验性功能的剩余期间可用 — 在 Chrome M126 提供。
以前的 API 也将在试验性功能期间可用;然而,计划在 Chrome M128 之后不久被淘汰。
在此之后,JSPI 的主要工作集中在标准化过程中。截至发布,JSPI 当前处于 W3C Wasm CG 进程的第 3 阶段。下一步,即进入第 4 阶段,标志着 JSPI 被正式采纳为 JavaScript 和 WebAssembly 生态系统的标准 API。
我们希望了解您对 JSPI 改变的看法!请加入 W3C WebAssembly Community Group 仓库 的讨论。