From 5bb3ba984d606d718d6ffef548275d03e003fa9a Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Mon, 6 Oct 2025 21:55:41 +0200 Subject: [PATCH 01/16] Add the concept of local tasks with an example --- .../embassy-stm32g4/src/bin/spawn_local.rs | 42 ++++++ rtic-macros/CHANGELOG.md | 1 + rtic-macros/src/codegen/module.rs | 142 +++++++++++++----- rtic-macros/src/codegen/software_tasks.rs | 12 +- rtic-macros/src/syntax/analyze.rs | 11 +- rtic-macros/src/syntax/ast.rs | 7 + rtic-macros/src/syntax/parse.rs | 21 ++- 7 files changed, 189 insertions(+), 47 deletions(-) create mode 100644 examples/embassy-stm32g4/src/bin/spawn_local.rs 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 Date: Sun, 19 Oct 2025 13:31:39 +0200 Subject: [PATCH 02/16] Add test for spawning a local task from a different prio This ensures that there is no method on the spawning task's local spawner for spawning the local task with the other priority. --- rtic-macros/ui/spawn-local-different-exec.rs | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 rtic-macros/ui/spawn-local-different-exec.rs diff --git a/rtic-macros/ui/spawn-local-different-exec.rs b/rtic-macros/ui/spawn-local-different-exec.rs new file mode 100644 index 000000000000..e088061a86c6 --- /dev/null +++ b/rtic-macros/ui/spawn-local-different-exec.rs @@ -0,0 +1,25 @@ +#![no_main] + +#[rtic_macros::mock_app(device = mock, dispatchers = [EXTI0])] +mod app { + use super::*; + + #[shared] + struct Shared {} + + #[local] + struct Local {} + + #[init] + fn init(_cx: init::Context) -> (Shared, Local) { + (Shared {}, Local {}) + } + + #[task(priority = 1, is_local_task = true)] + async fn foo(_cx: foo::Context) {} + + #[task(priority = 2)] + async fn bar(cx: bar::Context) { + cx.local_spawner.foo().ok(); + } +} From 8fbf8a9f0e28e1566b110e864c163bfe876cdcf0 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Sun, 19 Oct 2025 13:35:31 +0200 Subject: [PATCH 03/16] Add test for spawning a local task from init This ensures that there is no method in the globaly available module with the same name as the local task. --- rtic-macros/ui/spawn-local-from-init.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 rtic-macros/ui/spawn-local-from-init.rs diff --git a/rtic-macros/ui/spawn-local-from-init.rs b/rtic-macros/ui/spawn-local-from-init.rs new file mode 100644 index 000000000000..0029a8572b36 --- /dev/null +++ b/rtic-macros/ui/spawn-local-from-init.rs @@ -0,0 +1,21 @@ +#![no_main] + +#[rtic_macros::mock_app(device = mock, dispatchers = [EXTI0])] +mod app { + use super::*; + + #[shared] + struct Shared {} + + #[local] + struct Local {} + + #[init] + fn init(_cx: init::Context) -> (Shared, Local) { + foo::spawn().ok(); + (Shared {}, Local {}) + } + + #[task(priority = 1, is_local_task = true)] + async fn foo(_cx: foo::Context) {} +} From 355b478c4343feca55480989c56c506ebad644ee Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Sun, 19 Oct 2025 13:41:03 +0200 Subject: [PATCH 04/16] Add passing test for local task with non Send/Sync arg Ensure it is possible to pass a non send and non Sync argument when spawning a local task. This is ok since it will only be possible to spawn the task on the same executor and priority level --- rtic-macros/ui/spawn-local-no-send-sync.rs | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 rtic-macros/ui/spawn-local-no-send-sync.rs diff --git a/rtic-macros/ui/spawn-local-no-send-sync.rs b/rtic-macros/ui/spawn-local-no-send-sync.rs new file mode 100644 index 000000000000..6295b86d0053 --- /dev/null +++ b/rtic-macros/ui/spawn-local-no-send-sync.rs @@ -0,0 +1,29 @@ +#![no_main] + +#[rtic_macros::mock_app(device = mock, dispatchers = [EXTI0])] +mod app { + use super::*; + + #[shared] + struct Shared {} + + #[local] + struct Local {} + + #[init] + fn init(_cx: init::Context) -> (Shared, Local) { + task1::spawn().ok(); + (Shared {}, Local {}) + } + + #[task(priority = 1)] + async fn task1(cx: task1::Context) { + cx.local_spawner.task2(Default::default()).ok(); + } + + #[task(priority = 1, is_local_task = true)] + async fn task2(_cx: task2::Context, _nsns: super::NotSendNotSync) {} +} + +#[derive(Default)] +struct NotSendNotSync(PhantomData<*mut u8>); \ No newline at end of file From 518dfae01d5adb56a21eff69ee77fcfbf019b90b Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Sun, 19 Oct 2025 14:02:18 +0200 Subject: [PATCH 05/16] Fix spawn-local-no-send-sync --- rtic-macros/ui/spawn-local-no-send-sync.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rtic-macros/ui/spawn-local-no-send-sync.rs b/rtic-macros/ui/spawn-local-no-send-sync.rs index 6295b86d0053..83d025446b4b 100644 --- a/rtic-macros/ui/spawn-local-no-send-sync.rs +++ b/rtic-macros/ui/spawn-local-no-send-sync.rs @@ -1,5 +1,7 @@ #![no_main] +use std::marker::PhantomData; + #[rtic_macros::mock_app(device = mock, dispatchers = [EXTI0])] mod app { use super::*; From d9fe447a5d56be74c3efecf88b55ad8b53029719 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Sun, 19 Oct 2025 19:11:12 +0200 Subject: [PATCH 06/16] Rename `is_local_task` to just `local_task` and allow omitting the value --- rtic-macros/src/codegen/module.rs | 10 +++---- rtic-macros/src/syntax/analyze.rs | 2 +- rtic-macros/src/syntax/ast.rs | 4 +-- rtic-macros/src/syntax/parse.rs | 46 +++++++++++++++++++------------ 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/rtic-macros/src/codegen/module.rs b/rtic-macros/src/codegen/module.rs index 6da8c8abe67f..eb7b703b8016 100644 --- a/rtic-macros/src/codegen/module.rs +++ b/rtic-macros/src/codegen/module.rs @@ -134,8 +134,8 @@ 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 { + 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 { @@ -183,7 +183,7 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { } )); - if !is_local_task { + if !local_task { module_items.push(quote!( #(#cfgs)* #[doc(inline)] @@ -194,7 +194,7 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { let local_tasks_on_same_executor: Vec<_> = app .software_tasks .iter() - .filter(|(_, t)| t.args.is_local_task && t.args.priority == priority) + .filter(|(_, t)| t.args.local_task && t.args.priority == priority) .collect(); if !local_tasks_on_same_executor.is_empty() { @@ -204,7 +204,7 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 { /// /// This is useful for tasks that take args which are !Send/!Sync. /// - /// NOTE: This only works with tasks marked `is_local_task = true` + /// 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 diff --git a/rtic-macros/src/syntax/analyze.rs b/rtic-macros/src/syntax/analyze.rs index 4da3ce63140a..63bfba65678d 100644 --- a/rtic-macros/src/syntax/analyze.rs +++ b/rtic-macros/src/syntax/analyze.rs @@ -289,7 +289,7 @@ pub(crate) fn app(app: &App) -> Result { let channel = channels.entry(spawnee_prio).or_default(); channel.tasks.insert(name.clone()); - if !spawnee.args.is_local_task { + 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()); diff --git a/rtic-macros/src/syntax/ast.rs b/rtic-macros/src/syntax/ast.rs index 92bb5a11f0a0..cfd40db8be4a 100644 --- a/rtic-macros/src/syntax/ast.rs +++ b/rtic-macros/src/syntax/ast.rs @@ -261,7 +261,7 @@ pub struct SoftwareTaskArgs { /// /// Local tasks can only be spawned from the same executor. /// However they do not require Send and Sync - pub is_local_task: bool, + pub local_task: bool, } impl Default for SoftwareTaskArgs { @@ -270,7 +270,7 @@ impl Default for SoftwareTaskArgs { priority: 0, local_resources: LocalResources::new(), shared_resources: SharedResources::new(), - is_local_task: false, + local_task: false, } } } diff --git a/rtic-macros/src/syntax/parse.rs b/rtic-macros/src/syntax/parse.rs index 18ae64e12474..0085b86896c2 100644 --- a/rtic-macros/src/syntax/parse.rs +++ b/rtic-macros/src/syntax/parse.rs @@ -8,7 +8,10 @@ mod util; use proc_macro2::TokenStream as TokenStream2; use syn::{ - braced, parse::{self, Parse, ParseStream, Parser}, token::Brace, Attribute, Ident, Item, LitBool, LitInt, Meta, Token + braced, + parse::{self, Parse, ParseStream, Parser}, + token::Brace, + Attribute, Ident, Item, LitBool, LitInt, Meta, Token, }; use crate::syntax::{ @@ -194,7 +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" => { @@ -275,19 +298,6 @@ fn task_args(tokens: TokenStream2) -> 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")); } @@ -302,7 +312,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result parse::Result Date: Sun, 19 Oct 2025 19:12:39 +0200 Subject: [PATCH 07/16] Move example and remove success test --- .../embassy-stm32g4/src/bin/spawn_local.rs | 42 ------------------ examples/lm3s6965/examples/spawn_local.rs | 44 +++++++++++++++++++ rtic-macros/ui/spawn-local-no-send-sync.rs | 31 ------------- 3 files changed, 44 insertions(+), 73 deletions(-) delete mode 100644 examples/embassy-stm32g4/src/bin/spawn_local.rs create mode 100644 examples/lm3s6965/examples/spawn_local.rs delete mode 100644 rtic-macros/ui/spawn-local-no-send-sync.rs diff --git a/examples/embassy-stm32g4/src/bin/spawn_local.rs b/examples/embassy-stm32g4/src/bin/spawn_local.rs deleted file mode 100644 index 4df2a2cdfec0..000000000000 --- a/examples/embassy-stm32g4/src/bin/spawn_local.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![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/examples/lm3s6965/examples/spawn_local.rs b/examples/lm3s6965/examples/spawn_local.rs new file mode 100644 index 000000000000..71219b7d3ee0 --- /dev/null +++ b/examples/lm3s6965/examples/spawn_local.rs @@ -0,0 +1,44 @@ +#![no_main] +#![no_std] + +use panic_semihosting as _; + +#[rtic::app(device = lm3s6965, dispatchers = [SSI0])] +mod app { + use cortex_m_semihosting::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(); + cx.local_spawner.task3(Default::default()).unwrap(); + } + + #[task(priority = 1, local_task = true)] + async fn task2(_cx: task2::Context, _nsns: NotSendNotSync) { + hprintln!("Hello from task2!"); + } + + // The value of `local_task` may be omitted and is in that case interpreted as `true` + #[task(priority = 1, local_task)] + async fn task3(_cx: task3::Context, _nsns: NotSendNotSync) { + hprintln!("Hello from task3!"); + } +} + +#[derive(Default, Debug)] +struct NotSendNotSync(core::marker::PhantomData<*mut u8>); diff --git a/rtic-macros/ui/spawn-local-no-send-sync.rs b/rtic-macros/ui/spawn-local-no-send-sync.rs deleted file mode 100644 index 83d025446b4b..000000000000 --- a/rtic-macros/ui/spawn-local-no-send-sync.rs +++ /dev/null @@ -1,31 +0,0 @@ -#![no_main] - -use std::marker::PhantomData; - -#[rtic_macros::mock_app(device = mock, dispatchers = [EXTI0])] -mod app { - use super::*; - - #[shared] - struct Shared {} - - #[local] - struct Local {} - - #[init] - fn init(_cx: init::Context) -> (Shared, Local) { - task1::spawn().ok(); - (Shared {}, Local {}) - } - - #[task(priority = 1)] - async fn task1(cx: task1::Context) { - cx.local_spawner.task2(Default::default()).ok(); - } - - #[task(priority = 1, is_local_task = true)] - async fn task2(_cx: task2::Context, _nsns: super::NotSendNotSync) {} -} - -#[derive(Default)] -struct NotSendNotSync(PhantomData<*mut u8>); \ No newline at end of file From 025460b45fd40318e106f523421758df81a9ad08 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Sun, 19 Oct 2025 19:14:25 +0200 Subject: [PATCH 08/16] Update tests for - Rename `is_local_task` to just `local_task` and allow omitting the value --- rtic-macros/ui/spawn-local-different-exec.rs | 2 +- rtic-macros/ui/spawn-local-from-init.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rtic-macros/ui/spawn-local-different-exec.rs b/rtic-macros/ui/spawn-local-different-exec.rs index e088061a86c6..12168152d80d 100644 --- a/rtic-macros/ui/spawn-local-different-exec.rs +++ b/rtic-macros/ui/spawn-local-different-exec.rs @@ -15,7 +15,7 @@ mod app { (Shared {}, Local {}) } - #[task(priority = 1, is_local_task = true)] + #[task(priority = 1, local_task)] async fn foo(_cx: foo::Context) {} #[task(priority = 2)] diff --git a/rtic-macros/ui/spawn-local-from-init.rs b/rtic-macros/ui/spawn-local-from-init.rs index 0029a8572b36..3d789d44a1a9 100644 --- a/rtic-macros/ui/spawn-local-from-init.rs +++ b/rtic-macros/ui/spawn-local-from-init.rs @@ -16,6 +16,6 @@ mod app { (Shared {}, Local {}) } - #[task(priority = 1, is_local_task = true)] + #[task(priority = 1, local_task)] async fn foo(_cx: foo::Context) {} } From 5e658e4064e1f5ad67896cee25657f0fbb062c36 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Sun, 19 Oct 2025 19:50:27 +0200 Subject: [PATCH 09/16] Move tests to rtic/ui --- {rtic-macros => rtic}/ui/spawn-local-different-exec.rs | 4 +--- rtic/ui/spawn-local-different-exec.stderr | 7 +++++++ {rtic-macros => rtic}/ui/spawn-local-from-init.rs | 4 +--- rtic/ui/spawn-local-from-init.stderr | 8 ++++++++ 4 files changed, 17 insertions(+), 6 deletions(-) rename {rtic-macros => rtic}/ui/spawn-local-different-exec.rs (82%) create mode 100644 rtic/ui/spawn-local-different-exec.stderr rename {rtic-macros => rtic}/ui/spawn-local-from-init.rs (78%) create mode 100644 rtic/ui/spawn-local-from-init.stderr diff --git a/rtic-macros/ui/spawn-local-different-exec.rs b/rtic/ui/spawn-local-different-exec.rs similarity index 82% rename from rtic-macros/ui/spawn-local-different-exec.rs rename to rtic/ui/spawn-local-different-exec.rs index 12168152d80d..13fbe5226041 100644 --- a/rtic-macros/ui/spawn-local-different-exec.rs +++ b/rtic/ui/spawn-local-different-exec.rs @@ -1,9 +1,7 @@ #![no_main] -#[rtic_macros::mock_app(device = mock, dispatchers = [EXTI0])] +#[rtic::app(device = lm3s6965, dispatchers = [SSI0, GPIOA])] mod app { - use super::*; - #[shared] struct Shared {} 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-macros/ui/spawn-local-from-init.rs b/rtic/ui/spawn-local-from-init.rs similarity index 78% rename from rtic-macros/ui/spawn-local-from-init.rs rename to rtic/ui/spawn-local-from-init.rs index 3d789d44a1a9..118ea0df43cd 100644 --- a/rtic-macros/ui/spawn-local-from-init.rs +++ b/rtic/ui/spawn-local-from-init.rs @@ -1,9 +1,7 @@ #![no_main] -#[rtic_macros::mock_app(device = mock, dispatchers = [EXTI0])] +#[rtic::app(device = lm3s6965, dispatchers = [SSI0])] mod app { - use super::*; - #[shared] struct Shared {} 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 From 96b5a81779be9fc155721471a5e355cb53c382c4 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Sun, 19 Oct 2025 19:59:58 +0200 Subject: [PATCH 10/16] Revert fmt --- rtic-macros/src/codegen/software_tasks.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/rtic-macros/src/codegen/software_tasks.rs b/rtic-macros/src/codegen/software_tasks.rs index 23618f7bdbc9..de8261bcc157 100644 --- a/rtic-macros/src/codegen/software_tasks.rs +++ b/rtic-macros/src/codegen/software_tasks.rs @@ -37,16 +37,8 @@ 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)* From 266ddd1b110d6369289c614891483a2c46d74217 Mon Sep 17 00:00:00 2001 From: datdenkikniet Date: Sun, 19 Oct 2025 17:52:12 +0200 Subject: [PATCH 11/16] Use `is_multiple_of` --- rtic-monotonics/src/systick.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtic-monotonics/src/systick.rs b/rtic-monotonics/src/systick.rs index 1f705db4dbfd..5ef2f32195a9 100644 --- a/rtic-monotonics/src/systick.rs +++ b/rtic-monotonics/src/systick.rs @@ -74,7 +74,7 @@ impl SystickBackend { /// Use the prelude macros instead. pub fn _start(mut systick: SYST, sysclk: u32, timer_hz: u32) { assert!( - (sysclk % timer_hz) == 0, + sysclk.is_multiple_of(timer_hz), "timer_hz cannot evenly divide sysclk! Please adjust the timer or sysclk frequency." ); let reload = sysclk / timer_hz - 1; From f5e86943a6f86f36e3fded5a86bc0c54bcaa37b8 Mon Sep 17 00:00:00 2001 From: datdenkikniet Date: Sun, 19 Oct 2025 18:01:57 +0200 Subject: [PATCH 12/16] task-reference-in-spawn has new format in 1.90 --- rtic/ui/task-reference-in-spawn.stderr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 09598ad9e90776f3244e15147d8ff82966b9a558 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Sun, 19 Oct 2025 23:50:12 +0200 Subject: [PATCH 13/16] Add exit to example --- examples/lm3s6965/examples/spawn_local.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/examples/lm3s6965/examples/spawn_local.rs b/examples/lm3s6965/examples/spawn_local.rs index 71219b7d3ee0..e3c320f32b0f 100644 --- a/examples/lm3s6965/examples/spawn_local.rs +++ b/examples/lm3s6965/examples/spawn_local.rs @@ -25,18 +25,12 @@ mod app { async fn task1(cx: task1::Context) { hprintln!("Hello from task1!"); cx.local_spawner.task2(Default::default()).unwrap(); - cx.local_spawner.task3(Default::default()).unwrap(); } #[task(priority = 1, local_task = true)] async fn task2(_cx: task2::Context, _nsns: NotSendNotSync) { hprintln!("Hello from task2!"); - } - - // The value of `local_task` may be omitted and is in that case interpreted as `true` - #[task(priority = 1, local_task)] - async fn task3(_cx: task3::Context, _nsns: NotSendNotSync) { - hprintln!("Hello from task3!"); + debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator } } From a599039a4eaa12572fe9b9340e0df4bacd3db475 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Sun, 19 Oct 2025 23:56:59 +0200 Subject: [PATCH 14/16] Fix example --- examples/lm3s6965/examples/spawn_local.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/lm3s6965/examples/spawn_local.rs b/examples/lm3s6965/examples/spawn_local.rs index e3c320f32b0f..4ebe1b4ad77c 100644 --- a/examples/lm3s6965/examples/spawn_local.rs +++ b/examples/lm3s6965/examples/spawn_local.rs @@ -5,7 +5,7 @@ use panic_semihosting as _; #[rtic::app(device = lm3s6965, dispatchers = [SSI0])] mod app { - use cortex_m_semihosting::hprintln; + use cortex_m_semihosting::{debug, hprintln}; use super::*; #[shared] From 7f1521b7d1ae74fc7316864030171a08334dc789 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Mon, 20 Oct 2025 00:19:05 +0200 Subject: [PATCH 15/16] Fix example --- ci/expected/lm3s6965/spawn_local.run | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 ci/expected/lm3s6965/spawn_local.run 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! From 7f7c0d4523dcd26068ec5afe90481b8f33baeb44 Mon Sep 17 00:00:00 2001 From: Albin Hedman Date: Mon, 20 Oct 2025 00:37:00 +0200 Subject: [PATCH 16/16] 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..19f6ce87be54 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 +- 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/CHANGELOG.md b/rtic/CHANGELOG.md index 03d0a2c8ba18..e52bbd6abc93 100644 --- a/rtic/CHANGELOG.md +++ b/rtic/CHANGELOG.md @@ -23,6 +23,7 @@ Example: ### 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 ### Changed