From 578bcbff125ecb81ef9d82995a36de8535f70735 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Thu, 13 Nov 2025 01:46:49 +0100 Subject: [PATCH 1/8] Allow spawning non Send/Sync tasks from the same prio using local spawner --- rtic-macros/CHANGELOG.md | 1 + rtic-macros/src/codegen/module.rs | 136 ++++++++++++++++++++++-------- rtic-macros/src/codegen/util.rs | 8 +- rtic-macros/src/syntax/analyze.rs | 5 -- rtic/src/export.rs | 1 + rtic/src/export/dummy.rs | 15 ++++ 6 files changed, 124 insertions(+), 42 deletions(-) create mode 100644 rtic/src/export/dummy.rs 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..f421d5adc616 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; @@ -158,18 +129,21 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { }; let internal_spawn_ident = util::internal_task_ident(name, "spawn"); + let internal_spawn_helper_ident = util::internal_task_ident(name, "spawn_helper"); let internal_waker_ident = util::internal_task_ident(name, "waker"); let from_ptr_n_args = util::from_ptr_n_args_ident(spawnee.inputs.len()); - let (input_args, input_tupled, input_untupled, input_ty) = + let (generic_input_args, input_args, input_tupled, input_untupled, input_ty) = util::regroup_inputs(&spawnee.inputs); // Spawn caller items.push(quote!( #(#cfgs)* - /// Spawns the task directly + /// Spawns the task without checking if the spawner and spawnee are the same priority + /// + /// SAFETY: The caller needs to check that the spawner and spawnee are the same priority #[allow(non_snake_case)] #[doc(hidden)] - pub fn #internal_spawn_ident(#(#input_args,)*) -> ::core::result::Result<(), #input_ty> { + pub unsafe fn #internal_spawn_helper_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); @@ -183,6 +157,14 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { } } } + + /// Spawns the task directly + #[allow(non_snake_case)] + #[doc(hidden)] + pub fn #internal_spawn_ident(#(#generic_input_args,)*) -> ::core::result::Result<(), #input_ty> { + // SAFETY: The generic args require Send + Sync + unsafe { #internal_spawn_helper_ident(#(#input_untupled.to()),*) } + } )); // Waker @@ -204,11 +186,63 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { } )); - module_items.push(quote!( + module_items.push(quote!{ #(#cfgs)* #[doc(inline)] pub use super::#internal_spawn_ident as spawn; - )); + }); + + let tasks_on_same_executor: Vec<_> = app + .software_tasks + .iter() + .filter(|(_, t)| t.args.priority == priority) + .collect(); + + if !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. + pub local_spawner: #local_spawner + }); + let tasks = tasks_on_same_executor + .iter() + .map(|(ident, task)| { + // Copied mostly from software_tasks.rs + let internal_spawn_ident = util::internal_task_ident(ident, "spawn_helper"); + let attrs = &task.attrs; + let cfgs = &task.cfgs; + let generics = if task.is_bottom { + quote!() + } else { + quote!(<'a>) + }; + + let (_generic_input_args, 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 #(,#input_args)*) -> ::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_untupled,)*) } + } + } + }) + .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 +251,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/util.rs b/rtic-macros/src/codegen/util.rs index dda7e2903534..c94fd8b459bc 100644 --- a/rtic-macros/src/codegen/util.rs +++ b/rtic-macros/src/codegen/util.rs @@ -35,6 +35,8 @@ pub fn link_section_uninit() -> TokenStream2 { pub fn regroup_inputs( inputs: &[PatType], ) -> ( + // Generic args e.g. &[`_0: impl Dummy`, `_1: impl Dummy`] + Vec, // args e.g. &[`_0`], &[`_0: i32`, `_1: i64`] Vec, // tupled e.g. `_0`, `(_0, _1)` @@ -48,12 +50,14 @@ pub fn regroup_inputs( let ty = &inputs[0].ty; ( + vec![quote!(_0: impl rtic::export::dummy::Dummy + Send + Sync)], vec![quote!(_0: #ty)], quote!(_0), vec![quote!(_0)], quote!(#ty), ) } else { + let mut generic_args = vec![]; let mut args = vec![]; let mut pats = vec![]; let mut tys = vec![]; @@ -62,6 +66,8 @@ pub fn regroup_inputs( let i = Ident::new(&format!("_{i}"), Span::call_site()); let ty = &input.ty; + generic_args.push(quote!(#i: impl rtic::export::dummy::Dummy + Send + Sync)); + args.push(quote!(#i: #ty)); pats.push(quote!(#i)); @@ -74,7 +80,7 @@ pub fn regroup_inputs( quote!((#(#pats,)*)) }; let ty = quote!((#(#tys,)*)); - (args, tupled, pats, ty) + (generic_args, args, tupled, pats, ty) } } diff --git a/rtic-macros/src/syntax/analyze.rs b/rtic-macros/src/syntax/analyze.rs index 3e5e80bd76fa..7d1497979b34 100644 --- a/rtic-macros/src/syntax/analyze.rs +++ b/rtic-macros/src/syntax/analyze.rs @@ -287,11 +287,6 @@ pub(crate) fn app(app: &App) -> Result { 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()); - }); } // No channel should ever be empty diff --git a/rtic/src/export.rs b/rtic/src/export.rs index 0ee24cd5305d..0beeafbba923 100644 --- a/rtic/src/export.rs +++ b/rtic/src/export.rs @@ -1,6 +1,7 @@ pub use critical_section::CriticalSection; pub use portable_atomic as atomic; +pub mod dummy; pub mod executor; // Cortex-M target (any) diff --git a/rtic/src/export/dummy.rs b/rtic/src/export/dummy.rs new file mode 100644 index 000000000000..be16aeb5ae13 --- /dev/null +++ b/rtic/src/export/dummy.rs @@ -0,0 +1,15 @@ +// TODO: What should we name this? + +/// Dummy trait which will only ever be implemented where type T is Self +pub trait Dummy { + /// This should always be same as `Self` + type T; + fn to(self) -> Self::T; +} + +impl Dummy for T { + type T = T; + fn to(self) -> T { + self + } +} From 964642bf8ccf4c77c66698d75d2de1340433fa54 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Thu, 13 Nov 2025 01:47:37 +0100 Subject: [PATCH 2/8] Add example --- examples/lm3s6965/examples/spawn_local.rs | 39 +++++++++++++++++++++++ examples/lm3s6965/examples/wait-queue.rs | 4 +-- 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 examples/lm3s6965/examples/spawn_local.rs diff --git a/examples/lm3s6965/examples/spawn_local.rs b/examples/lm3s6965/examples/spawn_local.rs new file mode 100644 index 000000000000..74bb58237b0e --- /dev/null +++ b/examples/lm3s6965/examples/spawn_local.rs @@ -0,0 +1,39 @@ +#![no_main] +#![no_std] + +use panic_semihosting as _; + +#[rtic::app(device = lm3s6965, dispatchers = [SSI0])] +mod app { + use cortex_m_semihosting::{debug, hprintln}; + use super::*; + + #[shared] + struct Shared {} + + #[local] + struct Local {} + + #[init] + fn init(_cx: init::Context) -> (Shared, Local) { + task1::spawn().unwrap(); + //task2::spawn(Default::default()).ok(); <--- This is rejected since not all args are Send and Sync + (Shared {}, Local {}) + } + + #[task(priority = 1)] + async fn task1(cx: task1::Context) { + hprintln!("Hello from task1!"); + cx.local_spawner.task2(Default::default()).unwrap(); + } + + // Task where some args are !Send/!Sync + #[task(priority = 1)] + async fn task2(_cx: task2::Context, _nsns: NotSendNotSync) { + hprintln!("Hello from task2!"); + debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator + } +} + +#[derive(Default, Debug)] +struct NotSendNotSync(core::marker::PhantomData<*mut u8>); diff --git a/examples/lm3s6965/examples/wait-queue.rs b/examples/lm3s6965/examples/wait-queue.rs index 7be1c0a89cc5..e60d919170f9 100644 --- a/examples/lm3s6965/examples/wait-queue.rs +++ b/examples/lm3s6965/examples/wait-queue.rs @@ -28,8 +28,8 @@ mod app { fn init(cx: init::Context) -> (Shared, Local) { Mono::start(cx.core.SYST, 12_000_000); - incrementer::spawn(cx.local.wait_queue).ok().unwrap(); - waiter::spawn(cx.local.wait_queue).ok().unwrap(); + incrementer::spawn(&*cx.local.wait_queue).ok().unwrap(); + waiter::spawn(&*cx.local.wait_queue).ok().unwrap(); let count = 0; From c2f5a60f51fd6866f1a816c3ccb4f8a9ac562b50 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Thu, 13 Nov 2025 01:43:27 +0100 Subject: [PATCH 3/8] Add tests --- ci/expected/lm3s6965/spawn_local.run | 2 + rtic/ui/spawn-non-send-different-exec.rs | 28 ++++++++++ rtic/ui/spawn-non-send-different-exec.stderr | 8 +++ rtic/ui/spawn-non-send-from-init.rs | 24 ++++++++ rtic/ui/spawn-non-send-from-init.stderr | 51 +++++++++++++++++ rtic/ui/task-reference-in-spawn.stderr | 58 ++++++++++++++++++++ 6 files changed, 171 insertions(+) create mode 100644 ci/expected/lm3s6965/spawn_local.run create mode 100644 rtic/ui/spawn-non-send-different-exec.rs create mode 100644 rtic/ui/spawn-non-send-different-exec.stderr create mode 100644 rtic/ui/spawn-non-send-from-init.rs create mode 100644 rtic/ui/spawn-non-send-from-init.stderr diff --git a/ci/expected/lm3s6965/spawn_local.run b/ci/expected/lm3s6965/spawn_local.run new file mode 100644 index 000000000000..a909e77451be --- /dev/null +++ b/ci/expected/lm3s6965/spawn_local.run @@ -0,0 +1,2 @@ +Hello from task1! +Hello from task2! diff --git a/rtic/ui/spawn-non-send-different-exec.rs b/rtic/ui/spawn-non-send-different-exec.rs new file mode 100644 index 000000000000..cb284f4a5eb6 --- /dev/null +++ b/rtic/ui/spawn-non-send-different-exec.rs @@ -0,0 +1,28 @@ +#![no_main] + +#[rtic::app(device = lm3s6965, dispatchers = [SSI0, GPIOA])] +mod app { + use super::*; + + #[shared] + struct Shared {} + + #[local] + struct Local {} + + #[init] + fn init(_cx: init::Context) -> (Shared, Local) { + (Shared {}, Local {}) + } + + #[task(priority = 1)] + async fn foo(_cx: foo::Context, _nsns: NotSendNotSync) {} + + #[task(priority = 2)] + async fn bar(cx: bar::Context) { + cx.local_spawner.foo(Default::default()).ok(); + } +} + +#[derive(Default, Debug)] +struct NotSendNotSync(core::marker::PhantomData<*mut u8>); diff --git a/rtic/ui/spawn-non-send-different-exec.stderr b/rtic/ui/spawn-non-send-different-exec.stderr new file mode 100644 index 000000000000..42e00440f184 --- /dev/null +++ b/rtic/ui/spawn-non-send-different-exec.stderr @@ -0,0 +1,8 @@ +error[E0599]: no method named `foo` found for struct `__rtic_internal_bar_LocalSpawner` in the current scope + --> ui/spawn-non-send-different-exec.rs:23:26 + | + 3 | #[rtic::app(device = lm3s6965, dispatchers = [SSI0, GPIOA])] + | ------------------------------------------------------------ method `foo` not found for this struct +... +23 | cx.local_spawner.foo(Default::default()).ok(); + | ^^^ method not found in `__rtic_internal_bar_LocalSpawner` diff --git a/rtic/ui/spawn-non-send-from-init.rs b/rtic/ui/spawn-non-send-from-init.rs new file mode 100644 index 000000000000..7fb478209b1c --- /dev/null +++ b/rtic/ui/spawn-non-send-from-init.rs @@ -0,0 +1,24 @@ +#![no_main] + +#[rtic::app(device = lm3s6965, dispatchers = [SSI0])] +mod app { + use super::*; + + #[shared] + struct Shared {} + + #[local] + struct Local {} + + #[init] + fn init(_cx: init::Context) -> (Shared, Local) { + foo::spawn(NotSendNotSync::default()).ok(); + (Shared {}, Local {}) + } + + #[task(priority = 1)] + async fn foo(_cx: foo::Context, _nsns: NotSendNotSync) {} +} + +#[derive(Default, Debug)] +struct NotSendNotSync(core::marker::PhantomData<*mut u8>); diff --git a/rtic/ui/spawn-non-send-from-init.stderr b/rtic/ui/spawn-non-send-from-init.stderr new file mode 100644 index 000000000000..f89802011ed2 --- /dev/null +++ b/rtic/ui/spawn-non-send-from-init.stderr @@ -0,0 +1,51 @@ +error[E0277]: `*mut u8` cannot be sent between threads safely + --> ui/spawn-non-send-from-init.rs:15:20 + | +15 | foo::spawn(NotSendNotSync::default()).ok(); + | ---------- ^^^^^^^^^^^^^^^^^^^^^^^^^ `*mut u8` cannot be sent between threads safely + | | + | required by a bound introduced by this call + | + = help: within `NotSendNotSync`, the trait `Send` is not implemented for `*mut u8` +note: required because it appears within the type `PhantomData<*mut u8>` + --> $RUST/core/src/marker.rs + | + | pub struct PhantomData; + | ^^^^^^^^^^^ +note: required because it appears within the type `NotSendNotSync` + --> ui/spawn-non-send-from-init.rs:24:8 + | +24 | struct NotSendNotSync(core::marker::PhantomData<*mut u8>); + | ^^^^^^^^^^^^^^ +note: required by a bound in `__rtic_internal_foo_spawn` + --> ui/spawn-non-send-from-init.rs:3:1 + | + 3 | #[rtic::app(device = lm3s6965, dispatchers = [SSI0])] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `__rtic_internal_foo_spawn` + = note: this error originates in the attribute macro `rtic::app` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: `*mut u8` cannot be shared between threads safely + --> ui/spawn-non-send-from-init.rs:15:20 + | +15 | foo::spawn(NotSendNotSync::default()).ok(); + | ---------- ^^^^^^^^^^^^^^^^^^^^^^^^^ `*mut u8` cannot be shared between threads safely + | | + | required by a bound introduced by this call + | + = help: within `NotSendNotSync`, the trait `Sync` is not implemented for `*mut u8` +note: required because it appears within the type `PhantomData<*mut u8>` + --> $RUST/core/src/marker.rs + | + | pub struct PhantomData; + | ^^^^^^^^^^^ +note: required because it appears within the type `NotSendNotSync` + --> ui/spawn-non-send-from-init.rs:24:8 + | +24 | struct NotSendNotSync(core::marker::PhantomData<*mut u8>); + | ^^^^^^^^^^^^^^ +note: required by a bound in `__rtic_internal_foo_spawn` + --> ui/spawn-non-send-from-init.rs:3:1 + | + 3 | #[rtic::app(device = lm3s6965, dispatchers = [SSI0])] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `__rtic_internal_foo_spawn` + = note: this error originates in the attribute macro `rtic::app` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/rtic/ui/task-reference-in-spawn.stderr b/rtic/ui/task-reference-in-spawn.stderr index e741a61c4d08..7e907cb661d6 100644 --- a/rtic/ui/task-reference-in-spawn.stderr +++ b/rtic/ui/task-reference-in-spawn.stderr @@ -1,3 +1,43 @@ +error[E0106]: missing lifetime specifier + --> ui/task-reference-in-spawn.rs:26:68 + | +26 | async fn high_prio_print(_: high_prio_print::Context, mut_ref: &mut usize) { + | ^ expected named lifetime parameter + | + = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from +help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static` + | +26 | async fn high_prio_print(_: high_prio_print::Context, mut_ref: &'static mut usize) { + | +++++++ +help: consider introducing a named lifetime parameter + | + 3 ~ #[rtic::app(device = lm3s6965, dispatchers = [SSI0, QEI0, GPIOA])]<'a> + 4 | mod app { +... +25 | #[task(priority = 3)] +26 ~ async fn high_prio_print(_: high_prio_print::Context, mut_ref: &'a usize) { + | +help: alternatively, you might want to return an owned value + | +26 - async fn high_prio_print(_: high_prio_print::Context, mut_ref: &mut usize) { +26 + async fn high_prio_print(_: high_prio_print::Context, mut_ref: usize) { + | + +error[E0658]: anonymous lifetimes in `impl Trait` are unstable + --> ui/task-reference-in-spawn.rs:26:69 + | +26 | async fn high_prio_print(_: high_prio_print::Context, mut_ref: &mut usize) { + | ^ expected named lifetime parameter + | +help: consider introducing a named lifetime parameter + | + 3 ~ #[rtic::app(device = lm3s6965, dispatchers = [SSI0, QEI0, GPIOA])]<'a> + 4 | mod app { +... +25 | #[task(priority = 3)] +26 ~ async fn high_prio_print(_: high_prio_print::Context, mut_ref: &'a mut usize) { + | + error[E0521]: borrowed data escapes outside of function --> ui/task-reference-in-spawn.rs:3:1 | @@ -12,3 +52,21 @@ error[E0521]: borrowed data escapes outside of function | - let's call the lifetime of this reference `'1` | = note: this error originates in the attribute macro `rtic::app` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: lifetime may not live long enough + --> ui/task-reference-in-spawn.rs:3:1 + | + 3 | #[rtic::app(device = lm3s6965, dispatchers = [SSI0, QEI0, GPIOA])] + | -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | | + | let's call the lifetime of this reference `'2` + | method was supposed to return data with lifetime `'2` but it is returning data with lifetime `'1` +... +26 | async fn high_prio_print(_: high_prio_print::Context, mut_ref: &mut usize) { + | - let's call the lifetime of this reference `'1` + | + = note: this error originates in the attribute macro `rtic::app` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider reusing a named lifetime parameter and update trait if needed + | +26 | async fn high_prio_print(_: high_prio_print::Context, mut_ref: &'a mut usize) { + | ++ From d241893922fc7497a1ed7d1cab5ff21bf373cb3e Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Thu, 13 Nov 2025 01:29:34 +0100 Subject: [PATCH 4/8] Update changelog --- rtic-macros/CHANGELOG.md | 2 +- rtic/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/rtic-macros/CHANGELOG.md b/rtic-macros/CHANGELOG.md index 4964282b3670..51c7998c892a 100644 --- a/rtic-macros/CHANGELOG.md +++ b/rtic-macros/CHANGELOG.md @@ -10,7 +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 +- Allow tasks with !Send/!Sync to be spawned from the same priority level task using local spawner ## [v2.2.0] - 2025-06-22 diff --git a/rtic/CHANGELOG.md b/rtic/CHANGELOG.md index 4774bbb6b6a0..7c19dd2013c2 100644 --- a/rtic/CHANGELOG.md +++ b/rtic/CHANGELOG.md @@ -22,6 +22,7 @@ Example: ### Added +- Add attribute `local_task` for tasks that may take args that are !Send/!Sync and can only be spawned from same executor - Outer attributes applied to RTIC app module are now forwarded to the generated code. ### Changed From 92137f2884d6d99204edcdf44069a77eb964edd2 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Wed, 19 Nov 2025 21:32:14 +0100 Subject: [PATCH 5/8] Revert moved context --- rtic-macros/src/codegen/module.rs | 60 +++++++++++++++---------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/rtic-macros/src/codegen/module.rs b/rtic-macros/src/codegen/module.rs index f421d5adc616..123456ccf727 100644 --- a/rtic-macros/src/codegen/module.rs +++ b/rtic-macros/src/codegen/module.rs @@ -113,6 +113,36 @@ 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(t) = ctxt { let spawnee = &app.software_tasks[name]; let priority = spawnee.args.priority; @@ -251,36 +281,6 @@ 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 { From e0d4627e351231b64e085c49a61a16a13778446b Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Wed, 19 Nov 2025 22:40:50 +0100 Subject: [PATCH 6/8] Ensure local_spawner is added as a field to Context --- rtic-macros/src/codegen/module.rs | 87 ++++++++++++++++--------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/rtic-macros/src/codegen/module.rs b/rtic-macros/src/codegen/module.rs index 123456ccf727..80a34189d2d6 100644 --- a/rtic-macros/src/codegen/module.rs +++ b/rtic-macros/src/codegen/module.rs @@ -113,6 +113,17 @@ 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"); + if let Context::SoftwareTask(t) = ctxt { + 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. + pub local_spawner: #local_spawner + }); + values.push(quote!(local_spawner: #local_spawner { _p: core::marker::PhantomData })); + } + items.push(quote!( #(#cfgs)* /// Execution context @@ -228,51 +239,43 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { .filter(|(_, t)| t.args.priority == priority) .collect(); - if !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. - pub local_spawner: #local_spawner - }); - let tasks = tasks_on_same_executor - .iter() - .map(|(ident, task)| { - // Copied mostly from software_tasks.rs - let internal_spawn_ident = util::internal_task_ident(ident, "spawn_helper"); - let attrs = &task.attrs; - let cfgs = &task.cfgs; - let generics = if task.is_bottom { - quote!() - } else { - quote!(<'a>) - }; - - let (_generic_input_args, 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 #(,#input_args)*) -> ::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_untupled,)*) } - } + + let local_spawner = util::internal_task_ident(t, "LocalSpawner"); + let tasks = tasks_on_same_executor + .iter() + .map(|(ident, task)| { + // Copied mostly from software_tasks.rs + let internal_spawn_ident = util::internal_task_ident(ident, "spawn_helper"); + let attrs = &task.attrs; + let cfgs = &task.cfgs; + let generics = if task.is_bottom { + quote!() + } else { + quote!(<'a>) + }; + + let (_generic_input_args, 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 #(,#input_args)*) -> ::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_untupled,)*) } } - }) - .collect::>(); - values.push(quote!(local_spawner: #local_spawner { _p: core::marker::PhantomData })); - items.push(quote! { - struct #local_spawner { - _p: core::marker::PhantomData<*mut ()>, } + }) + .collect::>(); + items.push(quote! { + struct #local_spawner { + _p: core::marker::PhantomData<*mut ()>, + } - impl #local_spawner { - #(#tasks)* - } - }); - } + impl #local_spawner { + #(#tasks)* + } + }); module_items.push(quote!( #(#cfgs)* From 82c9d75a06766bcf9763551e6614622fac08dca5 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Wed, 19 Nov 2025 22:59:25 +0100 Subject: [PATCH 7/8] Cleanup --- rtic-macros/src/codegen/module.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rtic-macros/src/codegen/module.rs b/rtic-macros/src/codegen/module.rs index 80a34189d2d6..ca8bb1940fa1 100644 --- a/rtic-macros/src/codegen/module.rs +++ b/rtic-macros/src/codegen/module.rs @@ -227,11 +227,11 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { } )); - module_items.push(quote!{ + module_items.push(quote!( #(#cfgs)* #[doc(inline)] pub use super::#internal_spawn_ident as spawn; - }); + )); let tasks_on_same_executor: Vec<_> = app .software_tasks @@ -239,7 +239,6 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { .filter(|(_, t)| t.args.priority == priority) .collect(); - let local_spawner = util::internal_task_ident(t, "LocalSpawner"); let tasks = tasks_on_same_executor .iter() From ee5f77326cf1a46f81d820567882942b99aae2f2 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Wed, 19 Nov 2025 23:04:34 +0100 Subject: [PATCH 8/8] Rename Dummy to TaskArg --- rtic-macros/src/codegen/util.rs | 6 +++--- rtic/src/export.rs | 2 +- rtic/src/export/dummy.rs | 15 --------------- rtic/src/export/task_arg.rs | 22 ++++++++++++++++++++++ 4 files changed, 26 insertions(+), 19 deletions(-) delete mode 100644 rtic/src/export/dummy.rs create mode 100644 rtic/src/export/task_arg.rs diff --git a/rtic-macros/src/codegen/util.rs b/rtic-macros/src/codegen/util.rs index c94fd8b459bc..1a7f18b94f51 100644 --- a/rtic-macros/src/codegen/util.rs +++ b/rtic-macros/src/codegen/util.rs @@ -35,7 +35,7 @@ pub fn link_section_uninit() -> TokenStream2 { pub fn regroup_inputs( inputs: &[PatType], ) -> ( - // Generic args e.g. &[`_0: impl Dummy`, `_1: impl Dummy`] + // Generic args e.g. &[`_0: impl TaskArg`, `_1: impl TaskArg`] Vec, // args e.g. &[`_0`], &[`_0: i32`, `_1: i64`] Vec, @@ -50,7 +50,7 @@ pub fn regroup_inputs( let ty = &inputs[0].ty; ( - vec![quote!(_0: impl rtic::export::dummy::Dummy + Send + Sync)], + vec![quote!(_0: impl rtic::export::task_arg::TaskArg + Send + Sync)], vec![quote!(_0: #ty)], quote!(_0), vec![quote!(_0)], @@ -66,7 +66,7 @@ pub fn regroup_inputs( let i = Ident::new(&format!("_{i}"), Span::call_site()); let ty = &input.ty; - generic_args.push(quote!(#i: impl rtic::export::dummy::Dummy + Send + Sync)); + generic_args.push(quote!(#i: impl rtic::export::task_arg::TaskArg + Send + Sync)); args.push(quote!(#i: #ty)); diff --git a/rtic/src/export.rs b/rtic/src/export.rs index 0beeafbba923..c6b0dafa5449 100644 --- a/rtic/src/export.rs +++ b/rtic/src/export.rs @@ -1,8 +1,8 @@ pub use critical_section::CriticalSection; pub use portable_atomic as atomic; -pub mod dummy; pub mod executor; +pub mod task_arg; // Cortex-M target (any) #[cfg(feature = "cortex-m")] diff --git a/rtic/src/export/dummy.rs b/rtic/src/export/dummy.rs deleted file mode 100644 index be16aeb5ae13..000000000000 --- a/rtic/src/export/dummy.rs +++ /dev/null @@ -1,15 +0,0 @@ -// TODO: What should we name this? - -/// Dummy trait which will only ever be implemented where type T is Self -pub trait Dummy { - /// This should always be same as `Self` - type T; - fn to(self) -> Self::T; -} - -impl Dummy for T { - type T = T; - fn to(self) -> T { - self - } -} diff --git a/rtic/src/export/task_arg.rs b/rtic/src/export/task_arg.rs new file mode 100644 index 000000000000..05fa59a2593c --- /dev/null +++ b/rtic/src/export/task_arg.rs @@ -0,0 +1,22 @@ +/// A trait for types that can be passed as arguments when spawning tasks +/// +/// This trait will only ever be implemented where type `Self::T` is `Self` +/// +/// The global `my_task::spawn` requires its args to be `Send`. This trait has to +/// be used because we can not have a function with a where clause which +/// requires a concrete type to be `Send` if that type is not `Send`. The compiler +/// will error out on us. However hiding that behind a dummy trait which is +/// only implemented for that same type enables us to defer the error to when +/// the user erroneously tries to call the function. +pub trait TaskArg { + /// This should always be same as `Self` + type T; + fn to(self) -> Self::T; +} + +impl TaskArg for T { + type T = T; + fn to(self) -> T { + self + } +}