☰
wasm-bindgen 中 char 类型的跨语言传递:以 examples/char 为例从用法到 ABI 实现
2026/10/6 12:29:26 网站建设 项目流程
  • 开发工具

【免费下载链接】wasm-bindgen

Facilitating high-level interactions between Wasm modules and JavaScript

项目地址:https://gitcode.com/gh_mirrors/wa/wasm-bindgen
点击查看免费下载

本文以 wasm-bindgen 仓库自带的examples/char示例为线索,系统讲解 Rustchar类型如何与 JavaScript 字符串互相传递:从可直接运行的示例工程、Counter类的实战用法,到源码层基于u32的 ABI 编码、以及针对 Unicode 标量值边界(包括代理项与非法输入)的完整测试验证。读完本文,你将掌握在 wasm-bindgen 项目中安全、高效地跨边界传递单字符数据,并理解其底层机制。

示例概览:一个关于字符的计数器应用

examples/char是 wasm-bindgen 官方示例之一,主题是"与char类型打交道"(Working with thechartype)。它构建了一个网页应用:每点击一次按钮,就从一份包含数百个 Unicode 字符的列表中随机取出一个字符作为Counter的键(key),并展示当前计数,点击+按钮即可对计数加一。

与仓库中其他示例一样,该工程同时包含 Rust 与 JavaScript 两侧代码,覆盖了完整的"构建 → 加载 → 交互"链路:

文件作用
examples/char/src/lib.rsRust 侧:定义Counter结构体与字符相关的公开 API
examples/char/index.jsJS 侧:生成随机字符、调用 Rust 方法、渲染 DOM
examples/char/index.html页面骨架与样式
examples/char/chars-list.js预置的 Unicode 字符池(含 ASCII、拉丁扩展、希腊文、西里尔文、emoji 等)
examples/char/Cargo.tomlRust 工程配置(cdylib)
examples/char/package.jsonnpm 脚本与 devDependencies
examples/char/webpack.config.jswebpack + wasm-pack 插件配置

注意:示例对应仓库内的根 README 位于 examples/char/README.md,其中说明示例编译后的在线演示与文档托管在 wasm-bindgen 项目站点的exbuild/char/与examples/char.html页面。

本地运行方式

原文档给出的运行方式非常简洁,只需两步:

$ npm run serve

serve脚本定义在 examples/char/package.json 中,等价于webpack serve,会同时启动 webpack dev server 与 wasm-pack 编译流程。随后在浏览器中访问:

http://localhost:8080

即可看到示例页面:一个add counter按钮,点击后页面中会出现一个以随机字符命名的计数器卡片。

如果想要一次性产出静态构建产物,也可以使用同文件中的另一个脚本:

$ npm run build

它执行webpack,把结果输出到dist/char/目录(见下方构建配置小节)。

Rust 侧:用 char 作为结构体字段与参数

示例的 Rust 代码非常集中地展示了char在 wasm-bindgen 中的三种典型用法:结构体字段、构造函数参数、getter 与 setter 方法。核心实现在 examples/char/src/lib.rs:

use wasm_bindgen::prelude::*; // lifted from the `console_log` example #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] fn log(s: &str); } #[wasm_bindgen] #[derive(Debug)] pub struct Counter { key: char, count: i32, } #[wasm_bindgen] impl Counter { pub fn new(key: char, count: i32) -> Counter { log(&format!("Counter::new({key}, {count})")); Counter { key, count } } pub fn key(&self) -> char { log("Counter.key()"); self.key } pub fn count(&self) -> i32 { log("Counter.count"); self.count } pub fn increment(&mut self) { log("Counter.increment"); self.count += 1; } pub fn update_key(&mut self, key: char) { self.key = key; } }

从这份代码可以归纳出char在 wasm-bindgen 中的使用要点:

  • 公开字段:Counter.key是char类型,在 JS 侧读取key()时返回的是长度为 1 的 JS 字符串(单码点字符串);
  • 参数与返回值:new(key: char, ...)、update_key(&mut self, key: char)、key(&self) -> char展示了char作为入参、出参、可写状态的全流程;
  • 与i32等原生类型混用:Counter::new同时接收char与i32,说明char在 ABI 层面与数字类型一样走"按值传递"路径(详见下文 ABI 小节);
  • 日志辅助:示例从console_log示例中"借"来了#[wasm_bindgen(js_namespace = console)] fn log(s: &str),方便在浏览器控制台观察方法调用过程。

JS 侧:随机字符池与 DOM 交互

JS 侧的工作分两部分:维护字符池、调用 Rust 生成计数器。

字符池:覆盖广泛 Unicode 范围的测试素材

examples/char/chars-list.js 导出chars数组,内容从 ASCII 可打印字符(!、#、$…)一直延伸到拉丁扩展、希腊字母、西里尔字母,以及大量 emoji(如🕧、😀)。这一设计并非巧合——字符池越宽,越能覆盖char跨边界传递时可能遇到的各种码位区间,包括:

  • 1 字节码位(ASCII,如A、z);
  • 2 字节码位(如¡、Ä);
  • 3 字节码位(如Ā、ϑ);
  • 4 字节码位(emoji,如😀)。

调用 Rust 并渲染

examples/char/index.js 的核心逻辑如下:

import { chars } from './chars-list.js'; let imp = import('./pkg'); let mod; let counters = []; imp.then(wasm => { mod = wasm; addCounter(); document.getElementById('add-counter').addEventListener('click', () => addCounter()); }).catch(console.error); function addCounter() { let ctr = mod.Counter.new(randomChar(), 0); counters.push(ctr); update(); } function randomChar() { let idx = Math.floor(Math.random() * (chars.length - 1)); return chars.splice(idx, 1)[0]; // 取出后从池中移除,避免重复 }

关键点:

  • 异步加载:import('./pkg')动态导入 wasm-pack 生成的模块,pkg目录由WasmPackPlugin自动产出;
  • 构造 Rust 对象:mod.Counter.new(randomChar(), 0)直接以 JS 字符串调用 Rust 构造函数,说明 JS 侧不需要任何显式转换——一个字符的 JS 字符串就是 Rustchar的天然表示;
  • 读取字符:counter.key()返回的字符串被用于newCounter(key, value, cb)中的document.createTextNode('Counter ' + key),直接拼入 DOM 标题;
  • 更新计数:+按钮回调调用counter.increment()后刷新列表,对应 Rust 侧&mut self方法。

update()中还有一个值得注意的细节:渲染前会保留#add-counter按钮并移除其余子节点,实现整块重绘;每个计数卡片由newCounter创建,其中newField用两个<span>分别展示字段名与字段值。

底层原理:char 在 wasm-bindgen 中的 ABI 编码

char之所以能在 JS 字符串与 Rust 标量之间无缝切换,是因为 wasm-bindgen 为它定义了明确的 ABI 表示。查看 src/convert/impls.rs 的源码:

impl IntoWasmAbi for char { type Abi = u32; #[inline] fn into_abi(self) -> u32 { self as u32 } } impl FromWasmAbi for char { type Abi = u32; #[inline] unsafe fn from_abi(js: u32) -> char { // SAFETY: Checked in bindings. char::from_u32_unchecked(js) } }

从源码结构可以提炼出以下事实:

  1. char的 ABI 类型是u32:Rust 侧传入时把char直接按码位(Unicode Scalar Value)强转为u32;返回时由char::from_u32_unchecked还原。整个跨边界过程是单个 32 位整数,不涉及内存拷贝或指针。
  2. 校验发生在绑定层:from_abi的 SAFETY 注释明确写着"在 bindings 中检查"。也就是说,wasm-bindgen 生成的胶水代码负责把 JS 侧的单字符字符串先转换成合法的u32码位,再交给from_abi。非法输入(例如孤立的 UTF-16 代理项、非字符串类型)会在这一层被拦截并抛出可读的错误信息(见下文测试)。
  3. 与i32等原生类型同路径:char同bool、数字一样实现了IntoWasmAbi/FromWasmAbi的按值传递协议,这正是它能够作为结构体字段、构造参数自由进出的基础。

顺带一提,bool的 ABI 也是u32(见 src/convert/impls.rs),说明"用一个 32 位整数承载小型标量"是 wasm-bindgen 的通用设计。

边界行为:测试用例如何验证 char 往返

仓库的正式测试位于 tests/wasm/char.rs(Rust 侧)与 tests/wasm/char.js(JS 侧),它们把char的行为边界钉得清清楚楚。

往返正确性

tests/wasm/char.js 覆盖了多种码位宽度的字符:

assert.strictEqual(wasm.letter(), 'a'); assert.strictEqual(wasm.face(), '😀'); assert.strictEqual(wasm.rust_identity(''), '\u0000'); // U+0000 也能传递 assert.strictEqual(wasm.rust_identity('Ղ'), 'Ղ'); assert.strictEqual(wasm.rust_identity('ҝ'), 'ҝ'); assert.strictEqual(wasm.rust_identity('Δ'), 'Δ'); assert.strictEqual(wasm.rust_identity('䉨'), '䉨'); assert.strictEqual(wasm.rust_js_identity('㊻'), '㊻'); wasm.rust_letter('a'); wasm.rust_face('😀');

而 Rust 侧的 tests/wasm/char.rs 提供了对应的rust_identity、rust_js_identity、letter、face、rust_letter、rust_face,其中:

  • letter() -> char返回'a',face() -> char返回'😀'(4 字节 emoji),验证了 Rust → JS 方向;
  • rust_identity(c: char) -> char原样返回,验证 JS → Rust → JS 的完整往返;
  • rust_js_identity(c: char) -> char先调用 JS 侧js_identity再返回,验证 Rust → JS → Rust 的反向链路。

可选值语义

tests/wasm/char.js 还验证了Option<char>:

assert.strictEqual(wasm.rust_option_identity(undefined), undefined); assert.strictEqual(wasm.rust_option_identity(null), undefined); assert.strictEqual(wasm.rust_option_identity(''), '\u0000'); assert.strictEqual(wasm.rust_option_identity('\u0000'), '\u0000');

这说明undefined/null会映射为None,而空字符串''与'\u0000'都会正确映射为Some('\u0000')并原样返回——JS 空串在这里被当作"含有一个 U+0000 的字符串"处理。

非法输入的错误信息

tests/wasm/char.js 专门断言了两类非法输入:

assert.throws(() => wasm.rust_identity(55357), /c.codePointAt is not a function/); assert.throws(() => wasm.rust_identity('\uD83D'), /expected a valid Unicode scalar value, found 55357/); assert.throws(() => wasm.rust_option_identity('\uD83D'), /expected a valid Unicode scalar value, found 55357/);
  • 传入数字(如55357)时,绑定代码尝试调用字符串方法codePointAt失败,抛出c.codePointAt is not a function;
  • 传入孤立的 UTF-16 代理项('\uD83D'是😀的高代理位)时,因为其码位(55357)落在代理区(D800–DFFF),不是合法的 Unicode 标量值,绑定层抛出expected a valid Unicode scalar value, found 55357。

这两条断言直观印证了上文"校验在 bindings 层"的实现事实:wasm-bindgen 不会把非法码位静默塞进 Rust 侧,而是显式报错,保证char::from_u32_unchecked的 SAFETY 前提始终成立。

工程配置:让示例跑起来的三个配置文件

Rust 侧:cdylib

examples/char/Cargo.toml 的配置非常标准:

[package] authors = ["The wasm-bindgen Developers"] edition = "2021" name = "char" publish = false version = "0.0.0" [lib] crate-type = ["cdylib"] [dependencies] wasm-bindgen = { path = "../../" }

要点:

  • crate-type = ["cdylib"]是 wasm-bindgen 工程的标准要求,保证编译产物是可供 JS 加载的 wasm 动态库;
  • wasm-bindgen = { path = "../../" }通过相对路径引用仓库根目录的 wasm-bindgen 本体,即 Cargo.toml;
  • publish = false表明这是仓库内部的演示 crate。

npm 侧:webpack + wasm-pack 插件

examples/char/package.json 只定义了两个脚本:

{ "scripts": { "build": "webpack", "serve": "webpack serve" }, "devDependencies": { "@wasm-tool/wasm-pack-plugin": "catalog:", "html-webpack-plugin": "catalog:", "webpack": "catalog:", "webpack-cli": "catalog:", "webpack-dev-server": "catalog:" } }

catalog:表示依赖版本由仓库工作区的 pnpm catalog 统一管理,避免各示例版本漂移。

构建管道:WasmPackPlugin

examples/char/webpack.config.js 是整套构建的核心:

const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const webpack = require('webpack'); const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); module.exports = { entry: './index.js', output: { path: path.resolve(__dirname, '..', 'dist', 'char'), filename: 'index.js', }, plugins: [ new HtmlWebpackPlugin({ template: "index.html" }), new WasmPackPlugin({ crateDirectory: __dirname }), ], mode: 'development', experiments: { asyncWebAssembly: true } };
  • WasmPackPlugin以crateDirectory: __dirname(即examples/char)为 Rust crate 根目录,在 webpack 构建时自动执行 wasm-pack 编译并生成./pkg,因此index.js里import('./pkg')才能工作;
  • experiments.asyncWebAssembly: true开启 Webpack 5 的异步 wasm 支持,与import('./pkg')的异步加载方式匹配;
  • HtmlWebpackPlugin以index.html为模板注入打包产物;
  • 输出统一放到仓库级dist/char/目录,与仓库中多个示例共用dist/的结构一致。

小结

examples/char虽小,却是理解 wasm-bindgen 字符传递机制的理想标本:

  1. 用法层面:char可作为公开结构体字段、构造参数、getter/setter 返回值,JS 侧以单码点字符串直接互操作,无需手动编解码;
  2. ABI 层面:char以u32码位形式跨边界传递(src/convert/impls.rs),Rust 侧用char as u32与char::from_u32_unchecked完成双向转换;
  3. 安全层面:绑定层负责校验输入,非法码位(孤立代理项、非字符串)会被显式拒绝并抛出错误,测试用例(tests/wasm/char.rs、tests/wasm/char.js)为这一保证提供了完整证据;
  4. 工程层面:cdylib+WasmPackPlugin+asyncWebAssembly的组合,是仓库所有示例通用的"编译即运行"模板。

如果你需要在 Rust wasm 模块中传递单个 Unicode 字符(例如键盘按键、游戏输入、符号标识),直接照抄本示例的Counter写法即可:Rust 侧用char,JS 侧用单字符字符串,剩下的交给 wasm-bindgen。

  • 开发工具

【免费下载链接】wasm-bindgen

Facilitating high-level interactions between Wasm modules and JavaScript

项目地址:https://gitcode.com/gh_mirrors/wa/wasm-bindgen
点击查看免费下载
上一篇:Turso(Limbo)代码质量指南:生产级 SQL 数据库的 Rust 正确性工程实践
下一篇:LayerDivider:免费图片转PSD分层开源工具,一张插画自动拆成可编辑图层

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询