WASM 的 GC 提案对 Rust 编译到 WASM 意味着什么作为一个自学 Rust 并编译到 WASM 的人我一直有个困惑为什么 Rust 编译到 WASM 后生成的代码那么大一个简单的Hello World WASM 模块就有几十 KB而同样的 C 程序编译到 WASM 可能只有几 KB。后来才知道Rust 编译到 WASM 时需要自带一个分配器和panic handler这些占了不少空间。而 WASM 的 GC垃圾回收提案可能会改变这个局面。这篇就把我对 WASM GC 提案的理解和对 Rust-WASM 的影响分析写出来。一、WASM GC 提案是什么——一个的翻译式理解WASM GC 提案正式名称Wasm GC Proposal是 WebAssembly 标准的一个扩展目的是在 WASM 虚拟机里原生支持垃圾回收类型——struct、array、function reference 等。这些类型由 WASM 运行时自动管理生命周期不需要程序自己实现 free/GC。换句话说以前 WASM 里只有线性内存一块大 buffer程序自己管理现在 WASM 可以有GC 内存由 WASM 运行时自动回收的对象。这对哪些语言有好处Java/Kotlin/Dart这些语言本身就是 GC 语言编译到 WASM 时需要自带一个 GC runtime几百 KB。WASM GC 提案让它们可以用 WASM 原生的 GC不用自带体积大幅缩小。Python/Ruby同上GC 语言编译到 WASM 时自带 GC runtime 很重。Rust/C/C这些语言不需要 GC但 WASM GC 提案对它们有间接影响——WASM 运行时需要实现 GC 机制可能影响 WASM 的性能和内存模型。flowchart TD A[WASM GC 提案] -- B[新增 GC 类型] B -- B1[structrefbr/GC管理的结构体] B -- B2[arrayrefbr/GC管理的数组] B -- B3[funcref (增强)br/函数引用闭包] A -- C[GC 语言受益] C -- C1[Java/Kotlinbr/不用自带GC runtime] C -- C2[Dart/Pythonbr/体积大幅缩小] A -- D[Rust 的间接影响] D -- D1[WASM 运行时更复杂br/GC开销可能影响性能] D -- D2[新可能性br/Rust 生成 GC 类型代码] style C fill:#6f6 style D fill:#fc6二、Rust 编译到 WASM 的现状没有 GC全靠自己管当前 Rust 编译到 WASM 的流程Rust 代码 → LLVM IR → WASM 字节码WASM 模块自带线性内存由memory.grow管理堆分配由 Rust 的 allocator 实现默认是dlmalloc约 10KB 代码panic handler 和其他 runtime 代码也要自带这就是为什么 Rust-WASM 的体积比较大——它需要自带一整套运行时支持。没有 WASM 原生的 GCRust 只能在线性内存里自己管理堆。// Rust 编译到 WASM 时的默认配置 // 不使用 WASM GC所有对象都在线性内存里管理 use wasm_bindgen::prelude::*; #[wasm_bindgen] pub struct Counter { // 这个结构体在线性内存里由 Rust 的 allocator 管理 // WASM 运行时不会自动回收它 value: i32, } #[wasm_bindgen] pub fn create_counter() - Counter { // 在线性内存里分配 Counter // 需要 Rust 的 allocator自带 dlmalloc Counter { value: 0 } } #[wasm_bindgen] pub fn increment_counter(counter: mut Counter) - i32 { counter.value 1; counter.value } // 问题Counter 的销毁由 JavaScript 侧的 wasm_bindgen 管理 // 如果 JS 侧忘记调用 .free()Counter 就泄漏了wasm-bindgen 的内存泄漏风险// JavaScript 侧使用 WASM 模块 const counter wasm.create_counter(); console.log(wasm.increment_counter(counter)); // ❌ 如果忘记调用 counter.free()线性内存里的 Counter 就泄漏了 // wasm-bindgen 会用 FinalizationRegistry 尝试自动回收 // 但 FinalizationRegistry 的 GC 是不确定的可能不及时 // ✅ 正确做法手动释放 counter.free(); // 显式释放线性内存里的 Counterflowchart LR A[Rust 代码] -- B[LLVM → WASM] B -- C[线性内存br/dlmalloc allocatorbr/~10KB 代码] C -- D[wasm-bindgenbr/桥接 JS] D -- E[JS 调用 .free()br/手动释放] D -- F[忘记 .free()br/⚠️ 内存泄漏] style C fill:#f96 style F fill:#f66 style E fill:#6f6三、WASM GC 对 Rust 的潜在影响三个可能的场景WASM GC 提案对 Rust 有三个潜在影响场景场景1Rust 继续用线性内存GC 提案对 Rust 无直接影响这是最保守的场景。Rust 不使用 WASM 的 GC 类型继续在线性内存里管理堆。WASM GC 提案对 Rust 唯一的影响是WASM 运行时需要实现 GC 机制运行时本身变得更复杂、更重。但这不影响 Rust 生成的 WASM 代码。// 场景1Rust 不用 WASM GC继续用线性内存 // 和现在完全一样没有变化 #[wasm_bindgen] pub fn compute(data: [u8]) - Vecu8 { // 所有分配在线性内存里 let result data.iter().map(|b| b * 2).collect(); result }场景2Rust 用 WASM GC 类型替代 dlmalloc减少体积如果 Rust 编译器支持生成 WASM GC 类型那么 Rust 的堆分配可以用 WASM 原生的 GC 管理的arrayref替代 dlmalloc。这样 Rust 就不需要自带 allocatorWASM 模块的体积可以减少 10KB。// 场景2Rust 用 WASM GC 类型假设编译器支持 // VecT 的底层用 WASM arrayref 而不是 dlmalloc // 目前这是假设性的代码Rust 编译器还不支持 #[wasm_bindgen] pub fn compute_gc(data: [u8]) - Vecu8 { // Vecu8 的底层可能用 WASM 的 arrayref // 由 WASM 运行时的 GC 自动回收不需要 dlmalloc let result data.iter().map(|b| b * 2).collect(); result // 不需要手动 .free()WASM GC 自动回收 }场景3Rust 和 GC 语言共享 WASM 模块跨语言协作WASM GC 提案让不同语言编译到同一个 WASM 模块变得更容易。比如一个 WASM 模块里Kotlin 编译的部分用 GC 类型Rust 编译的部分用线性内存两种内存模型共存于同一个 WASM 运行时。flowchart TD A[WASM 模块] -- B[GC 内存区域br/Kotlin/Dart 使用] A -- C[线性内存区域br/Rust/C 使用] B -- D[WASM 运行时 GCbr/自动回收 structref/arrayref] C -- E[Rust allocatorbr/dlmalloc / wee_alloc] D -- F[两种内存共存br/互不干扰] C -- F style B fill:#6cf style C fill:#fc6 style F fill:#6f6三个场景的对比场景Rust 代码变化体积变化性能影响可行性不用 GC无无运行时更重✅ 现状用 GC arrayrefallocator 替换减少约10KBGC开销不确定⚠️ 需编译器支持跨语言共存无取决于组合两种内存共存⚠️ 需运行时支持四、实际应对Rust-WASM 开发者现在该做什么WASM GC 提案目前还在阶段3接近最终标准大部分 WASM 运行时还没实现。对 Rust-WASM 开发者来说现在不需要急着适配 GC但需要做以下准备// 1. 体积优化用 wee_alloc 替代 dlmalloc比 dlmalloc 小 // 在 Cargo.toml 添加: // [dependencies] // wee_alloc 0.4 #[global_allocator] static ALLOC: wee_alloc::WeeAlloc wee_alloc::WeeAlloc::INIT; // wee_alloc 只有约 1KB 代码比 dlmalloc 的 10KB 小很多 // 但性能稍差单线程不适合高频分配 // 2. 体积优化用 wasm-opt 压缩 WASM 字节码 // cargo install wasm-opt // wasm-opt -Oz -o output.wasm input.wasm // -Oz 是最激进的体积优化可以减少 20-40% 大小 // 3. 手动内存管理确保所有 Rust 导出类型都有 .free() #[wasm_bindgen] pub struct ManagedBuffer { data: Vecu8, } #[wasm_bindgen] impl ManagedBuffer { #[wasm_bindgen(constructor)] pub fn new(size: usize) - ManagedBuffer { ManagedBuffer { data: vec![0u8; size] } } // ✅ 关键提供 free 方法JS 必须调用 pub fn free(mut self) { // 手动释放 Vec 的内存 self.data Vec::new(); // drop 旧数据 } pub fn data(self) - [u8] { self.data } } // 4. 用 FinalizationRegistry 增加安全网JS 侧 // 防止忘记调用 .free() 导致泄漏 // JavaScript 代码 // const registry new FinalizationRegistry((heldValue) { // heldValue.free(); // 当 JS 对象被 GC 时自动释放 WASM 内存 // }); // registry.register(wasmObject, wasmObject);体积优化实测优化原始体积优化后体积减少dlmalloc默认45KB45KB0%wee_alloc45KB36KB20%wee_alloc wasm-opt -Oz36KB22KB39%理想WASM GC假设45KB~20KB~56%graph LR A[默认 WASMbr/45KB] -- B[wee_allocbr/36KB(-20%)] B -- C[wasm-opt -Ozbr/22KB(-39%)] C -- D[WASM GC(未来)br/~20KB(-56%)] A --|第一步| B B --|第二步| C style C fill:#6f6 style D fill:#6cf五、总结WASM GC 提案主要服务于 GC 语言Java、Dart、Kotlin让它们编译到 WASM 时不用自带 GC runtime体积大幅缩小。对 Rust 的影响是间接的短期Rust 继续用线性内存 dlmalloc/wee_alloc。体积优化靠 wee_alloc wasm-opt不需要等 GC 提案。中期如果 Rust 编译器支持生成 WASM GC 类型arrayref可以替代 dlmalloc减少约 10KB 代码。但这需要编译器团队的大量工作。长期WASM GC 可能让 Rust 和 GC 语言在同一个 WASM 模块里共存实现跨语言 WASM 协作。作为自学 Rust-WASM 的开发者我的建议是现在不要等 WASM GC先做好 wee_alloc wasm-opt 的体积优化确保手动内存管理所有导出类型都有 .free()。WASM GC 提案落地后再考虑是否迁移到 GC 类型——迁移成本可能很高因为 Rust 的所有权模型和 GC 的自动回收模型是两种完全不同的思维方式。WASM 的标准演进很快GC 提案只是其中一个。作为 WASM 开发者保持关注但不要急于跟进——稳定的工具链比最新的提案更重要。我踩过追最新提案导致代码不稳定的坑后来才知道先在现有稳定方案下把代码写好等提案真正落地了再迁移成本远低于一开始就追新标准。
WASM 的 GC 提案:对 Rust 编译到 WASM 意味着什么
WASM 的 GC 提案对 Rust 编译到 WASM 意味着什么作为一个自学 Rust 并编译到 WASM 的人我一直有个困惑为什么 Rust 编译到 WASM 后生成的代码那么大一个简单的Hello World WASM 模块就有几十 KB而同样的 C 程序编译到 WASM 可能只有几 KB。后来才知道Rust 编译到 WASM 时需要自带一个分配器和panic handler这些占了不少空间。而 WASM 的 GC垃圾回收提案可能会改变这个局面。这篇就把我对 WASM GC 提案的理解和对 Rust-WASM 的影响分析写出来。一、WASM GC 提案是什么——一个的翻译式理解WASM GC 提案正式名称Wasm GC Proposal是 WebAssembly 标准的一个扩展目的是在 WASM 虚拟机里原生支持垃圾回收类型——struct、array、function reference 等。这些类型由 WASM 运行时自动管理生命周期不需要程序自己实现 free/GC。换句话说以前 WASM 里只有线性内存一块大 buffer程序自己管理现在 WASM 可以有GC 内存由 WASM 运行时自动回收的对象。这对哪些语言有好处Java/Kotlin/Dart这些语言本身就是 GC 语言编译到 WASM 时需要自带一个 GC runtime几百 KB。WASM GC 提案让它们可以用 WASM 原生的 GC不用自带体积大幅缩小。Python/Ruby同上GC 语言编译到 WASM 时自带 GC runtime 很重。Rust/C/C这些语言不需要 GC但 WASM GC 提案对它们有间接影响——WASM 运行时需要实现 GC 机制可能影响 WASM 的性能和内存模型。flowchart TD A[WASM GC 提案] -- B[新增 GC 类型] B -- B1[structrefbr/GC管理的结构体] B -- B2[arrayrefbr/GC管理的数组] B -- B3[funcref (增强)br/函数引用闭包] A -- C[GC 语言受益] C -- C1[Java/Kotlinbr/不用自带GC runtime] C -- C2[Dart/Pythonbr/体积大幅缩小] A -- D[Rust 的间接影响] D -- D1[WASM 运行时更复杂br/GC开销可能影响性能] D -- D2[新可能性br/Rust 生成 GC 类型代码] style C fill:#6f6 style D fill:#fc6二、Rust 编译到 WASM 的现状没有 GC全靠自己管当前 Rust 编译到 WASM 的流程Rust 代码 → LLVM IR → WASM 字节码WASM 模块自带线性内存由memory.grow管理堆分配由 Rust 的 allocator 实现默认是dlmalloc约 10KB 代码panic handler 和其他 runtime 代码也要自带这就是为什么 Rust-WASM 的体积比较大——它需要自带一整套运行时支持。没有 WASM 原生的 GCRust 只能在线性内存里自己管理堆。// Rust 编译到 WASM 时的默认配置 // 不使用 WASM GC所有对象都在线性内存里管理 use wasm_bindgen::prelude::*; #[wasm_bindgen] pub struct Counter { // 这个结构体在线性内存里由 Rust 的 allocator 管理 // WASM 运行时不会自动回收它 value: i32, } #[wasm_bindgen] pub fn create_counter() - Counter { // 在线性内存里分配 Counter // 需要 Rust 的 allocator自带 dlmalloc Counter { value: 0 } } #[wasm_bindgen] pub fn increment_counter(counter: mut Counter) - i32 { counter.value 1; counter.value } // 问题Counter 的销毁由 JavaScript 侧的 wasm_bindgen 管理 // 如果 JS 侧忘记调用 .free()Counter 就泄漏了wasm-bindgen 的内存泄漏风险// JavaScript 侧使用 WASM 模块 const counter wasm.create_counter(); console.log(wasm.increment_counter(counter)); // ❌ 如果忘记调用 counter.free()线性内存里的 Counter 就泄漏了 // wasm-bindgen 会用 FinalizationRegistry 尝试自动回收 // 但 FinalizationRegistry 的 GC 是不确定的可能不及时 // ✅ 正确做法手动释放 counter.free(); // 显式释放线性内存里的 Counterflowchart LR A[Rust 代码] -- B[LLVM → WASM] B -- C[线性内存br/dlmalloc allocatorbr/~10KB 代码] C -- D[wasm-bindgenbr/桥接 JS] D -- E[JS 调用 .free()br/手动释放] D -- F[忘记 .free()br/⚠️ 内存泄漏] style C fill:#f96 style F fill:#f66 style E fill:#6f6三、WASM GC 对 Rust 的潜在影响三个可能的场景WASM GC 提案对 Rust 有三个潜在影响场景场景1Rust 继续用线性内存GC 提案对 Rust 无直接影响这是最保守的场景。Rust 不使用 WASM 的 GC 类型继续在线性内存里管理堆。WASM GC 提案对 Rust 唯一的影响是WASM 运行时需要实现 GC 机制运行时本身变得更复杂、更重。但这不影响 Rust 生成的 WASM 代码。// 场景1Rust 不用 WASM GC继续用线性内存 // 和现在完全一样没有变化 #[wasm_bindgen] pub fn compute(data: [u8]) - Vecu8 { // 所有分配在线性内存里 let result data.iter().map(|b| b * 2).collect(); result }场景2Rust 用 WASM GC 类型替代 dlmalloc减少体积如果 Rust 编译器支持生成 WASM GC 类型那么 Rust 的堆分配可以用 WASM 原生的 GC 管理的arrayref替代 dlmalloc。这样 Rust 就不需要自带 allocatorWASM 模块的体积可以减少 10KB。// 场景2Rust 用 WASM GC 类型假设编译器支持 // VecT 的底层用 WASM arrayref 而不是 dlmalloc // 目前这是假设性的代码Rust 编译器还不支持 #[wasm_bindgen] pub fn compute_gc(data: [u8]) - Vecu8 { // Vecu8 的底层可能用 WASM 的 arrayref // 由 WASM 运行时的 GC 自动回收不需要 dlmalloc let result data.iter().map(|b| b * 2).collect(); result // 不需要手动 .free()WASM GC 自动回收 }场景3Rust 和 GC 语言共享 WASM 模块跨语言协作WASM GC 提案让不同语言编译到同一个 WASM 模块变得更容易。比如一个 WASM 模块里Kotlin 编译的部分用 GC 类型Rust 编译的部分用线性内存两种内存模型共存于同一个 WASM 运行时。flowchart TD A[WASM 模块] -- B[GC 内存区域br/Kotlin/Dart 使用] A -- C[线性内存区域br/Rust/C 使用] B -- D[WASM 运行时 GCbr/自动回收 structref/arrayref] C -- E[Rust allocatorbr/dlmalloc / wee_alloc] D -- F[两种内存共存br/互不干扰] C -- F style B fill:#6cf style C fill:#fc6 style F fill:#6f6三个场景的对比场景Rust 代码变化体积变化性能影响可行性不用 GC无无运行时更重✅ 现状用 GC arrayrefallocator 替换减少约10KBGC开销不确定⚠️ 需编译器支持跨语言共存无取决于组合两种内存共存⚠️ 需运行时支持四、实际应对Rust-WASM 开发者现在该做什么WASM GC 提案目前还在阶段3接近最终标准大部分 WASM 运行时还没实现。对 Rust-WASM 开发者来说现在不需要急着适配 GC但需要做以下准备// 1. 体积优化用 wee_alloc 替代 dlmalloc比 dlmalloc 小 // 在 Cargo.toml 添加: // [dependencies] // wee_alloc 0.4 #[global_allocator] static ALLOC: wee_alloc::WeeAlloc wee_alloc::WeeAlloc::INIT; // wee_alloc 只有约 1KB 代码比 dlmalloc 的 10KB 小很多 // 但性能稍差单线程不适合高频分配 // 2. 体积优化用 wasm-opt 压缩 WASM 字节码 // cargo install wasm-opt // wasm-opt -Oz -o output.wasm input.wasm // -Oz 是最激进的体积优化可以减少 20-40% 大小 // 3. 手动内存管理确保所有 Rust 导出类型都有 .free() #[wasm_bindgen] pub struct ManagedBuffer { data: Vecu8, } #[wasm_bindgen] impl ManagedBuffer { #[wasm_bindgen(constructor)] pub fn new(size: usize) - ManagedBuffer { ManagedBuffer { data: vec![0u8; size] } } // ✅ 关键提供 free 方法JS 必须调用 pub fn free(mut self) { // 手动释放 Vec 的内存 self.data Vec::new(); // drop 旧数据 } pub fn data(self) - [u8] { self.data } } // 4. 用 FinalizationRegistry 增加安全网JS 侧 // 防止忘记调用 .free() 导致泄漏 // JavaScript 代码 // const registry new FinalizationRegistry((heldValue) { // heldValue.free(); // 当 JS 对象被 GC 时自动释放 WASM 内存 // }); // registry.register(wasmObject, wasmObject);体积优化实测优化原始体积优化后体积减少dlmalloc默认45KB45KB0%wee_alloc45KB36KB20%wee_alloc wasm-opt -Oz36KB22KB39%理想WASM GC假设45KB~20KB~56%graph LR A[默认 WASMbr/45KB] -- B[wee_allocbr/36KB(-20%)] B -- C[wasm-opt -Ozbr/22KB(-39%)] C -- D[WASM GC(未来)br/~20KB(-56%)] A --|第一步| B B --|第二步| C style C fill:#6f6 style D fill:#6cf五、总结WASM GC 提案主要服务于 GC 语言Java、Dart、Kotlin让它们编译到 WASM 时不用自带 GC runtime体积大幅缩小。对 Rust 的影响是间接的短期Rust 继续用线性内存 dlmalloc/wee_alloc。体积优化靠 wee_alloc wasm-opt不需要等 GC 提案。中期如果 Rust 编译器支持生成 WASM GC 类型arrayref可以替代 dlmalloc减少约 10KB 代码。但这需要编译器团队的大量工作。长期WASM GC 可能让 Rust 和 GC 语言在同一个 WASM 模块里共存实现跨语言 WASM 协作。作为自学 Rust-WASM 的开发者我的建议是现在不要等 WASM GC先做好 wee_alloc wasm-opt 的体积优化确保手动内存管理所有导出类型都有 .free()。WASM GC 提案落地后再考虑是否迁移到 GC 类型——迁移成本可能很高因为 Rust 的所有权模型和 GC 的自动回收模型是两种完全不同的思维方式。WASM 的标准演进很快GC 提案只是其中一个。作为 WASM 开发者保持关注但不要急于跟进——稳定的工具链比最新的提案更重要。我踩过追最新提案导致代码不稳定的坑后来才知道先在现有稳定方案下把代码写好等提案真正落地了再迁移成本远低于一开始就追新标准。