- 开发工具
【免费下载链接】wasm-bindgen
Facilitating high-level interactions between Wasm modules and JavaScript
本文以 wasm-bindgen 仓库的 console_log 示例(核心源码、Cargo.toml、package.json、webpack.config.js)为主体,系统讲解在 Rust 编译为 WebAssembly 后如何向浏览器控制台输出日志:从手写#[wasm_bindgen]绑定console.log,到用macro_rules!实现println!风格的日志宏,再到直接使用web-sys提供的现成 API。读完本文,你将掌握三种方案的完整写法、各自的适用场景,以及底层js_namespace/js_name属性与web-sys重载机制的工作原理。
运行方式:进入示例目录后执行
npm run serve,浏览器访问 http://localhost:8080 即可看到日志输出;示例已通过#[wasm_bindgen(start)]在模块加载时自动运行(src/lib.rs)。
示例的整体结构:一个入口,三种日志方案
console_log示例的核心逻辑非常紧凑:入口函数被标记为#[wasm_bindgen(start)],意味着 Wasm 模块实例化完成后会自动执行,无需从 JavaScript 侧手动调用(src/lib.rs):
use wasm_bindgen::prelude::*; #[wasm_bindgen(start)] fn run() { bare_bones(); using_a_macro(); using_web_sys(); }三个函数分别演示三种思路:
| 函数 | 方案 | 说明 |
|---|---|---|
bare_bones() | 手写extern "C"绑定console.log | 不依赖任何额外 crate,但需要自己保证注解正确 |
using_a_macro() | macro_rules!封装成console_log!宏 | 类似println!的格式化日志,弥补println!在 Wasm 目标上不可用的缺陷 |
using_web_sys() | 直接使用web-sys的console模块 | 官方 Web API 绑定,支持多参数重载与任意JsValue |
这三种方案的演进逻辑在源码注释中交代得很清楚:先“手动写绑定”,再“用宏提升开发体验”,最后“发现 web-sys 早就帮你做好了”。下文逐一展开。
方案一:手写#[wasm_bindgen]绑定 console.log
在不引入任何额外 crate 的前提下,可以通过extern "C"块把 JavaScript 函数直接声明为 Rust 可调用函数(src/lib.rs):
#[wasm_bindgen] extern "C" { // 用 js_namespace 绑定 console.log(..),而不是裸的 log(..) #[wasm_bindgen(js_namespace = console)] fn log(s: &str); // console.log 是高度多态的,可以用多个签名绑定同一个 JS 函数; // 此时必须用 js_name 保证始终调用 JS 中的 log #[wasm_bindgen(js_namespace = console, js_name = log)] fn log_u32(a: u32); // 多参数同样可行 #[wasm_bindgen(js_namespace = console, js_name = log)] fn log_many(a: &str, b: &str); } fn bare_bones() { log("Hello from Rust!"); log_u32(42); log_many("Logging", "many values!"); }三个关键点值得展开:
js_namespace = console:指定被调用的 JS 名字位于哪个命名空间下。没有它,生成的代码会调用裸的log(...);加上后生成的是console.log(...)。该属性的完整语义在 js_namespace 属性文档中有详细说明:它可以作用于任意导入(函数或类型),还可以接受字符串数组表示嵌套命名空间,例如#[wasm_bindgen(js_namespace = ["window", "document"])] fn write(...)会调用window.document.write。js_namespace可以写在单个 item 上,也可以整块提升到extern "C"外层,但两者不能同时出现。js_name = log:Rust 侧的函数名可以自由命名(如log_u32、log_many),通过js_name把它们都映射回 JS 侧的同一个console.log。这正是“一个 JS 函数、多个 Rust 签名”的实现基础。- 多态绑定:
console.log接受任意类型与任意数量的参数,因此示例用&str、u32、两个&str三种签名分别声明,编译后分别对应console.log的不同调用形态。
方案二:用 macro_rules! 实现 println! 风格的日志宏
第二个函数using_a_macro()演示了宏封装(src/lib.rs):
macro_rules! console_log { // 复用了 bare_bones 里导入的 log 函数 ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) } fn using_a_macro() { console_log!("Hello {}!", "world"); console_log!("Let's print some numbers..."); console_log!("1 + 3 = {}", 1 + 3); }宏的原理是:把任意 token 序列交给format_args!完成格式化(支持{}占位符、表达式求值等println!的语法),再.to_string()成&str传给方案一导入的log函数。
为什么需要这样一个宏?源码注释给出了明确原因:println!在 Wasm 目标上并不真正可用——标准库当前会把输出“吞掉”,因此想要println!式的调试体验,就得自己造一个类似的宏。这个宏恰好复用了方案一里导入的log,说明两种方案是层层递进、可组合的关系。
方案三:直接用 web-sys 的 console 模块
第三种方案最省事:web-syscrate 已经把console的所有方法都绑定好了(src/lib.rs):
fn using_web_sys() { use web_sys::console; console::log_1(&"Hello using web-sys".into()); let js: JsValue = 4.into(); console::log_2(&"Logging arbitrary values looks like".into(), &js); }这里用到了web-sys的按参数个数重载约定:log_1接收 1 个参数,log_2接收 2 个参数。查看web-sys的生成源码可以看到完整的重载族(gen_console.rs):
pub fn log_0(); pub fn log_1(data_1: &::wasm_bindgen::JsValue); pub fn log_2(data_1: &::wasm_bindgen::JsValue, data_2: &::wasm_bindgen::JsValue); pub fn log_3(/* ... */); // ... 一直到 log_7这些函数在生成的extern "C"块中同样由#[wasm_bindgen(js_namespace = "console", js_name = "log")]标注(gen_console.rs),与方案一的手写方式底层机制完全一致,只是由 webidl 代码生成器替你写好了。
需要特别注意的是:web-sys的所有 API 都按 feature 开关编译。console方法必须显式启用consolefeature,示例的 Cargo.toml 中即为:
[dependencies] wasm-bindgen = { path = "../../" } web-sys = { path = "../../crates/web-sys", features = ['console'] }之所以需要 feature 开关,是因为web-sys生成的 Web API 绑定体量巨大(仓库中 crates/web-sys/src 下有上千个.rs文件),按需编译才能控制体积与编译时间。
参数类型转换:&str / 数字如何变成 JsValue
方案三中的.into()值得单独解释:log_1、log_2的参数类型是&JsValue,所以传入的&str("Hello using web-sys")和数字(4) 都要先转换成JsValue。Rust 侧的&str在转换后是 JS 字符串,数字4变成 JS Number,日志输出效果与直接调用console.log("...", 4)一致。
这种转换能力来自wasm-bindgen的Into<JsValue>实现体系——基础类型、&str、String、数值类型、bool等都实现了到JsValue的转换,相关实现可查阅 convert/impls.rs。这也解释了为什么方案一里可以直接传&str/u32:#[wasm_bindgen]会在 ABI 层面自动完成 Rust 类型与 JS 值之间的翻译。
工程配置:Cargo.toml 与 webpack 打包
示例的工程配置同样值得一提:
Cargo.toml(examples/console_log/Cargo.toml):
[lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = { path = "../../" } web-sys = { path = "../../crates/web-sys", features = ['console'] }crate-type = ["cdylib"]是 wasm-bindgen 项目的标准配置,告诉编译器生成可供 Web 加载的动态库形态 Wasm 产物;wasm-bindgen与web-sys均以仓库内相对路径引用,这是本仓库示例目录的通用做法。
webpack 配置(webpack.config.js):
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); module.exports = { entry: './index.js', output: { path: path.resolve(__dirname, '..', 'dist', 'console_log'), filename: 'index.js', }, plugins: [ new HtmlWebpackPlugin(), new WasmPackPlugin({ crateDirectory: __dirname }), ], mode: 'development', experiments: { asyncWebAssembly: true } };核心机制:
WasmPackPlugin在 webpack 构建时自动调用wasm-pack,把当前目录(crateDirectory: __dirname,即console_log的 Rust crate)编译成pkg/目录下的 Wasm 产物;experiments.asyncWebAssembly: true启用 webpack 5 的异步 WebAssembly 支持,让import('./pkg')按需异步加载 Wasm 模块;- 入口 index.js 极为精简——
import('./pkg').catch(console.error),加载成功与否都交给控制台日志来体现(加载失败打印console.error,加载成功则由 Rust 侧run()输出三条日志),与示例主题完美呼应。
构建与运行
示例 README 给出的运行方式只有一条命令(README.md):
$ npm run serve对应 package.json 中的脚本"serve": "webpack serve"(构建脚本为"build": "webpack")。执行后访问 http://localhost:8080,打开浏览器开发者工具的控制台即可看到日志输出。由于#[wasm_bindgen(start)]入口在模块加载时自动运行,无需任何额外交互。
运行前需要确保环境具备:Rust 工具链(含wasm32-unknown-unknown目标)与wasm-pack、Node.js 与 npm/pnpm。依赖(@wasm-tool/wasm-pack-plugin、webpack、webpack-dev-server等)通过仓库根目录的 pnpm workspace 统一管理版本(pnpm-workspace.yaml),在示例目录执行npm install(或pnpm install)即可安装。
三种方案的取舍与典型应用场景
结合示例本身与web-sys的实现,可以给出如下选型建议:
| 方案 | 优点 | 局限 | 适用场景 |
|---|---|---|---|
手写extern "C"绑定 | 零依赖、完全可控 | 需手写js_namespace/js_name,签名与 JS 侧一致性全靠自己保证 | 只用到一两个 JS 函数的小项目、学习 wasm-bindgen 绑定原理 |
console_log!宏 | 提供println!式格式化语法,调试体验好 | 底层仍依赖手写的log绑定;无类型重载 | 需要在 Wasm 里频繁打印格式化调试信息 |
web-sys的console | 覆盖全部console.*方法、重载完备、官方维护 | 需要额外依赖并开启consolefeature,编译体积略增 | 生产项目、需要console::time/assert/warn等更多 API 的场景 |
从源码结构看,web-sys的console模块并不止log一族——gen_console.rs 中同样包含assert、count、debug、error、info、time、warn等全套 console API(如assert_with_condition_and_data一族),且同样遵循按参数个数与类型拆分的重载命名规则。因此一旦项目引入了web-sys,建议直接使用方案三,并配合console_log!这类宏获得格式化语法,二者互补。
小结
console_log示例虽然短小,却完整覆盖了 wasm-bindgen 调用 JS 函数的三种典型姿势:手写绑定(js_namespace+js_name)、macro_rules!宏封装、以及直接复用web-sys的官方绑定(按参数个数的重载族 + feature 开关)。理解这个示例,也就理解了 wasm-bindgen 一切 JS 导入绑定的底层骨架。想继续深入,可以对照阅读 js_namespace 属性文档、web-sys 使用指南以及同目录下的 hello_world 示例。
- 开发工具
【免费下载链接】wasm-bindgen
Facilitating high-level interactions between Wasm modules and JavaScript
相关推荐
wasm-bindgen 实战:从 `console.log` 手动绑定到 `println!` 风格日志宏的完整指南
wasm bindgen 实战:从 console.log 手动绑定到 println! 风格日志宏的完整指南 本指南围绕 wasm bindgen 官方示例
开发工具wasm-bindgen 官方示例全览:基于 `wasm-bindgen`、`js-sys` 与 `web-sys` 的实战入门指南
wasm bindgen 官方示例全览:基于 wasm bindgen 、 js sys 与 web sys 的实战入门指南 wasm bindgen 仓库的
开发工具wasm-bindgen 实战:用 web-sys 实现 requestAnimationFrame 循环
wasm bindgen 实战:用 web sys 实现 requestAnimationFrame 循环 requestAnimationFrame 是浏览器
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考