From ac2aec4e7a8e8a4c17c611810d382892398c9eb7 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Thu, 7 Dec 2023 00:43:18 +0100 Subject: executor: rename macro crate to embassy-executor-macros, bump it. --- embassy-executor-macros/Cargo.toml | 24 ++++ embassy-executor-macros/README.md | 15 +++ embassy-executor-macros/src/lib.rs | 175 +++++++++++++++++++++++++++++ embassy-executor-macros/src/macros/main.rs | 138 +++++++++++++++++++++++ embassy-executor-macros/src/macros/mod.rs | 2 + embassy-executor-macros/src/macros/task.rs | 114 +++++++++++++++++++ embassy-executor-macros/src/util/ctxt.rs | 73 ++++++++++++ embassy-executor-macros/src/util/mod.rs | 1 + 8 files changed, 542 insertions(+) create mode 100644 embassy-executor-macros/Cargo.toml create mode 100644 embassy-executor-macros/README.md create mode 100644 embassy-executor-macros/src/lib.rs create mode 100644 embassy-executor-macros/src/macros/main.rs create mode 100644 embassy-executor-macros/src/macros/mod.rs create mode 100644 embassy-executor-macros/src/macros/task.rs create mode 100644 embassy-executor-macros/src/util/ctxt.rs create mode 100644 embassy-executor-macros/src/util/mod.rs (limited to 'embassy-executor-macros') diff --git a/embassy-executor-macros/Cargo.toml b/embassy-executor-macros/Cargo.toml new file mode 100644 index 000000000..6ea7ddb71 --- /dev/null +++ b/embassy-executor-macros/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "embassy-executor-macros" +version = "0.4.0" +edition = "2021" +license = "MIT OR Apache-2.0" +description = "macros for creating the entry point and tasks for embassy-executor" +repository = "https://github.com/embassy-rs/embassy" +categories = [ + "embedded", + "no-std", + "asynchronous", +] + +[dependencies] +syn = { version = "2.0.15", features = ["full", "extra-traits"] } +quote = "1.0.9" +darling = "0.20.1" +proc-macro2 = "1.0.29" + +[lib] +proc-macro = true + +[features] +nightly = [] \ No newline at end of file diff --git a/embassy-executor-macros/README.md b/embassy-executor-macros/README.md new file mode 100644 index 000000000..a959c85d5 --- /dev/null +++ b/embassy-executor-macros/README.md @@ -0,0 +1,15 @@ +# embassy-executor-macros + +An [Embassy](https://embassy.dev) project. + +NOTE: Do not use this crate directly. The macros are re-exported by `embassy-executor`. + +## License + +This work is licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. diff --git a/embassy-executor-macros/src/lib.rs b/embassy-executor-macros/src/lib.rs new file mode 100644 index 000000000..c9d58746a --- /dev/null +++ b/embassy-executor-macros/src/lib.rs @@ -0,0 +1,175 @@ +#![doc = include_str!("../README.md")] +extern crate proc_macro; + +use darling::ast::NestedMeta; +use proc_macro::TokenStream; + +mod macros; +mod util; +use macros::*; +use syn::parse::{Parse, ParseBuffer}; +use syn::punctuated::Punctuated; +use syn::Token; + +struct Args { + meta: Vec, +} + +impl Parse for Args { + fn parse(input: &ParseBuffer) -> syn::Result { + let meta = Punctuated::::parse_terminated(input)?; + Ok(Args { + meta: meta.into_iter().collect(), + }) + } +} + +/// Declares an async task that can be run by `embassy-executor`. The optional `pool_size` parameter can be used to specify how +/// many concurrent tasks can be spawned (default is 1) for the function. +/// +/// +/// The following restrictions apply: +/// +/// * The function must be declared `async`. +/// * The function must not use generics. +/// * The optional `pool_size` attribute must be 1 or greater. +/// +/// +/// ## Examples +/// +/// Declaring a task taking no arguments: +/// +/// ``` rust +/// #[embassy_executor::task] +/// async fn mytask() { +/// // Function body +/// } +/// ``` +/// +/// Declaring a task with a given pool size: +/// +/// ``` rust +/// #[embassy_executor::task(pool_size = 4)] +/// async fn mytask() { +/// // Function body +/// } +/// ``` +#[proc_macro_attribute] +pub fn task(args: TokenStream, item: TokenStream) -> TokenStream { + let args = syn::parse_macro_input!(args as Args); + let f = syn::parse_macro_input!(item as syn::ItemFn); + + task::run(&args.meta, f).unwrap_or_else(|x| x).into() +} + +/// Creates a new `executor` instance and declares an application entry point for Cortex-M spawning the corresponding function body as an async task. +/// +/// The following restrictions apply: +/// +/// * The function must accept exactly 1 parameter, an `embassy_executor::Spawner` handle that it can use to spawn additional tasks. +/// * The function must be declared `async`. +/// * The function must not use generics. +/// * Only a single `main` task may be declared. +/// +/// ## Examples +/// Spawning a task: +/// +/// ``` rust +/// #[embassy_executor::main] +/// async fn main(_s: embassy_executor::Spawner) { +/// // Function body +/// } +/// ``` +#[proc_macro_attribute] +pub fn main_cortex_m(args: TokenStream, item: TokenStream) -> TokenStream { + let args = syn::parse_macro_input!(args as Args); + let f = syn::parse_macro_input!(item as syn::ItemFn); + main::run(&args.meta, f, main::cortex_m()).unwrap_or_else(|x| x).into() +} + +/// Creates a new `executor` instance and declares an application entry point for RISC-V spawning the corresponding function body as an async task. +/// +/// The following restrictions apply: +/// +/// * The function must accept exactly 1 parameter, an `embassy_executor::Spawner` handle that it can use to spawn additional tasks. +/// * The function must be declared `async`. +/// * The function must not use generics. +/// * Only a single `main` task may be declared. +/// +/// A user-defined entry macro can be optionally provided via the `entry` argument to override the default of `riscv_rt::entry`. +/// +/// ## Examples +/// Spawning a task: +/// +/// ``` rust +/// #[embassy_executor::main] +/// async fn main(_s: embassy_executor::Spawner) { +/// // Function body +/// } +/// ``` +/// +/// Spawning a task using a custom entry macro: +/// ``` rust +/// #[embassy_executor::main(entry = "esp_riscv_rt::entry")] +/// async fn main(_s: embassy_executor::Spawner) { +/// // Function body +/// } +/// ``` +#[proc_macro_attribute] +pub fn main_riscv(args: TokenStream, item: TokenStream) -> TokenStream { + let args = syn::parse_macro_input!(args as Args); + let f = syn::parse_macro_input!(item as syn::ItemFn); + main::run(&args.meta, f, main::riscv(&args.meta)) + .unwrap_or_else(|x| x) + .into() +} + +/// Creates a new `executor` instance and declares an application entry point for STD spawning the corresponding function body as an async task. +/// +/// The following restrictions apply: +/// +/// * The function must accept exactly 1 parameter, an `embassy_executor::Spawner` handle that it can use to spawn additional tasks. +/// * The function must be declared `async`. +/// * The function must not use generics. +/// * Only a single `main` task may be declared. +/// +/// ## Examples +/// Spawning a task: +/// +/// ``` rust +/// #[embassy_executor::main] +/// async fn main(_s: embassy_executor::Spawner) { +/// // Function body +/// } +/// ``` +#[proc_macro_attribute] +pub fn main_std(args: TokenStream, item: TokenStream) -> TokenStream { + let args = syn::parse_macro_input!(args as Args); + let f = syn::parse_macro_input!(item as syn::ItemFn); + main::run(&args.meta, f, main::std()).unwrap_or_else(|x| x).into() +} + +/// Creates a new `executor` instance and declares an application entry point for WASM spawning the corresponding function body as an async task. +/// +/// The following restrictions apply: +/// +/// * The function must accept exactly 1 parameter, an `embassy_executor::Spawner` handle that it can use to spawn additional tasks. +/// * The function must be declared `async`. +/// * The function must not use generics. +/// * Only a single `main` task may be declared. +/// +/// ## Examples +/// Spawning a task: +/// +/// ``` rust +/// #[embassy_executor::main] +/// async fn main(_s: embassy_executor::Spawner) { +/// // Function body +/// } +/// ``` +#[proc_macro_attribute] +pub fn main_wasm(args: TokenStream, item: TokenStream) -> TokenStream { + let args = syn::parse_macro_input!(args as Args); + let f = syn::parse_macro_input!(item as syn::ItemFn); + main::run(&args.meta, f, main::wasm()).unwrap_or_else(|x| x).into() +} diff --git a/embassy-executor-macros/src/macros/main.rs b/embassy-executor-macros/src/macros/main.rs new file mode 100644 index 000000000..3c0d58567 --- /dev/null +++ b/embassy-executor-macros/src/macros/main.rs @@ -0,0 +1,138 @@ +use darling::export::NestedMeta; +use darling::FromMeta; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Expr, ReturnType, Type}; + +use crate::util::ctxt::Ctxt; + +#[derive(Debug, FromMeta)] +struct Args { + #[darling(default)] + entry: Option, +} + +pub fn riscv(args: &[NestedMeta]) -> TokenStream { + let maybe_entry = match Args::from_list(args) { + Ok(args) => args.entry, + Err(e) => return e.write_errors(), + }; + + let entry = maybe_entry.unwrap_or("riscv_rt::entry".into()); + let entry = match Expr::from_string(&entry) { + Ok(expr) => expr, + Err(e) => return e.write_errors(), + }; + + quote! { + #[#entry] + fn main() -> ! { + let mut executor = ::embassy_executor::Executor::new(); + let executor = unsafe { __make_static(&mut executor) }; + executor.run(|spawner| { + spawner.must_spawn(__embassy_main(spawner)); + }) + } + } +} + +pub fn cortex_m() -> TokenStream { + quote! { + #[cortex_m_rt::entry] + fn main() -> ! { + let mut executor = ::embassy_executor::Executor::new(); + let executor = unsafe { __make_static(&mut executor) }; + executor.run(|spawner| { + spawner.must_spawn(__embassy_main(spawner)); + }) + } + } +} + +pub fn wasm() -> TokenStream { + quote! { + #[wasm_bindgen::prelude::wasm_bindgen(start)] + pub fn main() -> Result<(), wasm_bindgen::JsValue> { + let executor = ::std::boxed::Box::leak(::std::boxed::Box::new(::embassy_executor::Executor::new())); + + executor.start(|spawner| { + spawner.spawn(__embassy_main(spawner)).unwrap(); + }); + + Ok(()) + } + } +} + +pub fn std() -> TokenStream { + quote! { + fn main() -> ! { + let mut executor = ::embassy_executor::Executor::new(); + let executor = unsafe { __make_static(&mut executor) }; + + executor.run(|spawner| { + spawner.must_spawn(__embassy_main(spawner)); + }) + } + } +} + +pub fn run(args: &[NestedMeta], f: syn::ItemFn, main: TokenStream) -> Result { + #[allow(unused_variables)] + let args = Args::from_list(args).map_err(|e| e.write_errors())?; + + let fargs = f.sig.inputs.clone(); + + let ctxt = Ctxt::new(); + + if f.sig.asyncness.is_none() { + ctxt.error_spanned_by(&f.sig, "main function must be async"); + } + if !f.sig.generics.params.is_empty() { + ctxt.error_spanned_by(&f.sig, "main function must not be generic"); + } + if !f.sig.generics.where_clause.is_none() { + ctxt.error_spanned_by(&f.sig, "main function must not have `where` clauses"); + } + if !f.sig.abi.is_none() { + ctxt.error_spanned_by(&f.sig, "main function must not have an ABI qualifier"); + } + if !f.sig.variadic.is_none() { + ctxt.error_spanned_by(&f.sig, "main function must not be variadic"); + } + match &f.sig.output { + ReturnType::Default => {} + ReturnType::Type(_, ty) => match &**ty { + Type::Tuple(tuple) if tuple.elems.is_empty() => {} + Type::Never(_) => {} + _ => ctxt.error_spanned_by( + &f.sig, + "main function must either not return a value, return `()` or return `!`", + ), + }, + } + + if fargs.len() != 1 { + ctxt.error_spanned_by(&f.sig, "main function must have 1 argument: the spawner."); + } + + ctxt.check()?; + + let f_body = f.block; + let out = &f.sig.output; + + let result = quote! { + #[::embassy_executor::task()] + async fn __embassy_main(#fargs) #out { + #f_body + } + + unsafe fn __make_static(t: &mut T) -> &'static mut T { + ::core::mem::transmute(t) + } + + #main + }; + + Ok(result) +} diff --git a/embassy-executor-macros/src/macros/mod.rs b/embassy-executor-macros/src/macros/mod.rs new file mode 100644 index 000000000..572094ca6 --- /dev/null +++ b/embassy-executor-macros/src/macros/mod.rs @@ -0,0 +1,2 @@ +pub mod main; +pub mod task; diff --git a/embassy-executor-macros/src/macros/task.rs b/embassy-executor-macros/src/macros/task.rs new file mode 100644 index 000000000..5161e1020 --- /dev/null +++ b/embassy-executor-macros/src/macros/task.rs @@ -0,0 +1,114 @@ +use darling::export::NestedMeta; +use darling::FromMeta; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote}; +use syn::{parse_quote, Expr, ExprLit, ItemFn, Lit, LitInt, ReturnType, Type}; + +use crate::util::ctxt::Ctxt; + +#[derive(Debug, FromMeta)] +struct Args { + #[darling(default)] + pool_size: Option, +} + +pub fn run(args: &[NestedMeta], f: syn::ItemFn) -> Result { + let args = Args::from_list(args).map_err(|e| e.write_errors())?; + + let pool_size = args.pool_size.unwrap_or(Expr::Lit(ExprLit { + attrs: vec![], + lit: Lit::Int(LitInt::new("1", Span::call_site())), + })); + + let ctxt = Ctxt::new(); + + if f.sig.asyncness.is_none() { + ctxt.error_spanned_by(&f.sig, "task functions must be async"); + } + if !f.sig.generics.params.is_empty() { + ctxt.error_spanned_by(&f.sig, "task functions must not be generic"); + } + if !f.sig.generics.where_clause.is_none() { + ctxt.error_spanned_by(&f.sig, "task functions must not have `where` clauses"); + } + if !f.sig.abi.is_none() { + ctxt.error_spanned_by(&f.sig, "task functions must not have an ABI qualifier"); + } + if !f.sig.variadic.is_none() { + ctxt.error_spanned_by(&f.sig, "task functions must not be variadic"); + } + match &f.sig.output { + ReturnType::Default => {} + ReturnType::Type(_, ty) => match &**ty { + Type::Tuple(tuple) if tuple.elems.is_empty() => {} + Type::Never(_) => {} + _ => ctxt.error_spanned_by( + &f.sig, + "task functions must either not return a value, return `()` or return `!`", + ), + }, + } + + let mut arg_names = Vec::new(); + let mut fargs = f.sig.inputs.clone(); + + for arg in fargs.iter_mut() { + match arg { + syn::FnArg::Receiver(_) => { + ctxt.error_spanned_by(arg, "task functions must not have receiver arguments"); + } + syn::FnArg::Typed(t) => match t.pat.as_mut() { + syn::Pat::Ident(id) => { + arg_names.push(id.ident.clone()); + id.mutability = None; + } + _ => { + ctxt.error_spanned_by(arg, "pattern matching in task arguments is not yet supported"); + } + }, + } + } + + ctxt.check()?; + + let task_ident = f.sig.ident.clone(); + let task_inner_ident = format_ident!("__{}_task", task_ident); + + let mut task_inner = f; + let visibility = task_inner.vis.clone(); + task_inner.vis = syn::Visibility::Inherited; + task_inner.sig.ident = task_inner_ident.clone(); + + #[cfg(feature = "nightly")] + let mut task_outer: ItemFn = parse_quote! { + #visibility fn #task_ident(#fargs) -> ::embassy_executor::SpawnToken { + type Fut = impl ::core::future::Future + 'static; + const POOL_SIZE: usize = #pool_size; + static POOL: ::embassy_executor::raw::TaskPool = ::embassy_executor::raw::TaskPool::new(); + unsafe { POOL._spawn_async_fn(move || #task_inner_ident(#(#arg_names,)*)) } + } + }; + #[cfg(not(feature = "nightly"))] + let mut task_outer: ItemFn = parse_quote! { + #visibility fn #task_ident(#fargs) -> ::embassy_executor::SpawnToken { + const POOL_SIZE: usize = #pool_size; + static POOL: ::embassy_executor::_export::TaskPoolRef = ::embassy_executor::_export::TaskPoolRef::new(); + unsafe { POOL.get::<_, POOL_SIZE>()._spawn_async_fn(move || #task_inner_ident(#(#arg_names,)*)) } + } + }; + + task_outer.attrs.append(&mut task_inner.attrs.clone()); + + let result = quote! { + // This is the user's task function, renamed. + // We put it outside the #task_ident fn below, because otherwise + // the items defined there (such as POOL) would be in scope + // in the user's code. + #[doc(hidden)] + #task_inner + + #task_outer + }; + + Ok(result) +} diff --git a/embassy-executor-macros/src/util/ctxt.rs b/embassy-executor-macros/src/util/ctxt.rs new file mode 100644 index 000000000..74c872c3c --- /dev/null +++ b/embassy-executor-macros/src/util/ctxt.rs @@ -0,0 +1,73 @@ +// nifty utility borrowed from serde :) +// https://github.com/serde-rs/serde/blob/master/serde_derive/src/internals/ctxt.rs + +use std::cell::RefCell; +use std::fmt::Display; +use std::thread; + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn; + +/// A type to collect errors together and format them. +/// +/// Dropping this object will cause a panic. It must be consumed using `check`. +/// +/// References can be shared since this type uses run-time exclusive mut checking. +#[derive(Default)] +pub struct Ctxt { + // The contents will be set to `None` during checking. This is so that checking can be + // enforced. + errors: RefCell>>, +} + +impl Ctxt { + /// Create a new context object. + /// + /// This object contains no errors, but will still trigger a panic if it is not `check`ed. + pub fn new() -> Self { + Ctxt { + errors: RefCell::new(Some(Vec::new())), + } + } + + /// Add an error to the context object with a tokenenizable object. + /// + /// The object is used for spanning in error messages. + pub fn error_spanned_by(&self, obj: A, msg: T) { + self.errors + .borrow_mut() + .as_mut() + .unwrap() + // Curb monomorphization from generating too many identical methods. + .push(syn::Error::new_spanned(obj.into_token_stream(), msg)); + } + + /// Add one of Syn's parse errors. + #[allow(unused)] + pub fn syn_error(&self, err: syn::Error) { + self.errors.borrow_mut().as_mut().unwrap().push(err); + } + + /// Consume this object, producing a formatted error string if there are errors. + pub fn check(self) -> Result<(), TokenStream> { + let errors = self.errors.borrow_mut().take().unwrap(); + match errors.len() { + 0 => Ok(()), + _ => Err(to_compile_errors(errors)), + } + } +} + +fn to_compile_errors(errors: Vec) -> proc_macro2::TokenStream { + let compile_errors = errors.iter().map(syn::Error::to_compile_error); + quote!(#(#compile_errors)*) +} + +impl Drop for Ctxt { + fn drop(&mut self) { + if !thread::panicking() && self.errors.borrow().is_some() { + panic!("forgot to check for errors"); + } + } +} diff --git a/embassy-executor-macros/src/util/mod.rs b/embassy-executor-macros/src/util/mod.rs new file mode 100644 index 000000000..28702809e --- /dev/null +++ b/embassy-executor-macros/src/util/mod.rs @@ -0,0 +1 @@ +pub mod ctxt; -- cgit