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:
parent
f1c35b40c7
commit
e24528051b
18 changed files with 414 additions and 8 deletions
2
.github/workflows/rust.yml
vendored
2
.github/workflows/rust.yml
vendored
|
@ -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:
|
||||
|
|
|
@ -18,3 +18,4 @@ nrf = []
|
|||
stm32 = []
|
||||
rp = []
|
||||
std = []
|
||||
wasm = []
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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" ] }
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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" ] }
|
||||
|
|
74
embassy/src/executor/arch/wasm.rs
Normal file
74
embassy/src/executor/arch/wasm.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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)]
|
||||
|
|
135
embassy/src/time/driver_wasm.rs
Normal file
135
embassy/src/time/driver_wasm.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
17
examples/wasm/Cargo.toml
Normal 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
26
examples/wasm/README.md
Normal 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
25
examples/wasm/index.html
Normal 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
37
examples/wasm/src/lib.rs
Normal 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();
|
||||
}
|
Loading…
Reference in a new issue