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
This commit is contained in:
Ulf Lilleengen 2021-09-13 14:35:40 +02:00
parent f1c35b40c7
commit e24528051b
18 changed files with 414 additions and 8 deletions

View file

@ -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:

View file

@ -18,3 +18,4 @@ nrf = []
stm32 = []
rp = []
std = []
wasm = []

View file

@ -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(&macro_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()
}

View file

@ -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" ] }

View file

@ -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"] }

View file

@ -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"] }

View file

@ -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" ] }

View file

@ -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<Closure<dyn FnMut(JsValue)>>,
}
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());
}
}
}

View file

@ -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;

View file

@ -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};

View file

@ -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)]

View file

@ -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<f64>,
closure: Option<Closure<dyn FnMut() + 'static>>,
}
unsafe impl Send for AlarmState {}
impl AlarmState {
const fn new() -> Self {
Self {
token: None,
closure: None,
}
}
}
#[wasm_bindgen]
extern "C" {
fn setTimeout(closure: &Closure<dyn FnMut()>, millis: u32) -> f64;
fn clearTimeout(token: f64);
}
struct TimeDriver {
alarm_count: AtomicU8,
once: Once,
alarms: UninitCell<Mutex<[AlarmState; ALARM_COUNT]>>,
zero_instant: UninitCell<StdInstant>,
}
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<AlarmHandle> {
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<T>(MaybeUninit<UnsafeCell<T>>);
unsafe impl<T> Send for UninitCell<T> {}
unsafe impl<T> Sync for UninitCell<T> {}
impl<T> UninitCell<T> {
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<T: Copy> UninitCell<T> {
pub unsafe fn read(&self) -> T {
ptr::read(self.as_mut_ptr())
}
}

View file

@ -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;

View file

@ -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"

17
examples/wasm/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
authors = ["Ulf Lilleengen <lulf@redhat.com>"]
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"

26
examples/wasm/README.md Normal file
View file

@ -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.

25
examples/wasm/index.html Normal file
View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
</head>
<body>
<!-- Note the usage of `type=module` here as this is an ES6 module -->
<script type="module">
// Use ES module import syntax to import functionality from the module
// that we have compiled.
//
// Note that the `default` import is an initialization function which
// will "boot" the module and make it ready to use. Currently browsers
// don't support natively imported WebAssembly as an ES module, but
// eventually the manual initialization won't be required!
import init from './pkg/embassy_wasm_example.js';
await init();
</script>
<h1>Log</h1>
<div>
<ul id="log">
</ul>
</div>
</body>
</html>

37
examples/wasm/src/lib.rs Normal file
View file

@ -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();
}