Add the concept of local tasks with an example

This commit is contained in:
Albin Hedman 2025-10-06 21:55:41 +02:00
parent 300ad99b74
commit 5bb3ba984d
7 changed files with 189 additions and 47 deletions

View file

@ -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>);

View file

@ -10,6 +10,7 @@ For each category, *Added*, *Changed*, *Fixed* add new entries at the top!
### Added ### Added
- Outer attributes applied to RTIC app module are now forwarded to the generated code. - 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 ## [v2.2.0] - 2025-06-22

View file

@ -1,5 +1,6 @@
use crate::syntax::{ast::App, Context}; use crate::syntax::{ast::App, Context};
use crate::{analyze::Analysis, codegen::bindings::interrupt_mod, codegen::util}; use crate::{analyze::Analysis, codegen::bindings::interrupt_mod, codegen::util};
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use quote::quote; 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 internal_context_name = util::internal_task_ident(name, "Context");
let exec_name = util::internal_task_ident(name, "EXEC"); let exec_name = util::internal_task_ident(name, "EXEC");
items.push(quote!( if let Context::SoftwareTask(t) = ctxt {
#(#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 {
let spawnee = &app.software_tasks[name]; let spawnee = &app.software_tasks[name];
let priority = spawnee.args.priority; let priority = spawnee.args.priority;
let cfgs = &spawnee.cfgs; 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) = let (input_args, input_tupled, input_untupled, input_ty) =
util::regroup_inputs(&spawnee.inputs); 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 // Spawn caller
items.push(quote!( items.push(quote!(
#(#cfgs)* #(#cfgs)*
/// Spawns the task directly /// Spawns the task directly
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[doc(hidden)] #[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. // SAFETY: If `try_allocate` succeeds one must call `spawn`, which we do.
unsafe { unsafe {
let exec = rtic::export::executor::AsyncTaskExecutor::#from_ptr_n_args(#name, &#exec_name); 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!( if !is_local_task {
#(#cfgs)* module_items.push(quote!(
#[doc(inline)] #(#cfgs)*
pub use super::#internal_spawn_ident as spawn; #[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!( module_items.push(quote!(
#(#cfgs)* #(#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() { if items.is_empty() {
quote!() quote!()
} else { } else {

View file

@ -37,8 +37,16 @@ pub fn codegen(app: &App, analysis: &Analysis) -> TokenStream2 {
let cfgs = &task.cfgs; let cfgs = &task.cfgs;
let stmts = &task.stmts; let stmts = &task.stmts;
let inputs = &task.inputs; let inputs = &task.inputs;
let lifetime = if task.is_bottom { quote!('static) } else { quote!('a) }; let lifetime = if task.is_bottom {
let generics = if task.is_bottom { quote!() } else { quote!(<'a>) }; quote!('static)
} else {
quote!('a)
};
let generics = if task.is_bottom {
quote!()
} else {
quote!(<'a>)
};
user_tasks.push(quote!( user_tasks.push(quote!(
#(#attrs)* #(#attrs)*

View file

@ -285,13 +285,16 @@ pub(crate) fn app(app: &App) -> Result<Analysis, syn::Error> {
for (name, spawnee) in &app.software_tasks { for (name, spawnee) in &app.software_tasks {
let spawnee_prio = spawnee.args.priority; let spawnee_prio = spawnee.args.priority;
// TODO: What is this?
let channel = channels.entry(spawnee_prio).or_default(); let channel = channels.entry(spawnee_prio).or_default();
channel.tasks.insert(name.clone()); channel.tasks.insert(name.clone());
// All inputs are send as we do not know from where they may be spawned. if !spawnee.args.is_local_task {
spawnee.inputs.iter().for_each(|input| { // All inputs are send as we do not know from where they may be spawned.
send_types.insert(input.ty.clone()); spawnee.inputs.iter().for_each(|input| {
}); send_types.insert(input.ty.clone());
});
}
} }
// No channel should ever be empty // No channel should ever be empty

View file

@ -256,6 +256,12 @@ pub struct SoftwareTaskArgs {
/// Shared resources that can be accessed from this context /// Shared resources that can be accessed from this context
pub shared_resources: SharedResources, 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 { impl Default for SoftwareTaskArgs {
@ -264,6 +270,7 @@ impl Default for SoftwareTaskArgs {
priority: 0, priority: 0,
local_resources: LocalResources::new(), local_resources: LocalResources::new(),
shared_resources: SharedResources::new(), shared_resources: SharedResources::new(),
is_local_task: false,
} }
} }
} }

View file

@ -8,10 +8,7 @@ mod util;
use proc_macro2::TokenStream as TokenStream2; use proc_macro2::TokenStream as TokenStream2;
use syn::{ use syn::{
braced, braced, parse::{self, Parse, ParseStream, Parser}, token::Brace, Attribute, Ident, Item, LitBool, LitInt, Meta, Token
parse::{self, Parse, ParseStream, Parser},
token::Brace,
Attribute, Ident, Item, LitInt, Meta, Token,
}; };
use crate::syntax::{ use crate::syntax::{
@ -197,6 +194,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
let mut shared_resources = None; let mut shared_resources = None;
let mut local_resources = None; let mut local_resources = None;
let mut prio_span = None; let mut prio_span = None;
let mut is_local_task = None;
loop { loop {
if input.is_empty() { if input.is_empty() {
@ -277,6 +275,19 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
local_resources = Some(util::parse_local_resources(input)?); 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")); return Err(parse::Error::new(ident.span(), "unexpected argument"));
} }
@ -291,6 +302,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
} }
let shared_resources = shared_resources.unwrap_or_default(); let shared_resources = shared_resources.unwrap_or_default();
let local_resources = local_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 { Ok(if let Some(binds) = binds {
// Hardware tasks can't run at anything lower than 1 // Hardware tasks can't run at anything lower than 1
@ -317,6 +329,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
priority, priority,
shared_resources, shared_resources,
local_resources, local_resources,
is_local_task,
}) })
}) })
}) })