From e24528051b9ed26157bee93e32e37e7b06b8f4cc Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Mon, 13 Sep 2021 14:35:40 +0200 Subject: [PATCH] Add WASM support for executor * Adds an executor for WASM runtimes based on wasm_bindgen. * Add time driver based on JS time handling. * Add example that can run in browser locally. * Update to critical-section version that supports 'std' flag --- .github/workflows/rust.yml | 2 + embassy-macros/Cargo.toml | 1 + embassy-macros/src/lib.rs | 79 +++++++++++++++++ embassy-nrf/Cargo.toml | 2 +- embassy-rp/Cargo.toml | 2 +- embassy-stm32/Cargo.toml | 2 +- embassy/Cargo.toml | 8 +- embassy/src/executor/arch/wasm.rs | 74 ++++++++++++++++ embassy/src/executor/mod.rs | 3 +- embassy/src/executor/raw/mod.rs | 2 +- embassy/src/lib.rs | 2 +- embassy/src/time/driver_wasm.rs | 135 ++++++++++++++++++++++++++++++ embassy/src/time/mod.rs | 3 + examples/stm32h7/Cargo.toml | 2 +- examples/wasm/Cargo.toml | 17 ++++ examples/wasm/README.md | 26 ++++++ examples/wasm/index.html | 25 ++++++ examples/wasm/src/lib.rs | 37 ++++++++ 18 files changed, 414 insertions(+), 8 deletions(-) create mode 100644 embassy/src/executor/arch/wasm.rs create mode 100644 embassy/src/time/driver_wasm.rs create mode 100644 examples/wasm/Cargo.toml create mode 100644 examples/wasm/README.md create mode 100644 examples/wasm/index.html create mode 100644 examples/wasm/src/lib.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 3ddda1a3..79a5ad79 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -94,6 +94,8 @@ jobs: target: thumbv6m-none-eabi - package: examples/stm32g0 target: thumbv6m-none-eabi + - package: examples/wasm + target: wasm32-unknown-unknown steps: - uses: actions/checkout@v2 with: diff --git a/embassy-macros/Cargo.toml b/embassy-macros/Cargo.toml index b56f0f72..95512123 100644 --- a/embassy-macros/Cargo.toml +++ b/embassy-macros/Cargo.toml @@ -18,3 +18,4 @@ nrf = [] stm32 = [] rp = [] std = [] +wasm = [] diff --git a/embassy-macros/src/lib.rs b/embassy-macros/src/lib.rs index ab327ace..708eed4c 100644 --- a/embassy-macros/src/lib.rs +++ b/embassy-macros/src/lib.rs @@ -450,3 +450,82 @@ pub fn main(args: TokenStream, item: TokenStream) -> TokenStream { }; result.into() } + +#[cfg(feature = "wasm")] +#[proc_macro_attribute] +pub fn main(args: TokenStream, item: TokenStream) -> TokenStream { + let macro_args = syn::parse_macro_input!(args as syn::AttributeArgs); + let task_fn = syn::parse_macro_input!(item as syn::ItemFn); + + let macro_args = match MainArgs::from_list(¯o_args) { + Ok(v) => v, + Err(e) => { + return TokenStream::from(e.write_errors()); + } + }; + + let embassy_path = macro_args.embassy_prefix.append("embassy"); + + let mut fail = false; + if task_fn.sig.asyncness.is_none() { + task_fn + .sig + .span() + .unwrap() + .error("task functions must be async") + .emit(); + fail = true; + } + if !task_fn.sig.generics.params.is_empty() { + task_fn + .sig + .span() + .unwrap() + .error("main function must not be generic") + .emit(); + fail = true; + } + + let args = task_fn.sig.inputs.clone(); + + if args.len() != 1 { + task_fn + .sig + .span() + .unwrap() + .error("main function must have one argument") + .emit(); + fail = true; + } + + if fail { + return TokenStream::new(); + } + + let task_fn_body = task_fn.block.clone(); + + let embassy_path = embassy_path.path(); + let embassy_prefix_lit = macro_args.embassy_prefix.literal(); + + let result = quote! { + #[#embassy_path::task(embassy_prefix = #embassy_prefix_lit)] + async fn __embassy_main(#args) { + #task_fn_body + } + + use wasm_bindgen::prelude::*; + + #[wasm_bindgen(start)] + pub fn main() -> Result<(), JsValue> { + static EXECUTOR: #embassy_path::util::Forever<#embassy_path::executor::Executor> = #embassy_path::util::Forever::new(); + let executor = EXECUTOR.put(#embassy_path::executor::Executor::new()); + + executor.start(|spawner| { + spawner.spawn(__embassy_main(spawner)).unwrap(); + }); + + Ok(()) + } + }; + result.into() +} diff --git a/embassy-nrf/Cargo.toml b/embassy-nrf/Cargo.toml index f4284013..edc1e22a 100644 --- a/embassy-nrf/Cargo.toml +++ b/embassy-nrf/Cargo.toml @@ -45,7 +45,7 @@ cortex-m = "0.7.3" embedded-hal = "0.2.6" embedded-dma = "0.1.2" futures = { version = "0.3.17", default-features = false } -critical-section = "0.2.1" +critical-section = "0.2.2" rand_core = "0.6.3" nrf52805-pac = { version = "0.10.1", optional = true, features = [ "rt" ] } diff --git a/embassy-rp/Cargo.toml b/embassy-rp/Cargo.toml index 77000511..1702e0df 100644 --- a/embassy-rp/Cargo.toml +++ b/embassy-rp/Cargo.toml @@ -27,7 +27,7 @@ defmt = { version = "0.2.3", optional = true } log = { version = "0.4.14", optional = true } cortex-m-rt = ">=0.6.15,<0.8" cortex-m = "0.7.3" -critical-section = "0.2.1" +critical-section = "0.2.2" rp2040-pac2 = { git = "https://github.com/embassy-rs/rp2040-pac2", rev="9ad7223a48a065e612bc7dc7be5bf5bd0b41cfc4", features = ["rt"] } #rp2040-pac2 = { path = "../../rp/rp2040-pac2", features = ["rt"] } diff --git a/embassy-stm32/Cargo.toml b/embassy-stm32/Cargo.toml index b34d5685..3758c062 100644 --- a/embassy-stm32/Cargo.toml +++ b/embassy-stm32/Cargo.toml @@ -21,7 +21,7 @@ futures = { version = "0.3.17", default-features = false, features = ["async-awa rand_core = "0.6.3" sdio-host = "0.5.0" embedded-sdmmc = { git = "https://github.com/thalesfragoso/embedded-sdmmc-rs", branch = "async", optional = true } -critical-section = "0.2.1" +critical-section = "0.2.2" bare-metal = "1.0.0" atomic-polyfill = "0.1.3" stm32-metapac = { version = "0.1.0", path = "../stm32-metapac", features = ["rt"] } diff --git a/embassy/Cargo.toml b/embassy/Cargo.toml index ae06bc19..8aac861e 100644 --- a/embassy/Cargo.toml +++ b/embassy/Cargo.toml @@ -8,6 +8,7 @@ resolver = "2" [features] default = [] std = ["futures/std", "embassy-traits/std", "time", "time-tick-1mhz", "embassy-macros/std"] +wasm = ["wasm-bindgen", "js-sys", "embassy-macros/wasm", "wasm-timer", "time", "time-tick-1mhz"] # Enable `embassy::time` module. # NOTE: This feature is only intended to be enabled by crates providing the time driver implementation. @@ -40,10 +41,15 @@ pin-project = { version = "1.0.8", default-features = false } embassy-macros = { version = "0.1.0", path = "../embassy-macros"} embassy-traits = { version = "0.1.0", path = "../embassy-traits"} atomic-polyfill = "0.1.3" -critical-section = "0.2.1" +critical-section = "0.2.2" embedded-hal = "0.2.6" heapless = "0.7.5" +# WASM dependencies +wasm-bindgen = { version = "0.2.76", features = ["nightly"], optional = true } +js-sys = { version = "0.3", optional = true } +wasm-timer = { version = "0.2.5", optional = true } + [dev-dependencies] embassy = { path = ".", features = ["executor-agnostic"] } futures-executor = { version = "0.3.17", features = [ "thread-pool" ] } diff --git a/embassy/src/executor/arch/wasm.rs b/embassy/src/executor/arch/wasm.rs new file mode 100644 index 00000000..f069ebc3 --- /dev/null +++ b/embassy/src/executor/arch/wasm.rs @@ -0,0 +1,74 @@ +use core::marker::PhantomData; +use js_sys::Promise; +use wasm_bindgen::prelude::*; + +use super::{ + raw::{self, util::UninitCell}, + Spawner, +}; + +/// WASM executor, wasm_bindgen to schedule tasks on the JS event loop. +pub struct Executor { + inner: raw::Executor, + ctx: &'static WasmContext, + not_send: PhantomData<*mut ()>, +} + +pub(crate) struct WasmContext { + promise: Promise, + closure: UninitCell>, +} + +impl WasmContext { + pub fn new() -> Self { + Self { + promise: Promise::resolve(&JsValue::undefined()), + closure: UninitCell::uninit(), + } + } +} + +impl Executor { + /// Create a new Executor. + pub fn new() -> Self { + let ctx = &*Box::leak(Box::new(WasmContext::new())); + let inner = raw::Executor::new( + |p| unsafe { + let ctx = &*(p as *const () as *const WasmContext); + let _ = ctx.promise.then(ctx.closure.as_mut()); + }, + ctx as *const _ as _, + ); + Self { + inner, + not_send: PhantomData, + ctx, + } + } + + /// Run the executor. + /// + /// The `init` closure is called with a [`Spawner`] that spawns tasks on + /// this executor. Use it to spawn the initial task(s). After `init` returns, + /// the executor starts running the tasks. + /// + /// To spawn more tasks later, you may keep copies of the [`Spawner`] (it is `Copy`), + /// for example by passing it as an argument to the initial tasks. + /// + /// This function requires `&'static mut self`. This means you have to store the + /// Executor instance in a place where it'll live forever and grants you mutable + /// access. There's a few ways to do this: + /// + /// - a [Forever](crate::util::Forever) (safe) + /// - a `static mut` (unsafe) + /// - a local variable in a function you know never returns (like `fn main() -> !`), upgrading its lifetime with `transmute`. (unsafe) + pub fn start(&'static mut self, init: impl FnOnce(Spawner)) { + unsafe { + let executor = &self.inner; + self.ctx.closure.write(Closure::new(move |_| { + executor.poll(); + })); + init(self.inner.spawner()); + } + } +} diff --git a/embassy/src/executor/mod.rs b/embassy/src/executor/mod.rs index 1f6bdd27..3ec24c13 100644 --- a/embassy/src/executor/mod.rs +++ b/embassy/src/executor/mod.rs @@ -3,7 +3,8 @@ #![deny(missing_docs)] #[cfg_attr(feature = "std", path = "arch/std.rs")] -#[cfg_attr(not(feature = "std"), path = "arch/arm.rs")] +#[cfg_attr(feature = "wasm", path = "arch/wasm.rs")] +#[cfg_attr(not(any(feature = "std", feature = "wasm")), path = "arch/arm.rs")] mod arch; pub mod raw; mod spawner; diff --git a/embassy/src/executor/raw/mod.rs b/embassy/src/executor/raw/mod.rs index 05fb2975..08de7773 100644 --- a/embassy/src/executor/raw/mod.rs +++ b/embassy/src/executor/raw/mod.rs @@ -10,7 +10,7 @@ mod run_queue; #[cfg(feature = "time")] mod timer_queue; -mod util; +pub(crate) mod util; mod waker; use atomic_polyfill::{AtomicU32, Ordering}; diff --git a/embassy/src/lib.rs b/embassy/src/lib.rs index 84714228..2eadefb0 100644 --- a/embassy/src/lib.rs +++ b/embassy/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(not(any(feature = "std", feature = "wasm")), no_std)] #![feature(generic_associated_types)] #![feature(const_fn_trait_bound)] #![feature(const_fn_fn_ptr_basics)] diff --git a/embassy/src/time/driver_wasm.rs b/embassy/src/time/driver_wasm.rs new file mode 100644 index 00000000..0a927082 --- /dev/null +++ b/embassy/src/time/driver_wasm.rs @@ -0,0 +1,135 @@ +use atomic_polyfill::{AtomicU8, Ordering}; +use std::cell::UnsafeCell; +use std::mem::MaybeUninit; +use std::ptr; +use std::sync::{Mutex, Once}; +use wasm_bindgen::prelude::*; +use wasm_timer::Instant as StdInstant; + +use crate::time::driver::{AlarmHandle, Driver}; + +const ALARM_COUNT: usize = 4; + +struct AlarmState { + token: Option, + closure: Option>, +} + +unsafe impl Send for AlarmState {} + +impl AlarmState { + const fn new() -> Self { + Self { + token: None, + closure: None, + } + } +} + +#[wasm_bindgen] +extern "C" { + fn setTimeout(closure: &Closure, millis: u32) -> f64; + fn clearTimeout(token: f64); +} + +struct TimeDriver { + alarm_count: AtomicU8, + + once: Once, + alarms: UninitCell>, + zero_instant: UninitCell, +} + +const ALARM_NEW: AlarmState = AlarmState::new(); +crate::time_driver_impl!(static DRIVER: TimeDriver = TimeDriver { + alarm_count: AtomicU8::new(0), + once: Once::new(), + alarms: UninitCell::uninit(), + zero_instant: UninitCell::uninit(), +}); + +impl TimeDriver { + fn init(&self) { + self.once.call_once(|| unsafe { + self.alarms.write(Mutex::new([ALARM_NEW; ALARM_COUNT])); + self.zero_instant.write(StdInstant::now()); + }); + } +} + +impl Driver for TimeDriver { + fn now(&self) -> u64 { + self.init(); + + let zero = unsafe { self.zero_instant.read() }; + StdInstant::now().duration_since(zero).as_micros() as u64 + } + + unsafe fn allocate_alarm(&self) -> Option { + let id = self + .alarm_count + .fetch_update(Ordering::AcqRel, Ordering::Acquire, |x| { + if x < ALARM_COUNT as u8 { + Some(x + 1) + } else { + None + } + }); + + match id { + Ok(id) => Some(AlarmHandle::new(id)), + Err(_) => None, + } + } + + fn set_alarm_callback(&self, alarm: AlarmHandle, callback: fn(*mut ()), ctx: *mut ()) { + self.init(); + let mut alarms = unsafe { self.alarms.as_ref() }.lock().unwrap(); + let alarm = &mut alarms[alarm.id() as usize]; + alarm.closure.replace(Closure::new(move || { + callback(ctx); + })); + } + + fn set_alarm(&self, alarm: AlarmHandle, timestamp: u64) { + self.init(); + let mut alarms = unsafe { self.alarms.as_ref() }.lock().unwrap(); + let alarm = &mut alarms[alarm.id() as usize]; + let timeout = (timestamp - self.now()) as u32; + if let Some(token) = alarm.token { + clearTimeout(token); + } + alarm.token = Some(setTimeout(alarm.closure.as_ref().unwrap(), timeout / 1000)); + } +} + +pub(crate) struct UninitCell(MaybeUninit>); +unsafe impl Send for UninitCell {} +unsafe impl Sync for UninitCell {} + +impl UninitCell { + pub const fn uninit() -> Self { + Self(MaybeUninit::uninit()) + } + unsafe fn as_ptr(&self) -> *const T { + (*self.0.as_ptr()).get() + } + + pub unsafe fn as_mut_ptr(&self) -> *mut T { + (*self.0.as_ptr()).get() + } + + pub unsafe fn as_ref(&self) -> &T { + &*self.as_ptr() + } + + pub unsafe fn write(&self, val: T) { + ptr::write(self.as_mut_ptr(), val) + } +} + +impl UninitCell { + pub unsafe fn read(&self) -> T { + ptr::read(self.as_mut_ptr()) + } +} diff --git a/embassy/src/time/mod.rs b/embassy/src/time/mod.rs index 6ce18d47..c8971bd1 100644 --- a/embassy/src/time/mod.rs +++ b/embassy/src/time/mod.rs @@ -51,6 +51,9 @@ mod timer; #[cfg(feature = "std")] mod driver_std; +#[cfg(feature = "wasm")] +mod driver_wasm; + pub use delay::{block_for, Delay}; pub use duration::Duration; pub use instant::Instant; diff --git a/examples/stm32h7/Cargo.toml b/examples/stm32h7/Cargo.toml index 94586b8a..c581e212 100644 --- a/examples/stm32h7/Cargo.toml +++ b/examples/stm32h7/Cargo.toml @@ -35,7 +35,7 @@ futures = { version = "0.3.17", default-features = false, features = ["async-awa rtt-target = { version = "0.3.1", features = ["cortex-m"] } heapless = { version = "0.7.5", default-features = false } rand_core = "0.6.3" -critical-section = "0.2.1" +critical-section = "0.2.2" micromath = "2.0.0" diff --git a/examples/wasm/Cargo.toml b/examples/wasm/Cargo.toml new file mode 100644 index 00000000..77f513ff --- /dev/null +++ b/examples/wasm/Cargo.toml @@ -0,0 +1,17 @@ +[package] +authors = ["Ulf Lilleengen "] +edition = "2018" +name = "embassy-wasm-example" +version = "0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +embassy = { version = "0.1.0", path = "../../embassy", features = ["log", "wasm"] } + +wasm-logger = "0.2.0" +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = ["Document", "Element", "HtmlElement", "Node", "Window" ] } +log = "0.4.11" +critical-section = "0.2.2" diff --git a/examples/wasm/README.md b/examples/wasm/README.md new file mode 100644 index 00000000..4bed4a79 --- /dev/null +++ b/examples/wasm/README.md @@ -0,0 +1,26 @@ +# WASM example + +Examples use a CLI tool named `wasm-pack` to build this example: + +``` +cargo install wasm-pack +``` + +## Building + +To build the example, run: + +``` +wasm-pack build --target web +``` + +## Running + +To run the example, start a webserver server the local folder: + + +``` +python -m http.server +``` + +Then, open a browser at https://127.0.0.1:8000 and watch the ticker print entries to the window. diff --git a/examples/wasm/index.html b/examples/wasm/index.html new file mode 100644 index 00000000..05e8b29f --- /dev/null +++ b/examples/wasm/index.html @@ -0,0 +1,25 @@ + + + + + + + + +

Log

+
+
    +
+
+ + diff --git a/examples/wasm/src/lib.rs b/examples/wasm/src/lib.rs new file mode 100644 index 00000000..0aa32a70 --- /dev/null +++ b/examples/wasm/src/lib.rs @@ -0,0 +1,37 @@ +#![feature(type_alias_impl_trait)] +#![allow(incomplete_features)] + +use embassy::{ + executor::Spawner, + time::{Duration, Timer}, +}; + +#[embassy::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(Duration::from_secs(1)).await; + } +} + +#[embassy::main] +async fn main(spawner: Spawner) { + wasm_logger::init(wasm_logger::Config::default()); + spawner.spawn(ticker()).unwrap(); +}