Rust 和 Wasm 的多线程
WebAssembly 首次发布时是一个 MVP,尽管它很小,但它催生了大量令人兴奋的项目,这些项目如今在所有主要浏览器中都能正常运行。Rust 也利用了 wasm MVP 的成功,通过 wasm-bindgen
和 wasm-pack
等工具,使 MVP 感觉不那么简陋。然而,WebAssembly 还有更大的野心!从一开始,它就旨在通过新的功能和特性来扩展 WebAssembly 规范。
我特别期待 WebAssembly 将要推出的一个功能是 线程 提案。不幸的是,线程提案在 Spectre 和 Meltdown 漏洞首次公布时被搁置,但现在它又开始重新获得动力!浏览器将在不久的将来开始发布 SharedArrayBuffer
,而 wasm 的线程也不会落后太多。
wasm 的线程等功能会对 Rust 及其在 Web 上的应用产生重大影响,我们希望确保 Rust 在这些功能可用时已做好准备,并适合 wasm 线程!我最近开始尝试更多地参与 WebAssembly 社区组,这似乎是测试 Rust 支持的绝佳机会,同时如果需要,还可以为提案本身提供反馈!
如果你渴望一个圆满的结局,你可以 跳到最后,那里(剧透)有一个演示展示了 Rust、WebAssembly 和线程在浏览器中的应用。
注意:作为对未来读者的说明,这篇文章描述并使用了许多功能,这些功能在撰写本文时是不稳定的。这里的内容在遥远的未来可能并不准确,示例可能不再有效。我们会尽力保持更新,但如果你是在本文撰写很久之后阅读的,请谨慎对待!
WebAssembly 线程提案
虽然人们可能会天真地认为“WebAssembly 线程”的概念类似于“添加 pthreads”或“添加 std::thread
”到 wasm,但 WebAssembly 中的线程当前提案实际上大不相同!线程提案并没有提供完整的库体验,而是指定了构建线程库的基本构建块。
原子指令
你可能会注意到线程提案的第一个方面是添加了 原子指令。用 Rust 的术语来说,这意味着 AtomicUsize
及其同类将实际编译为原子操作,而今天它们 只是降低到单线程等效项(因为没有线程!)。虽然这些指令必不可少,但它们在没有 wait
和 notify
之前并不太令人兴奋。
原子修改允许我们执行一定程度的同步,但完整的同步通常需要实际阻塞一个线程,直到另一个线程完成。这就是 i32.atomic.wait
和 atomic.notify
指令发挥作用的地方。首先,我们可以使用 i32.atomic.wait
(原子地)阻塞一个线程,然后另一个线程可以执行 atomic.notify
来唤醒阻塞在同一地址上的线程。我相信这类似于 Linux 上的 futexes,虽然我自己从未使用过它们!
仅凭这一项新增功能,我们现在就可以开始了解如何形成原语,并且该提案确实有一个 互斥锁实现示例,这也是 Rust Mutex
类型实现方式。
好吧,这很好,但我们如何生成多个线程呢?
通过 Web Workers 实现并行
WebAssembly 最大的优势之一是它扩展了 Web 平台,而不是试图取代它。虽然 wasm 模块本身主要只能直接操作数字,但它们可以导入任何任意函数,这使 wasm 可以完全访问 Web 平台,包括 DOM。从第一天起,WebAssembly 就是关于重用和增强 Web 平台体验,避免为新功能重新发明轮子。
wasm 的线程提案也不例外!Web 已经通过 Web Workers 支持多线程 Web 应用程序,这正是用于将多线程执行引入 WebAssembly 的方法。
但是,Web Workers 提供了非常有限的能力来在线程之间共享资源。通信和同步是通过消息传递 (postMessage
) 完成的,但你只能发送支持 结构化克隆 的值。用 Rust 的术语来说,JS 中只有很少的类型是 Send
,并且在将对象发送到另一个线程时,你总是 Clone
。
我们的目标是共享资源!事实证明,支持结构化克隆的类型之一是 WebAssembly.Module
。今天在 Web 上执行 wasm 需要使用 wasm JS API,你创建一个 WebAssembly.Module
,它类似于可执行文件(编译代码)的文本和数据部分,然后你从该模块创建一个 WebAssembly.Instance
,它是你实际获得堆和栈的地方。今天我们已经可以在线程之间传递 WebAssembly.Module
,尽管必须在线程之间 Clone
它,但在大多数引擎中看起来大致如下
pub struct Module {
contents: Arc<ModuleContents>,
}
// ...
这意味着克隆操作非常便宜!
共享我们的代码只是故事的一半。许多语言(包括 Rust)也依赖于共享内存作为构建各种并发范式(如消息传递或互斥锁)的原语。
共享内存
继续“没有专门为 wasm 提供的基本新功能”的主题,共享内存本质上是建立在已经稳定的(或者更确切地说,即将稳定)JS API 上:SharedArrayBuffer
。一个 SharedArrayBuffer
就像一个 ArrayBuffer
,只是它是共享的!这体现在结构化克隆算法中,你可以将其视为类似于 WebAssembly.Module
,内部包含一个 Arc
,克隆起来很便宜。
使用 SharedArrayBuffer
,JS 已经可以在工作线程和主线程之间共享内存,这使得计算大量数据并将其发送到另一个线程变得很便宜。(或者至少消除了在线程之间复制数据的需要)。
今天的 WebAssembly 模块可以选择与最多一个 “线性内存” 实例相关联。用非 wasm 的术语来说,你可以在 wasm 模块中放入一根 RAM。这个 WebAssembly.Memory
今天总是由 ArrayBuffer
支持,但你很快就能将内存标记为“共享”,这意味着它由 SharedArrayBuffer
支持。这随后意味着由 SharedArrayBuffer
支持的 WebAssembly.Memory
的结构化克隆将引用相同的内存!
此时,这些部分肯定开始融合在一起。我们已经可以在线程之间共享模块(代码),很快我们就能在线程之间共享内存了!有了这些新功能,我们可以在多个 Web 工作线程上快速高效地实例化一个 WebAssembly.Module
,所有这些工作线程都可以访问相同的内存。
一次初始化内存
WebAssembly 模块的一个有趣方面是内存会自动为你初始化。例如,假设你有一个看起来像这样的 Rust 程序
#[no_mangle]
pub extern fn get_data() -> *const u8 {
"the data".as_ptr()
}
如果我们从 JS 中调用它,并且如果我们读取返回的指针,我们实际上会看到 the data
!但是,谁实际将这些字节写入线性内存呢?每个 wasm 模块都可以有 数据段,这些数据段指定一个位于内存偏移处的字节块。在实例化模块时,wasm 运行时会将每个数据段复制到指定到线性内存的偏移量。
但是等等,如果我们在多个线程上实例化我们的模块,这可不是个好主意!假设我们有看起来像这样的代码
#[no_mangle]
pub extern fn get_ticket() -> usize {
static TICKET: AtomicUsize = AtomicUsize::new(1);
TICKET.fetch_add(1, SeqCst)
}
这里我们将有一个由 1usize
组成的数据段,它包含三个零字节,然后是一个字节。每次实例化模块时,我们都会通过覆盖先前值将此计数器重置为 1!我们希望发生的是,第一个线程初始化内存,所有其他线程应该只使用已经存在的内存。
为了解决这个问题,我们转向了 批量内存操作提案。虽然批量内存操作提案最初主要是作为执行 memcpy
和 memset
的原生方法,但现在它也获得了“被动内存段”的能力,这正好解决了我们这里遇到的问题。
每个数据段都可以被标记为“被动”,这意味着它在实例化时不会自动复制到内存中。相反,模块必须通过 memory.init
指令手动初始化内存。使用 memory.init
,我们可以将内存从任何数据段复制到内存中的任何位置。
有了 memory.init
,我们至少有能力解决多重初始化问题,但目前还不清楚我们将在工具链中如何利用它。稍后会详细介绍!
现有的 WebAssembly 功能和线程
关于 WebAssembly 线程(和批量内存)提案中提出的新功能就介绍到这里。在我们深入探讨这些功能的实际使用方式之前,值得快速回顾一下 WebAssembly 的现有功能以及它们在多线程环境中的意义。
第一个有趣的方面(我们将在后面利用它)是 start
函数。WebAssembly 模块可以将一个函数标记为在模块实例化时自动执行。这个 start
函数可以执行一些操作,比如静态初始化,甚至可能是 wasm 模仿可执行文件的 main
函数,但它目前在 Rust 中没有暴露或使用。在线程提案中,start
函数的语义并没有改变,但这意味着它不再是一次性初始化!相反,start
函数仍然在每个实例中运行,由于我们在多个 Web 工作线程上创建了多个实例,因此 start
函数更像是一个“线程初始化”而不是“全局初始化”。稍后会详细介绍!
接下来,让我们看一下 global
变量。请注意,这些不是 Rust 的 static
变量(如上面的 TICKET
),它们被编译为位于线性内存中。实际上,Rust 目前没有提供创建、获取或设置自定义 global
变量的能力,因此这在 Rust 中很大程度上是一个未公开的 WebAssembly 功能。然而,在 WebAssembly 中,global
正如其名称所暗示的那样,是一个实例的全局变量,可以获取、设置,甚至可以导出到 JS!全局变量更像是一个虚拟寄存器而不是线性内存,因为它只能包含一组固定的类型。
有趣的是,全局变量是针对每个实例的。这意味着在多实例环境中,它们实际上不是全局变量,而是线程局部变量!我们每个 wasm 实例都将拥有自己的全局变量集,其他实例无法访问它们,这为我们提供了线程局部数据的基础。稍后也会详细介绍!
WebAssembly 中的表可能在多线程 wasm 世界中也有一些有趣的用例,但我自己并不确定这些用例是什么,因此现在我们将主要忽略它们。除此之外,这应该涵盖了大多数 wasm 功能以及它们与线程的关系!
在 Rust 中使用线程
现在我们已经回顾了 WebAssembly 线程提案的要点,你可能会像我第一次阅读它时一样感到困惑。当然,所有这些功能听起来都很棒,但如何在语言级别安全且符合人体工程学地暴露它们?一些问题很好地自包含,比如 Rust 中 Mutex
的实现,但还有很多其他问题并不那么自包含,比如
-
首先是堆栈!LLVM(Rust 的代码生成器)假设它不仅可以使用本机 wasm 堆栈(它是针对每个实例的,因此是“线程局部”的),还可以使用线性内存堆栈。这意味着我们需要一个指向线性内存的堆栈指针(LLVM 已经很方便地将其放在一个
global
中),该指针对于每个线程都是唯一的,并且有人需要为每个线程分配这些堆栈。 -
接下来是线程局部数据。我们有了
global
变量的线程局部变量基础,但如前所述,Rust(以及 LLVM 或 LLD)实际上没有提供操作或使用自定义global
变量的能力。我们如何在 Rust 中实现标准库的thread_local!
宏? -
我们之前谈到了内存初始化,以及我们不想重新初始化和清除内存,但实际上是谁在做这件事?据推测,我们所有的数据段都需要是
passive
,但谁在安全地执行memory.init
? -
我们实际上将如何生成线程?谁负责实际创建 Web 工作线程?同样,通过什么机制
WebAssembly.Module
和WebAssembly.Memory
在工作线程之间传输并在正确的位置实例化? -
在使用
wasm-bindgen
等工具时,shim JS 如何到达所有具有 wasm 实例的工作线程?这个包装器 JS 是为了使调用 Rust 更符合人体工程学而需要的,我们不想过度使用主线程!
不幸的是,我们今天还没有所有这些问题的答案。当我们不想让它们相互关联时,这些问题也相互交织在一起!
Rust 对 Web 上 WebAssembly 的愿景是互操作性。你应该能够使用 Rust 和 WebAssembly,而无需你的应用程序的其他部分知道。此外,你的依赖关系图中深层的某个 crate 可能依赖于 JS 功能(比如 NPM 包或 web-sys
),你也不应该需要知道这一点!
目前尚不清楚我们是否能够在 Web 平台上为线程维护这种愿景。这就是我希望与其他人一起集思广益或获得帮助和想法的地方。毕竟,线程提案并不稳定,理论上我们还有很多时间来想出一些可以帮助我们的方法!
不过,我不想留下这样的悬念!虽然今天并非所有上述问题都有很好的答案,但我一直在努力至少为 wasm-bindgen
中的许多问题找到一个可行的解决方案。让我们看一下它,看看我们是否能够在今天实际演示线程和 WebAssembly!
线程和 wasm-bindgen
wasm-bindgen
工具由两部分组成。一部分是 过程宏,即 #[wasm_bindgen]
属性,它在编译时扩展并运行。这将在你的 Rust 代码中生成 shim,并为第二部分(wasm-bindgen
CLI)准备最终的二进制文件。CLI 工具 wasm-bindgen
处于独特的位置,可以对 WebAssembly 模块进行各种疯狂的转换(它 已经做到了!)。
WebAssembly 二进制格式 有很好的规范,并且令人惊讶地易于操作。 wasm-bindgen
CLI 工具目前正在使用优秀的 parity-wasm
crate 来解析 WebAssembly,这使得 wasm-bindgen
很容易进行奇特的转换。(很快就会有更多关于这方面的信息,一个更轻松的解决方案也正在开发中!)
有了 CLI 工具和 parity-wasm
,我们摆脱了 LLVM 的“束缚”(也就是说,在工具中进行实验比在 LLVM 本身中更容易),并且可以访问 WebAssembly 的全部功能集。让我们利用这种新获得的力量来解决上面的一些问题。
注入线程局部 global
虽然 LLVM/LLD 目前没有能力发出自定义 global
变量,但我们在 wasm-bindgen
中可以!这是一种实现线程局部存储的简单方法,因此让我们让 wasm-bindgen
注入两个全局变量
-
首先是线程 ID。线程 ID 在许多应用程序中都很有用,但我们目前特别感兴趣的是标准库的
ReentrantMutex
,它需要知道哪个线程是哪个,以便在使用可重入锁时知道。 -
接下来是 TCB 槽。TCB 是一个“线程控制块”,通常用于存储线程运行时中分配的结构。这个分配的结构是许多其他运行时相关功能的入口点,但现在我们主要将其用作用户定义的线程局部值的存储。或者换句话说,这就是我们将在 Rust 中实现
thread_local!
的方式。
在 wasm 模块中添加两个类型为 i32
的 global
变量很容易,但我们也需要管理它们!仍然有人需要实际分配线程 ID,并且我们还需要能够访问它。
为此,让我们从 wasm-bindgen
的剧本中再拿一个技巧,重写函数调用。我们定义一个像这样导入的函数
#[link(wasm_import_module = "__wbindgen_thread_xform__")]
extern {
fn __wbindgen_thread_id() -> u32;
}
实际上神奇地变成了 get_global $thread_id
。 call
指令实际上与 get_global
有一对一的替换,因此这里的重写非常简单!我们可以使用类似的“内联函数”,比如 __wbindgen_tcb_get
和 __wbindgen_tcb_set
来获取/设置 TCB。
接下来,让我们弄清楚如何初始化这个线程 ID 全局变量。
从哪里 start
?
我们之前看到 WebAssembly 提供了一个 start
函数,该函数在模块实例化时自动调用,在多线程环境中,这是针对每个线程的初始化。这实际上正是我们想要的线程 ID 初始化以及其他方面的初始化!
使用 wasm-bindgen
,我们可以通过一个注入的 start
函数解决上面很多问题。我们甚至可以在完成时调用之前的 start
函数,以保持语义等效性!我们注入的函数将执行以下步骤
-
原子地递增一个注入的全局线程 ID 计数器。我们在
wasm-bindgen
中为线性内存保留了 4 个字节的空间,这个地址将跟踪所有曾经存在的线程。这个原子加法的结果可以存储在我们的线程 ID 全局变量中,这意味着我们刚刚分配并初始化了我们的线程 ID! -
如果线程 ID 为零,我们就知道我们是第一个线程(主线程)。这是一个初始化内存的好时机,因此
wasm-bindgen
可以将我们所有数据段标记为passive
,如果我们的 ID 为 0,我们可以调用memory.init
。 -
如果我们的线程 ID 不是 0,那么我们就知道我们是一个生成的线程。LLVM 已经安排好为我们的堆栈指针提供一个
global
,但它的初始值仅对主线程有效。为了继续执行,我们需要设置它。为了分配堆栈,我们可以利用方便的memory.grow
指令,这是一种快速而简便的分配内存方法,无需使用标准库的实际内存分配器(调用它反过来需要堆栈)。一旦我们有了堆栈,我们就可以更新我们的堆栈指针全局变量,我们应该就可以开始了! -
最后,如果之前有
start
函数可用,我们可以在此时委托给它并调用它。
好的,我们正在取得进展!通过假设所有线程都使用完全相同的 WebAssembly.Module
,一个注入的 start
函数可以涵盖很多方面,使线程变得易于使用。
管理 WebAssembly.Memory
默认情况下,所有用 Rust 编译的 wasm 二进制文件都会导出它们定义的 memory
。这意味着 wasm 模块的实例化将自动创建一个 WebAssembly.Memory
实例,并使其可供使用。然而,这与线程不兼容,因为我们希望所有模块都使用相同的内存实例!
相反,我们需要安排内存被导入而不是导出。这可能有点难以设置和使用,因此 wasm-bindgen
可以继续在 JS 绑定中处理实例化,这样用户就不必担心它。
请注意,目前 LLVM/LLD 也没有实现标记为 shared
的内存对象,因此作为一个小细节,wasm-bindgen
也可以处理它。
共享 JS shim,生成 Worker
这就是 wasm-bindgen
的故事开始走向“这种特定策略似乎不再具有长期可行性”领域的地方。我们需要处理的最后几件事实际上是生成 web worker,并以某种方式将 WebAssembly.Module
和 WebAssembly.Memory
放到每个 worker 上。
我最初尝试看看我们是否可以使用 Webpack,因为对于大规模集成来说,有一个捆绑器故事会很棒。不幸的是,我遇到了一些问题,比如 你无法访问 WebAssembly.Module
,而且我不清楚 worker 如何使用不同的实例化路径,该路径会 onmessage
来等待模块/内存,然后在收到后实例化。不过不用担心,我相信我们会以某种方式为它想出一个捆绑器故事!
接下来,我转向了 wasm-bindgen
的 --no-modules
选项,看看是否可以做些什么。目前它导出一个全局变量(名为 wasm_bindgen
),它是一个函数,它接受 wasm 文件的路径来实例化。我将其扩展为接受此路径或 WebAssembly.Module
实例(以及 WebAssembly.Memory
)。这样,当给定路径时,它可以创建内存并执行 fetch/实例化,但使用 WebAssembly.Module
时,它可以避免 fetch 并使用提供的内存来实例化。
由于 --no-modules
使设置一切变得非常手动,因此让主线程像往常一样工作,提供模块/内存的访问器,启动 web worker,并将模块/内存发布到每个 worker 非常容易。在 worker 内部,我们可以导入 --no-modules
生成的 JS,等待消息,等待实例化,然后开始执行一些工作。
总而言之,这种设置使某些东西能够工作。这绝对不是一个长期的解决方案,因为还没有使用捆绑器或 node.js 等运行时的路径。不过,在 wasm 线程稳定之前,我们肯定会完善所有这些细节!
演示:光线追踪
呼!这真是很多信息和背景,但希望你对线程提案是什么以及我们如何在 Rust 和 wasm-bindgen
中利用它有了更好的了解。现在让我们进入正题。
我们最初努力使用 Mandelbrot 集 渲染来实现一个很酷的 Rayon,但不幸的是,生成 Web Worker 的限制意味着我们 无法使用 Rayon。再加上我自己注意力不集中以及对 Mandelbrot 的理解不足,我转向了光线追踪!
多年没有接触光线追踪,我在网上搜索了一下,看看是否有我可以尝试的现有 Rust 光线追踪器。我找到的 我最喜欢的那个 不幸地需要 nightly 版本,并且最后一次编译是在 2017 年年中,但 @bheisler 在 2017 年初有一个 很棒的教程,并且 相关代码 今天仍然可以编译和运行(太棒了!)。在 进行了一些无关紧要的修改 之后,我能够在网络上按原样使用该项目。
顺便说一句,这是 Rust 和 Cargo 的一个很棒的方面。找到一个光线追踪器、集成它、将其编译为 wasm 并将其在浏览器中运行,这一切都毫不费力。
对于光线追踪(或者至少是这个光线追踪器的工作方式),这是一个令人尴尬的并行任务,因为图像的所有像素都是完全独立地渲染的。这意味着我们可以很容易地设置一种方法来 在 worker 线程之间分配像素的工作。
我认为这个演示的最后一个很酷的部分是看到渐进式渲染,看看图像在渲染时的样子。主线程会不时地请求 worker 线程的更新,并且 它们会将一个 ImageData
发送到主线程,该线程可以将其渲染到画布上。
对于这个演示,请记住,这涉及很多不稳定和 nightly 技术。它只在 Firefox 中工作(截至撰写本文时),因为其他浏览器还没有实现 memory.init
指令。
你会在左边找到一个巨大的 JSON 块,它描述了要渲染的场景。现在它是一个非常简单的光线追踪器,所以它只支持平面和球体,但你可以移动物体,添加球体等等。如果你愿意,我很乐意得到一些帮助来 实现更复杂的渲染。
未来工作
虽然我们已经做出了演示,但我们还有很多工作要做!以下是一些剩余任务的重点。
主线程不允许 atomic.wait
浏览器的主线程无法执行 atomic.wait
指令,如果执行此指令,它将无条件地抛出异常。这意味着,默认情况下,互斥锁在主线程上争用时将无法工作!此外,这意味着目前与主线程同步的唯一方法是在 worker 中使用 postMessage
。
这种情况尤其加剧了 Rust 的全局分配器 dlmalloc 是全局同步的。这意味着,如果你的主线程分配内存,它可能会偶尔抛出异常,如果争用!这实际上也是上面演示中的一个 bug!
我在线程提案库中 提交了一个问题 来讨论这个问题,希望我们能够找到一个合理的方法来解决主线程至少仍然可以分配内存的问题!到目前为止,我了解到一个关于 Atomics.waitAsync
的提案,它是一种唤醒主线程的第二种机制。还有一些关于自定义分配器的想法,它在很大程度上是无锁的,但在争用期间在主线程上回退到 memory.grow
。然而,在目前,这使得主线程很难使用 crates.io 上的任意库,因为它们必须经过审核,以确保任何同步。
我们可能会为 Rust 实现的“解决方法”是在互斥锁实现中简单地自旋,而不是在线程 ID 为 0 时使用 atomic.wait
。除了这是一种糟糕的同步方式之外,它还将第一个实例化始终在主线程上进行,而这可能并不总是正确的!
线程退出尚未实现
现在,在 Rust 中的模型中,并没有真正退出线程的概念。这意味着,如果一个线程确实退出了(即 worker 被 gc 了),那么它会泄漏内存分配,例如
- 线程的堆栈(它永远不会被回收或重用)
- 线程本地存储中的所有数据(Rust 不会注册析构函数)
最终,我们需要添加线程退出的概念,以便我们能够正确地处理这种情况,并回收资源以供以后重用。这可能是一个 WeakRef
提案 可以提供帮助的情况,因为它会在 js 对象被 gc 时自动运行线程退出。
堆栈溢出再次很糟糕
在布局线性内存时,LLD 默认情况下会先放置静态数据,然后放置主线程的堆栈。但这有一个问题,如果主线程发生堆栈溢出,它会静默地破坏所有静态数据!为了解决这个问题,我们向 LLD 传递 --stack-first
,它会将堆栈放在内存中的第一个位置,从而导致堆栈溢出,因为发生了越界内存访问而导致陷阱。
不幸的是,我们没有为所有 worker 线程提供这种便利。worker 线程与以前一样遇到同样的问题,如果发生堆栈溢出,它会静默地破坏堆或静态数据。
我们有一个可用的选项,就是在递减堆栈指针之前插入一个序言(由 LLVM 或 wasm-bindgen
生成),以检查我们是否有足够的线性堆栈空间(如果没有则触发异常),但目前尚不清楚这种更改会对所有函数产生什么样的性能影响!其他解决方案可能需要新的 wasm 功能,例如取消映射内存以强制操作触发异常。
你也可以做香肠!
如果你对香肠的制作过程和/或如何参与其中感到好奇,这里列出了构建此演示所做的更改以及一些有用的存储库!
- 关于批量内存操作的概述已针对语义和编码进行了澄清。
wabt
套件现在支持批量内存操作指令。parity-wasm
现在支持批量内存操作。- 同步 Rust 标准库中的全局分配器。
- 为 Rust 标准库实现线程本地存储。
- 修复生成的
wasm-bindgen
JS API 不存在的情况(在 Web Workers 中实例化所需)。 - 切换到在
js-sys
中使用线程本地存储来缓存global()
。 - 将
#[derive(Debug, Clone)]
添加到js_sys::Promise
。 - 将
TypedArray.slice
绑定添加到js-sys
。 - 确保
JsValue
不是Send
。 - 修复主线程上 future 未完成的边缘情况。
- 在 Rust 标准库中初步实现 wasm32 原子操作。
- 优化
libm
中 wasm32 上的内在函数。 - Firefox 对
memory.init
的解码需要更新。 - 在
wasm-bindgen
中实现对 WebAssembly 线程的支持。
如果你已经看到了这里,你可能已经知道 Rust 中的线程故事还需要一些工作!我们很乐意得到你的帮助,随时加入 Mozilla 的 IRC 上的 #rust-wasm
频道,#wg-wasm
Discord 频道,或者在 GitHub 上关注 wasm-bindgen
或 wasm-pack
。