Embassy 在 WebAssembly 上运行:用 Rust async 编写浏览器端 ticker 应用的完整实战指南
嵌入式物联网异步编程【免费下载链接】embassyModern embedded framework, using Rust and async.项目地址https://gitcode.com/gh_mirrors/em/embassy点击查看免费下载本指南基于当前仓库中的 examples/wasm 示例讲解如何将 Embassy 的异步运行时embassy-executor与时间驱动embassy-time编译为 WASM并在浏览器中运行你将学会使用wasm-pack构建、启动本地 Web 服务器、通过 ES Module 加载产物并理解 Embassy 的 executor 如何通过 Promise 调度到 JavaScript 事件循环、Timer::after_secs又如何借助setTimeout工作。读完本文你可以在浏览器中直接复用熟悉的上层 async API为嵌入式逻辑开发网页端演示或仿真工具。为什么 Embassy 可以跑在浏览器里Embassy 是一个面向嵌入式系统的 Rust async 框架但它的核心executor、同步原语、时间抽象在架构上是平台无关的。仓库中embassy-executor、embassy-time、embassy-sync等 crate 通过 feature 机制选择不同的底层实现在 MCU 上对应 Cortex-M/RISC-V 平台和硬件定时器在浏览器中则对应 WASM 平台和setTimeout。embassy-executor的 Cargo.toml 中定义了多种“flavor”其中之一即为{ name wasm, target wasm32-unknown-unknown, features [platform-wasm, executor-thread] }对应的 feature 定义为## WASM platform. platform-wasm [_platform, dep:wasm-bindgen, dep:js-sys]这意味着启用platform-wasm时executor 通过wasm-bindgen与js-sys对接浏览器运行时。因此同一个#[embassy_executor::task]异步任务既可以在 MCU 上跑也可以不加修改地在浏览器 WASM 中运行——这正是本示例的价值所在。环境准备安装 wasm-pack示例使用官方 CLI 工具wasm-pack进行构建。README 指定的版本为 0.12.1安装命令如下cargo install wasm-pack --version 0.12.1说明wasm-pack负责将 Rust 编译产物打包成可供网页直接引用的pkg目录包含.jsglue 代码与.wasm二进制。请确保你的 Rust 工具链已安装wasm32-unknown-unknown目标可通过rustup target add wasm32-unknown-unknown添加。示例的Cargo.toml中同样声明了构建目标见 package.metadata.embassytarget wasm32-unknown-unknown。示例项目结构一览本示例目录包含四个文件文件作用Cargo.toml定义 crate 类型与 WASM 相关依赖/featuresrc/lib.rsEmbassy 异步任务与入口实现index.html宿主页面以 ES Module 加载编译产物README.md构建与运行说明本文基础由于编译目标是浏览器代码以cdylib形式导出。关键配置如下[lib] crate-type [cdylib] [dependencies] embassy-sync { version 0.8.0, path ../../embassy-sync, features [log] } embassy-executor { version 0.10.0, path ../../embassy-executor, features [platform-wasm, executor-thread, log] } embassy-time { version 0.5.1, path ../../embassy-time, features [log, wasm] }这里每个 feature 都对应一条 WASM 支持链路embassy-executor的platform-wasm选择 platform/wasm.rs 作为 executor 平台实现embassy-executor的executor-thread启用基于线程模型这里即 JS 事件循环驱动的 executor同时该文件开头明确限制了executor-interrupt与platform-wasm不能共存#[cfg(feature executor-interrupt)] compile_error!(executor-interrupt is not supported with platform-wasm.);embassy-time的wasm启用 driver_wasm.rs 时间驱动其 feature 定义为见 embassy-time/Cargo.tomlwasm [dep:wasm-bindgen, dep:js-sys, dep:web-time, tick-hz-1_000_000, dep:embassy-time-queue-utils]wasm-logger、web-sys、log用于在浏览器控制台输出日志并操作 DOM。源码解析一个每秒打印一次的 ticker 任务核心逻辑在 src/lib.rs 中分为任务定义与入口两部分。任务定义操作 DOM 的异步循环#[embassy_executor::task] async fn ticker() { let window web_sys::window().expect(no global window exists); let mut counter 0; loop { let document window.document().expect(should have a document on window); let list document.get_element_by_id(log).expect(should have a log element); let li document.create_element(li).expect(error creating list item element); li.set_text_content(Some(format!(tick {}, counter))); list.append_child(li).expect(error appending list item); log::info!(tick {}, counter); counter 1; Timer::after_secs(1).await; } }#[embassy_executor::task]宏把一个async fn变成可被 spawn 的 Embassy 任务。任务内部通过web-sys操作浏览器 DOM向页面中idlog的ul列表追加li元素同时用log::info!输出到浏览器控制台。关键点在于Timer::after_secs(1).await——这不是忙等而是挂起当前任务并交给时间驱动1 秒后由驱动重新唤醒任务继续执行。入口初始化日志并启动任务#[embassy_executor::main] async fn main(spawner: Spawner) { wasm_logger::init(wasm_logger::Config::default()); spawner.spawn(ticker().unwrap()); }#[embassy_executor::main]在 WASM 平台下实际展开为main_wasm宏见 embassy-executor-macros/src/lib.rs 中pub fn main_wasm的定义它创建 WASM executor 实例把函数体作为初始任务运行并将Spawner传入以便 spawn 更多任务。整个流程非常接近嵌入式写法wasm_logger::init对应 MCU 上的日志初始化spawner.spawn(ticker())对应在嵌入式 main 中启动任务。也就是说这套代码结构完全可以移植到实际的 no_std 目标上。构建wasm-pack build在examples/wasm目录下执行wasm-pack build --target web--target web会生成面向浏览器、以 ES Module 形式导入的产物输出到pkg/子目录其中关键文件包括embassy_wasm_example.jsglue 代码导出默认的初始化函数embassy_wasm_example_bg.wasm编译出的 WebAssembly 二进制embassy_wasm_example.d.tsTypeScript 类型声明可选。构建完成后pkg/目录会出现在examples/wasm下供 index.html 引用。运行启动本地 Web 服务器并在浏览器中观察由于浏览器安全策略不允许从file://直接加载模块化 WASMREADME 建议启动一个本地 HTTP 服务器。在examples/wasm目录下执行python -m http.server然后打开浏览器访问http://127.0.0.1:8000此时应看到页面出现h1Log/h1标题和一个idlog的列表列表每秒新增一个tick 0、tick 1、tick 2……条目同时在浏览器开发者工具Console中可以看到wasm-logger输出的对应log::info!消息。若端口被占用可换用python -m http.server 8080等端口。页面如何加载 WASM 模块index.html 使用 ES Module 语法加载编译产物script typemodule import init from ./pkg/embassy_wasm_example.js; await init(); /script其中init是 wasm-bindgen 生成的初始化函数负责实例化.wasm二进制并执行 Rust 侧入口即main_wasm。页面中的列表容器与代码中get_element_by_id(log)对应h1Log/h1 div ul idlog /ul /div底层原理executor 如何“嫁接”到 JS 事件循环理解这个示例的关键在于 embassy-executor/src/platform/wasm.rs 的实现。WASM 平台没有线程与中断Executor 采用“由 JS 事件循环反复轮询”的模型每个 WASM executor 持有一个WasmContext其中保存了一个js_sys::Promise和一个ClosureExecutor::new()通过Box::leak获得static上下文并把上下文指针交给底层的raw::Executor当任务需要被唤醒时WasmPender::pend被调用它执行promise.then(closure)将一次轮询调度到微任务队列——即“借用”浏览器的 Promise 机制作为调度信号impl crate::pender::Pender for WasmPender { fn pend(context: *mut ()) { let signaler: static WasmContext unsafe { std::mem::transmute(context) }; let _ signaler.promise.then(unsafe { signaler.closure.as_mut() }); } }Executor::start()将闭包写入WasmContext闭包内部调用executor.poll()即每次微任务回调都会推进一次任务轮询。因此Timer::after_secs(1).await的完整链路是任务挂起 → 时间驱动注册一个setTimeout回调见下文→ 1 秒后回调触发 → 唤醒任务 → 通过Promise.then安排poll→ executor 推进ticker任务到下一个tick。时间驱动setTimeout 背后的实现embassy-time在 WASM 下的时间驱动位于 embassy-time/src/driver_wasm.rs。它通过wasm_bindgen直接对接浏览器的定时器 API#[wasm_bindgen] extern C { fn setTimeout(closure: Closuredyn FnMut(), millis: u32) - f64; fn clearTimeout(token: f64); }驱动内部使用web_time::Instant维护单调时钟借助embassy_time_queue_utils::Queue管理定时器队列当某个Timer::after_secs(1)到期时回调触发队列中对应任务再由 pender 唤醒。这也是为什么embassy-time的wasmfeature 需要tick-hz-1_000_000与web-time、js-sys等依赖——时间基准确到微秒级别而真实唤醒仍依赖浏览器定时器。限制与注意事项从 platform/wasm.rs 和仓库配置可以总结出几条使用前提不支持中断驱动 executorexecutor-interruptfeature 在platform-wasm下会触发编译错误只能使用executor-thread模型单线程模型所有任务运行在浏览器主线程的事件循环中任务体内不应包含长时间同步阻塞操作否则会卡住页面与定时器回调需要 Web 环境时间驱动依赖setTimeout等浏览器 API因此该示例面向浏览器而非纯 Node 的裸执行环境如需 Node 环境需另行配置驱动版本锁定README 明确要求wasm-pack版本为 0.12.1安装其他版本时产物结构与初始化方式可能发生变化日志通过wasm-logger输出到 Console若启用logfeature 但未初始化 loggerlog::info!将静默丢弃。小结通过本示例你可以看到Embassy 的抽象层让同一份异步代码横跨嵌入式与浏览器两种截然不同的执行环境。构建命令wasm-pack build --target webpython -m http.server即可在数秒内跑起一个每秒输出 tick 的浏览器应用而其底层executor 借助Promise.then挂入 JS 微任务队列时间驱动借助setTimeout实现延迟唤醒。理解这两条链路的实现细节platform/wasm.rs、driver_wasm.rs你就能在此基础上扩展出更复杂的浏览器端仿真、可视化或协议调试工具并复用embassy-sync、embassy-net等上层组件的能力。赞分享嵌入式物联网异步编程【免费下载链接】embassyModern embedded framework, using Rust and async.项目地址https://gitcode.com/gh_mirrors/em/embassy点击查看免费下载相关推荐FastAPI文档暗黑模式prefers-color-scheme智能主题切换指南 FastAPI文档暗黑模式prefers color scheme智能主题切换指南 FastAPI作为高性能的现代Python Web框架不仅提供了卓后端Web框架API设计Avalonia WebAssembly应用浏览器端运行Avalonia WebAssembly应用浏览器端运行 概述 Avalonia UI作为.NET生态系统中领先的跨平台UI框架现已全面支持WebAssem跨平台桌面应用UI组件fastText 浏览器实战使用 WebAssembly 在浏览器中编译、训练与预测完整指南fastText 浏览器实战使用 WebAssembly 在浏览器中编译、训练与预测完整指南 fastText 是一个用于高效学习词表示word repreNLP机器学习上一篇Snippai常见问题解答新手必看的20个实用技巧下一篇Chrome MCP Server终极后台任务管理指南OffscreenManager与Service Worker深度解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考