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..4ebe1b4ad77c --- /dev/null +++ b/examples/lm3s6965/examples/spawn_local.rs @@ -0,0 +1,38 @@ +#![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 it is a local task + (Shared {}, Local {}) + } + + #[task(priority = 1)] + async fn task1(cx: task1::Context) { + hprintln!("Hello from task1!"); + cx.local_spawner.task2(Default::default()).unwrap(); + } + + #[task(priority = 1, local_task = true)] + 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/rtic-macros/CHANGELOG.md b/rtic-macros/CHANGELOG.md index f9a9445e5d07..19f6ce87be54 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. +- Add attribute `local_task` for tasks that may take args that are !Send/!Sync and can only be spawned from 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..eb7b703b8016 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 local_task = app.software_tasks[t].args.local_task; + let unsafety = if 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 !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.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 `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/syntax/analyze.rs b/rtic-macros/src/syntax/analyze.rs index 3e5e80bd76fa..63bfba65678d 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.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..cfd40db8be4a 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 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(), + local_task: false, } } } diff --git a/rtic-macros/src/syntax/parse.rs b/rtic-macros/src/syntax/parse.rs index ea7ff29409ad..0085b86896c2 100644 --- a/rtic-macros/src/syntax/parse.rs +++ b/rtic-macros/src/syntax/parse.rs @@ -11,7 +11,7 @@ use syn::{ braced, parse::{self, Parse, ParseStream, Parser}, token::Brace, - Attribute, Ident, Item, LitInt, Meta, Token, + Attribute, Ident, Item, LitBool, LitInt, Meta, Token, }; use crate::syntax::{ @@ -197,6 +197,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result parse::Result(); + + // Only local_task supports omitting the value + if &*ident_s == "local_task" { + if local_task.is_some() { + return Err(parse::Error::new( + ident.span(), + "argument appears more than once", + )); + } + + if eq.is_ok() { + let lit: LitBool = input.parse()?; + local_task = Some(lit.value); + } else { + local_task = Some(true); // Default to true + } + break; + } else if let Err(e) = eq { + return Err(e); + }; match &*ident_s { "binds" => { @@ -291,6 +312,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result parse::Result (Shared, Local) { + (Shared {}, Local {}) + } + + #[task(priority = 1, local_task)] + async fn foo(_cx: foo::Context) {} + + #[task(priority = 2)] + async fn bar(cx: bar::Context) { + cx.local_spawner.foo().ok(); + } +} diff --git a/rtic/ui/spawn-local-different-exec.stderr b/rtic/ui/spawn-local-different-exec.stderr new file mode 100644 index 000000000000..64051c3b943d --- /dev/null +++ b/rtic/ui/spawn-local-different-exec.stderr @@ -0,0 +1,7 @@ +error[E0609]: no field `local_spawner` on type `__rtic_internal_bar_Context<'_>` + --> ui/spawn-local-different-exec.rs:21:12 + | +21 | cx.local_spawner.foo().ok(); + | ^^^^^^^^^^^^^ unknown field + | + = note: available field is: `__rtic_internal_p` diff --git a/rtic/ui/spawn-local-from-init.rs b/rtic/ui/spawn-local-from-init.rs new file mode 100644 index 000000000000..118ea0df43cd --- /dev/null +++ b/rtic/ui/spawn-local-from-init.rs @@ -0,0 +1,19 @@ +#![no_main] + +#[rtic::app(device = lm3s6965, dispatchers = [SSI0])] +mod app { + #[shared] + struct Shared {} + + #[local] + struct Local {} + + #[init] + fn init(_cx: init::Context) -> (Shared, Local) { + foo::spawn().ok(); + (Shared {}, Local {}) + } + + #[task(priority = 1, local_task)] + async fn foo(_cx: foo::Context) {} +} diff --git a/rtic/ui/spawn-local-from-init.stderr b/rtic/ui/spawn-local-from-init.stderr new file mode 100644 index 000000000000..745b4f65e1b9 --- /dev/null +++ b/rtic/ui/spawn-local-from-init.stderr @@ -0,0 +1,8 @@ +error[E0425]: cannot find function `spawn` in module `foo` + --> ui/spawn-local-from-init.rs:13:14 + | +13 | foo::spawn().ok(); + | ^^^^^ not found in `foo` + | + = help: consider importing this function: + std::thread::spawn diff --git a/rtic/ui/task-reference-in-spawn.stderr b/rtic/ui/task-reference-in-spawn.stderr index 38de78c6a2dd..e741a61c4d08 100644 --- a/rtic/ui/task-reference-in-spawn.stderr +++ b/rtic/ui/task-reference-in-spawn.stderr @@ -1,7 +1,7 @@ error[E0521]: borrowed data escapes outside of function --> ui/task-reference-in-spawn.rs:3:1 | -3 | #[rtic::app(device = lm3s6965, dispatchers = [SSI0, QEI0, GPIOA])] + 3 | #[rtic::app(device = lm3s6965, dispatchers = [SSI0, QEI0, GPIOA])] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | | | `_0` is a reference that is only valid in the function body