Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions examples/embassy-stm32g4/src/bin/spawn_local.rs
Original file line number Diff line number Diff line change
@@ -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>);
1 change: 1 addition & 0 deletions rtic-macros/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
142 changes: 105 additions & 37 deletions rtic-macros/src/codegen/module.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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::<Vec<_>>();
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::<Vec<_>>();
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)*
Expand All @@ -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 {
Expand Down
12 changes: 10 additions & 2 deletions rtic-macros/src/codegen/software_tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an unrelated formatting change, or am I missing a functionality change here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an unrelated formatting change

I think so, yes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like me to revert this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, yes. If you submit it as a separate PR I can force-merge it, while getting rid of this thing bugging you

quote!('static)
} else {
quote!('a)
};
let generics = if task.is_bottom {
quote!()
} else {
quote!(<'a>)
};

user_tasks.push(quote!(
#(#attrs)*
Expand Down
11 changes: 7 additions & 4 deletions rtic-macros/src/syntax/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,13 +285,16 @@ pub(crate) fn app(app: &App) -> Result<Analysis, syn::Error> {
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
Expand Down
7 changes: 7 additions & 0 deletions rtic-macros/src/syntax/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -264,6 +270,7 @@ impl Default for SoftwareTaskArgs {
priority: 0,
local_resources: LocalResources::new(),
shared_resources: SharedResources::new(),
is_local_task: false,
}
}
}
Expand Down
21 changes: 17 additions & 4 deletions rtic-macros/src/syntax/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -197,6 +194,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
let mut shared_resources = None;
let mut local_resources = None;
let mut prio_span = None;
let mut is_local_task = None;

loop {
if input.is_empty() {
Expand Down Expand Up @@ -277,6 +275,19 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
local_resources = Some(util::parse_local_resources(input)?);
}

"is_local_task" => {
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"));
}
Expand All @@ -291,6 +302,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
}
let shared_resources = shared_resources.unwrap_or_default();
let local_resources = local_resources.unwrap_or_default();
let is_local_task = is_local_task.unwrap_or(false);

Ok(if let Some(binds) = binds {
// Hardware tasks can't run at anything lower than 1
Expand All @@ -317,6 +329,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
priority,
shared_resources,
local_resources,
is_local_task,
})
})
})
Expand Down
25 changes: 25 additions & 0 deletions rtic-macros/ui/spawn-local-different-exec.rs
Original file line number Diff line number Diff line change
@@ -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)]
Copy link
Contributor

@datdenkikniet datdenkikniet Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific need/reason not to default to all tasks at the same priority to be locally spawnable? If the executor/performance characteristics are expected to be the same, and all of the non-local spawning is unaffected, is there any need for being able to turn off local tasks?

If one of the (few) reasons is that introducing new functionality like this introduces lots of unknowns, or there is some fixed-size overhead for this, could it make more sense to put this functionality behind a cargo feature in rtic/rtic-macros instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woops, I see now that I left this as a single comment: I meant to put it up as part of a review.

I think the name change still applies, but the new question (why have it be a configuration field at all) is more important.

Copy link
Contributor

@datdenkikniet datdenkikniet Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah OK, I've thought about this a little more and now realize that this isn't an additive change, making the task local-only also removes the global spawning mechanism. That doesn't really fit behind a feature, so I guess the name change (is_local_task -> local_task) and extra allowed form are actually relevant.

To save you from clicking through the history, this was the original comment:

To be consistent, I think we should rename this to local_task, and accept both of these forms:

#[task(local_task)]
#[task(local_task = <bool>)]

Copy link
Author

@usbalbin usbalbin Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[...] this isn't an additive change [...]

Yeah exactly. Marking a task as local_task is quite a fundamental change with large limitations.

async fn foo(_cx: foo::Context) {}

#[task(priority = 2)]
async fn bar(cx: bar::Context) {
cx.local_spawner.foo().ok();
}
}
21 changes: 21 additions & 0 deletions rtic-macros/ui/spawn-local-from-init.rs
Original file line number Diff line number Diff line change
@@ -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) {}
}
Loading
Loading