在写完 ESP8266 IoT 架构之后,我就在想一件事情:既然代码开源,IoT 网关岂不是谁都能连?
于是我试图在微信小程序端做文章。但开源的小程序不能硬编码密钥,也不能仅靠接口鉴权。
我想到了很早之前预研的 WebAssembly(WASM)。配合后端防护,可以大幅提升攻击者的成本。
一、背景与目标
实现的功能非常简单:
传入 小程序 APPID + 设备 ID + 时间戳,使用 Ed25519 签名,返回 Token。
以前在微信小程序调通过 WxWebAssembly(简称 wxwasm),但当时主要处理 Int 类型,这次需要返回字符串。
当我拿出以前的预研代码进行修改时,我才意识到:
“能用 Rust 写 WASM” 和 “能在微信小程序里稳定运行”,完全是两回事。
二、第一阶段:理想很丰满 —— Rust + wasm-bindgen
最初的环境是:
- Rust 1.95
wasm-bindgenwasm-pack
Cargo.toml
[dependencies]
wasm-bindgen = "0.2"
ed25519-dalek = { version = "2.1", features = ["rand_core"] }
hex = "0.4"
lib.rs
[wasm_bindgen]
pub fn generate_token(appid: &str, device_id: &str, timestamp: &str) -> Result<String, JsValue> {
// Ed25519 签名逻辑
}
编译很顺利(就是有点慢 😭),本地 test.html 测试一切正常。
但一进微信小程序,噩梦开始了。
三、第二阶段:现实很骨感 —— 版本地狱
1. externref 错误
加载时报错:不支持 externref 类型。
网上 AI 搜索建议降级 Rust。于是我安装了 Rust 1.75.0。
结果:
wasm-bindgen开始报错- AI 建议锁定版本:
wasm-bindgen = "=0.2.87" - 接着
ed25519-dalek报错,要求更高版本 Rust
一番折腾后,配置变成了:
ed25519-dalek = { version = "=2.1.0", default-features = false, features = ["rand_core"] }
编译通过了 ✅
复制到微信开发者工具……
还是报错 ❌
TypeError: Cannot read property '__wbindgen_add_to_stack_pointer' of undefined
那一刻,我已经有点生气了 😤
四、第三阶段:换 C?又一条死路
一气之下,我决定换 C + Emscripten:
- 安装 Emscripten SDK
- 各种依赖下载失败
- 想到 C 的第三方库管理远不如 Rust
于是又放弃了。
五、第四阶段:冷静下来,从头再来
休息一天后,我重新回到 Rust。
这次的目标只有一个:先把字符串跑通,再谈业务。
最终确定的“稳定组合”是:
- Rust 1.72.1
- wasm-bindgen 0.2.87
Cargo.toml(关键点)
[package]
name = "wasm_reverse"
version = "0.1.0"
edition = "2021" # ⚠️ 不能是 2024
lib.rs
use wasm_bindgen::prelude::*;
[wasm_bindgen]
pub fn reverse_string(input: &str) -> String {
input.chars().rev().collect()
}
本以为这次稳了,结果报错:
SyntaxError: Cannot use 'import.meta' outside a module
这句话的内容:
微信小程序逻辑层 ≠ 标准浏览器
- ❌ 不支持 ESM
- ❌ 不支持动态
import - ❌ 更不是 Node
也就是说:wasm-pack 生成的胶水代码,在小程序里几乎不可用。
六、第五阶段:WXWebAssembly + RuntimeError
我改用手动重写胶水代码,使用 微信官方的 WXWebAssembly,手动加载 .wasm 文件。
WASM 能加载了 ✅
但一调用函数,立刻崩溃 ❌:
RuntimeError: unreachable
at __wbindgen_malloc
查了一圈才明白:
wasm-bindgen依赖完整的初始化流程- 小程序里我只做了
instantiate - 内存分配器根本没有准备好
👉 我在跑一个 “半初始化” 的 WASM 实例。
七、第六阶段:放弃wasm-bindgen,使用 C ABI
为了彻底稳定,我做了一个重要决定:
放弃 wasm-bindgen,手写 C ABI。
新规则非常简单:
- 所有数据通过 指针 + 长度 传递
- 内存由 Rust 分配
- JS 只负责拷贝和释放
虽然写起来麻烦了很多,但好处是:
- ✅ 我知道每一块内存在哪里
- ✅ 知道谁在管它
- ✅ 知道什么时候释放
编译命令:
cargo build --target wasm32-unknown-unknown --release
胶水代码:
const WASM_PATH = '/utils/btwasm.wasm'
let wasm = null
function getWasm() {
if (wasm) return Promise.resolve(wasm)
return new Promise((resolve, reject) => {
WXWebAssembly.instantiate(WASM_PATH, {})
.then(res => {
wasm = res.instance.exports
resolve(wasm)
})
.catch(err => {
console.error('[WASM] load failed', err)
reject(err)
})
})
}
八、第七阶段:JS 传参的隐形坑
终于调通了自己的逻辑,却又遇到诡异问题:
参数不正确
token: |wx123456
调用成功 ✅
返回值也有 ✅
但内容明显不对 ❌
排查后才恍然大悟:
我传给 WASM 的,并不是内存里的数据,而是 JS 对象本身。
Rust 把 JS 对象的地址当成指针去读,自然全是垃圾数据。
这也是为什么很多人第一次写 WASM 都会觉得:
“明明调用成功了,结果却莫名其妙。”
胶水代码:
async generateToken(appid, agentId, timestamp) {
const w = await getWasm()
const enc = new TextEncoder()
const dec = new TextDecoder()
const appidBytes = enc.encode(appid)
const agentBytes = enc.encode(agentId)
const timeBytes = enc.encode(timestamp)
// 1️⃣ 申请 WASM 内存
const appidPtr = w.malloc(appidBytes.length)
const agentPtr = w.malloc(agentBytes.length)
const timePtr = w.malloc(timeBytes.length)
// 2️⃣ 拷贝到 WASM 内存
const mem = new Uint8Array(w.memory.buffer)
mem.set(appidBytes, appidPtr)
mem.set(agentBytes, agentPtr)
mem.set(timeBytes, timePtr)
// 3️⃣ 输出长度位置
const outLenPtr = w.memory.buffer.byteLength - 8
// 4️⃣ 调用
const ptr = w.generate_token(
appidPtr,
appidBytes.length,
agentPtr,
agentBytes.length,
timePtr,
timeBytes.length,
outLenPtr
)
const outLen = new Uint32Array(w.memory.buffer)[outLenPtr / 4]
const result = new Uint8Array(w.memory.buffer, ptr, outLen)
const text = dec.decode(result)
// 5️⃣ 释放
w.free_buf(ptr, outLen)
w.free(appidPtr)
w.free(agentPtr)
w.free(timePtr)
return text
}
九、第八阶段:体积反而变大了
原本 wasm-bindgen 只有 70KB
自己搭完这套体系,变成了 120KB+
原因很现实:
- 引入了分配器
- 标准库没有完全裁剪
panic / format / string都在悄悄占空间
最终通过激进优化才压下来:
[lib]
crate-type = ["cdylib"]
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = "symbols"
最终生成的wasm不到50KB。
十、总结
这次折腾下来,我最大的感受是:
WASM 不是“写完就跑”,而是“写完还要适配运行环境”。
尤其是 微信小程序这种非标准 Web 容器:
- 胶水代码越少,稳定性越高
- 越接近 C ABI,越不容易翻车
- 内存模型不清,一切都是玄学
如果你也在考虑 Rust + WASM + 小程序,我的建议是:
- 别迷信官方示例
- 别急着写业务
- 先把内存模型和环境限制搞清楚
那一步,才是最贵的。
本文为真实项目复盘,未删减、未美化。