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

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