diff --git a/examples/embassy-stm32g4/src/bin/spawn_local.rs b/examples/embassy-stm32g4/src/bin/spawn_local.rs new file mode 100644 index 000000000000..4df2a2cdfec0 --- /dev/null +++ b/examples/embassy-stm32g4/src/bin/spawn_local.rs @@ -0,0 +1,42 @@ +#![no_main] +#![no_std] + +use core::marker::PhantomData; +use rtic::app; +use {defmt_rtt as _, panic_probe as _}; +pub mod pac { + pub use embassy_stm32::pac::Interrupt as interrupt; + pub use embassy_stm32::pac::*; +} + +#[app(device = pac, peripherals = false, dispatchers = [SPI1])] +mod app { + use super::*; + + #[shared] + struct Shared {} + + #[local] + struct Local {} + + #[init] + fn init(_cx: init::Context) -> (Shared, Local) { + task1::spawn().ok(); + //task2::spawn(Default::default()).ok(); <--- This is rejected since it is a local task + (Shared {}, Local {}) + } + + #[task(priority = 1)] + async fn task1(cx: task1::Context) { + defmt::info!("Hello from task1!"); + cx.local_spawner.task2(Default::default()).ok(); + } + + #[task(priority = 1, is_local_task = true)] + async fn task2(_cx: task2::Context, _nsns: super::NotSendNotSync) { + defmt::info!("Hello from task1!"); + } +} + +#[derive(Default)] +struct NotSendNotSync(PhantomData<*mut u8>); diff --git a/rtic-macros/CHANGELOG.md b/rtic-macros/CHANGELOG.md index f9a9445e5d07..4964282b3670 100644 --- a/rtic-macros/CHANGELOG.md +++ b/rtic-macros/CHANGELOG.md @@ -10,6 +10,7 @@ For each category, *Added*, *Changed*, *Fixed* add new entries at the top! ### Added - Outer attributes applied to RTIC app module are now forwarded to the generated code. +- Local spawner for spawning tasks with !Send/!Sync args on the same executor ## [v2.2.0] - 2025-06-22 diff --git a/rtic-macros/src/codegen/module.rs b/rtic-macros/src/codegen/module.rs index 1d2f90a6928b..6da8c8abe67f 100644 --- a/rtic-macros/src/codegen/module.rs +++ b/rtic-macros/src/codegen/module.rs @@ -1,5 +1,6 @@ use crate::syntax::{ast::App, Context}; use crate::{analyze::Analysis, codegen::bindings::interrupt_mod, codegen::util}; + use proc_macro2::TokenStream as TokenStream2; use quote::quote; @@ -112,37 +113,7 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { let internal_context_name = util::internal_task_ident(name, "Context"); let exec_name = util::internal_task_ident(name, "EXEC"); - items.push(quote!( - #(#cfgs)* - /// Execution context - #[allow(non_snake_case)] - #[allow(non_camel_case_types)] - pub struct #internal_context_name<'a> { - #[doc(hidden)] - __rtic_internal_p: ::core::marker::PhantomData<&'a ()>, - #(#fields,)* - } - - #(#cfgs)* - impl<'a> #internal_context_name<'a> { - #[inline(always)] - #[allow(missing_docs)] - pub unsafe fn new(#core) -> Self { - #internal_context_name { - __rtic_internal_p: ::core::marker::PhantomData, - #(#values,)* - } - } - } - )); - - module_items.push(quote!( - #(#cfgs)* - #[doc(inline)] - pub use super::#internal_context_name as Context; - )); - - if let Context::SoftwareTask(..) = ctxt { + if let Context::SoftwareTask(t) = ctxt { let spawnee = &app.software_tasks[name]; let priority = spawnee.args.priority; let cfgs = &spawnee.cfgs; @@ -163,13 +134,21 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { let (input_args, input_tupled, input_untupled, input_ty) = util::regroup_inputs(&spawnee.inputs); + let is_local_task = app.software_tasks[t].args.is_local_task; + let unsafety = if is_local_task { + // local tasks are only safe to call from the same executor + quote! { unsafe } + } else { + quote! {} + }; + // Spawn caller items.push(quote!( #(#cfgs)* /// Spawns the task directly #[allow(non_snake_case)] #[doc(hidden)] - pub fn #internal_spawn_ident(#(#input_args,)*) -> ::core::result::Result<(), #input_ty> { + pub #unsafety fn #internal_spawn_ident(#(#input_args,)*) -> ::core::result::Result<(), #input_ty> { // SAFETY: If `try_allocate` succeeds one must call `spawn`, which we do. unsafe { let exec = rtic::export::executor::AsyncTaskExecutor::#from_ptr_n_args(#name, &#exec_name); @@ -204,11 +183,70 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { } )); - module_items.push(quote!( - #(#cfgs)* - #[doc(inline)] - pub use super::#internal_spawn_ident as spawn; - )); + if !is_local_task { + module_items.push(quote!( + #(#cfgs)* + #[doc(inline)] + pub use super::#internal_spawn_ident as spawn; + )); + } + + let local_tasks_on_same_executor: Vec<_> = app + .software_tasks + .iter() + .filter(|(_, t)| t.args.is_local_task && t.args.priority == priority) + .collect(); + + if !local_tasks_on_same_executor.is_empty() { + let local_spawner = util::internal_task_ident(t, "LocalSpawner"); + fields.push(quote! { + /// Used to spawn tasks on the same executor + /// + /// This is useful for tasks that take args which are !Send/!Sync. + /// + /// NOTE: This only works with tasks marked `is_local_task = true` + /// and which have the same priority and thus will run on the + /// same executor. + pub local_spawner: #local_spawner + }); + let tasks = local_tasks_on_same_executor + .iter() + .map(|(ident, task)| { + // Copied mostly from software_tasks.rs + let internal_spawn_ident = util::internal_task_ident(ident, "spawn"); + let attrs = &task.attrs; + let cfgs = &task.cfgs; + let inputs = &task.inputs; + let generics = if task.is_bottom { + quote!() + } else { + quote!(<'a>) + }; + let input_vals = inputs.iter().map(|i| &i.pat).collect::>(); + let (_input_args, _input_tupled, _input_untupled, input_ty) = util::regroup_inputs(&task.inputs); + quote! { + #(#attrs)* + #(#cfgs)* + #[allow(non_snake_case)] + pub(super) fn #ident #generics(&self #(,#inputs)*) -> ::core::result::Result<(), #input_ty> { + // SAFETY: This is safe to call since this can only be called + // from the same executor + unsafe { #internal_spawn_ident(#(#input_vals,)*) } + } + } + }) + .collect::>(); + values.push(quote!(local_spawner: #local_spawner { _p: core::marker::PhantomData })); + items.push(quote! { + struct #local_spawner { + _p: core::marker::PhantomData<*mut ()>, + } + + impl #local_spawner { + #(#tasks)* + } + }); + } module_items.push(quote!( #(#cfgs)* @@ -217,6 +255,36 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { )); } + items.push(quote!( + #(#cfgs)* + /// Execution context + #[allow(non_snake_case)] + #[allow(non_camel_case_types)] + pub struct #internal_context_name<'a> { + #[doc(hidden)] + __rtic_internal_p: ::core::marker::PhantomData<&'a ()>, + #(#fields,)* + } + + #(#cfgs)* + impl<'a> #internal_context_name<'a> { + #[inline(always)] + #[allow(missing_docs)] + pub unsafe fn new(#core) -> Self { + #internal_context_name { + __rtic_internal_p: ::core::marker::PhantomData, + #(#values,)* + } + } + } + )); + + module_items.push(quote!( + #(#cfgs)* + #[doc(inline)] + pub use super::#internal_context_name as Context; + )); + if items.is_empty() { quote!() } else { diff --git a/rtic-macros/src/codegen/software_tasks.rs b/rtic-macros/src/codegen/software_tasks.rs index de8261bcc157..23618f7bdbc9 100644 --- a/rtic-macros/src/codegen/software_tasks.rs +++ b/rtic-macros/src/codegen/software_tasks.rs @@ -37,8 +37,16 @@ pub fn codegen(app: &App, analysis: &Analysis) -> TokenStream2 { let cfgs = &task.cfgs; let stmts = &task.stmts; let inputs = &task.inputs; - let lifetime = if task.is_bottom { quote!('static) } else { quote!('a) }; - let generics = if task.is_bottom { quote!() } else { quote!(<'a>) }; + let lifetime = if task.is_bottom { + quote!('static) + } else { + quote!('a) + }; + let generics = if task.is_bottom { + quote!() + } else { + quote!(<'a>) + }; user_tasks.push(quote!( #(#attrs)* diff --git a/rtic-macros/src/syntax/analyze.rs b/rtic-macros/src/syntax/analyze.rs index 3e5e80bd76fa..4da3ce63140a 100644 --- a/rtic-macros/src/syntax/analyze.rs +++ b/rtic-macros/src/syntax/analyze.rs @@ -285,13 +285,16 @@ pub(crate) fn app(app: &App) -> Result { for (name, spawnee) in &app.software_tasks { let spawnee_prio = spawnee.args.priority; + // TODO: What is this? let channel = channels.entry(spawnee_prio).or_default(); channel.tasks.insert(name.clone()); - // All inputs are send as we do not know from where they may be spawned. - spawnee.inputs.iter().for_each(|input| { - send_types.insert(input.ty.clone()); - }); + if !spawnee.args.is_local_task { + // All inputs are send as we do not know from where they may be spawned. + spawnee.inputs.iter().for_each(|input| { + send_types.insert(input.ty.clone()); + }); + } } // No channel should ever be empty diff --git a/rtic-macros/src/syntax/ast.rs b/rtic-macros/src/syntax/ast.rs index 44c1385d24fb..92bb5a11f0a0 100644 --- a/rtic-macros/src/syntax/ast.rs +++ b/rtic-macros/src/syntax/ast.rs @@ -256,6 +256,12 @@ pub struct SoftwareTaskArgs { /// Shared resources that can be accessed from this context pub shared_resources: SharedResources, + + /// Local tasks + /// + /// Local tasks can only be spawned from the same executor. + /// However they do not require Send and Sync + pub is_local_task: bool, } impl Default for SoftwareTaskArgs { @@ -264,6 +270,7 @@ impl Default for SoftwareTaskArgs { priority: 0, local_resources: LocalResources::new(), shared_resources: SharedResources::new(), + is_local_task: false, } } } diff --git a/rtic-macros/src/syntax/parse.rs b/rtic-macros/src/syntax/parse.rs index ea7ff29409ad..18ae64e12474 100644 --- a/rtic-macros/src/syntax/parse.rs +++ b/rtic-macros/src/syntax/parse.rs @@ -8,10 +8,7 @@ mod util; use proc_macro2::TokenStream as TokenStream2; use syn::{ - braced, - parse::{self, Parse, ParseStream, Parser}, - token::Brace, - Attribute, Ident, Item, LitInt, Meta, Token, + braced, parse::{self, Parse, ParseStream, Parser}, token::Brace, Attribute, Ident, Item, LitBool, LitInt, Meta, Token }; use crate::syntax::{ @@ -197,6 +194,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result parse::Result { + if is_local_task.is_some() { + return Err(parse::Error::new( + ident.span(), + "argument appears more than once", + )); + } + + let lit: LitBool = input.parse()?; + + is_local_task = Some(lit.value); + } + _ => { return Err(parse::Error::new(ident.span(), "unexpected argument")); } @@ -291,6 +302,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result parse::Result