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/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; diff --git a/rtic-macros/CHANGELOG.md b/rtic-macros/CHANGELOG.md index f9a9445e5d07..51c7998c892a 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. +- 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-macros/src/codegen/module.rs b/rtic-macros/src/codegen/module.rs index 1d2f90a6928b..ca8bb1940fa1 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,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 @@ -142,7 +154,7 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { 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 +170,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 +198,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 @@ -210,6 +233,49 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { 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(); + + 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::>(); + items.push(quote! { + struct #local_spawner { + _p: core::marker::PhantomData<*mut ()>, + } + + impl #local_spawner { + #(#tasks)* + } + }); + module_items.push(quote!( #(#cfgs)* #[doc(inline)] diff --git a/rtic-macros/src/codegen/util.rs b/rtic-macros/src/codegen/util.rs index dda7e2903534..1a7f18b94f51 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 TaskArg`, `_1: impl TaskArg`] + 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::task_arg::TaskArg + 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::task_arg::TaskArg + 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/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 diff --git a/rtic/src/export.rs b/rtic/src/export.rs index 0ee24cd5305d..c6b0dafa5449 100644 --- a/rtic/src/export.rs +++ b/rtic/src/export.rs @@ -2,6 +2,7 @@ pub use critical_section::CriticalSection; pub use portable_atomic as atomic; pub mod executor; +pub mod task_arg; // Cortex-M target (any) #[cfg(feature = "cortex-m")] 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 + } +} 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) { + | ++