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

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

View file

@ -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::<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)*
@ -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 {

View file

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

View file

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

View file

@ -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,
}
}
}

View file

@ -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<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() {
@ -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"));
}
@ -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
@ -317,6 +329,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
priority,
shared_resources,
local_resources,
is_local_task,
})
})
})