Skip to content

Commit 5bb3ba9

Browse files
committed
Add the concept of local tasks with an example
1 parent 300ad99 commit 5bb3ba9

File tree

7 files changed

+189
-47
lines changed

7 files changed

+189
-47
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#![no_main]
2+
#![no_std]
3+
4+
use core::marker::PhantomData;
5+
use rtic::app;
6+
use {defmt_rtt as _, panic_probe as _};
7+
pub mod pac {
8+
pub use embassy_stm32::pac::Interrupt as interrupt;
9+
pub use embassy_stm32::pac::*;
10+
}
11+
12+
#[app(device = pac, peripherals = false, dispatchers = [SPI1])]
13+
mod app {
14+
use super::*;
15+
16+
#[shared]
17+
struct Shared {}
18+
19+
#[local]
20+
struct Local {}
21+
22+
#[init]
23+
fn init(_cx: init::Context) -> (Shared, Local) {
24+
task1::spawn().ok();
25+
//task2::spawn(Default::default()).ok(); <--- This is rejected since it is a local task
26+
(Shared {}, Local {})
27+
}
28+
29+
#[task(priority = 1)]
30+
async fn task1(cx: task1::Context) {
31+
defmt::info!("Hello from task1!");
32+
cx.local_spawner.task2(Default::default()).ok();
33+
}
34+
35+
#[task(priority = 1, is_local_task = true)]
36+
async fn task2(_cx: task2::Context, _nsns: super::NotSendNotSync) {
37+
defmt::info!("Hello from task1!");
38+
}
39+
}
40+
41+
#[derive(Default)]
42+
struct NotSendNotSync(PhantomData<*mut u8>);

rtic-macros/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ For each category, *Added*, *Changed*, *Fixed* add new entries at the top!
1010
### Added
1111

1212
- Outer attributes applied to RTIC app module are now forwarded to the generated code.
13+
- Local spawner for spawning tasks with !Send/!Sync args on the same executor
1314

1415
## [v2.2.0] - 2025-06-22
1516

rtic-macros/src/codegen/module.rs

Lines changed: 105 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::syntax::{ast::App, Context};
22
use crate::{analyze::Analysis, codegen::bindings::interrupt_mod, codegen::util};
3+
34
use proc_macro2::TokenStream as TokenStream2;
45
use quote::quote;
56

@@ -112,37 +113,7 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 {
112113
let internal_context_name = util::internal_task_ident(name, "Context");
113114
let exec_name = util::internal_task_ident(name, "EXEC");
114115

115-
items.push(quote!(
116-
#(#cfgs)*
117-
/// Execution context
118-
#[allow(non_snake_case)]
119-
#[allow(non_camel_case_types)]
120-
pub struct #internal_context_name<'a> {
121-
#[doc(hidden)]
122-
__rtic_internal_p: ::core::marker::PhantomData<&'a ()>,
123-
#(#fields,)*
124-
}
125-
126-
#(#cfgs)*
127-
impl<'a> #internal_context_name<'a> {
128-
#[inline(always)]
129-
#[allow(missing_docs)]
130-
pub unsafe fn new(#core) -> Self {
131-
#internal_context_name {
132-
__rtic_internal_p: ::core::marker::PhantomData,
133-
#(#values,)*
134-
}
135-
}
136-
}
137-
));
138-
139-
module_items.push(quote!(
140-
#(#cfgs)*
141-
#[doc(inline)]
142-
pub use super::#internal_context_name as Context;
143-
));
144-
145-
if let Context::SoftwareTask(..) = ctxt {
116+
if let Context::SoftwareTask(t) = ctxt {
146117
let spawnee = &app.software_tasks[name];
147118
let priority = spawnee.args.priority;
148119
let cfgs = &spawnee.cfgs;
@@ -163,13 +134,21 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 {
163134
let (input_args, input_tupled, input_untupled, input_ty) =
164135
util::regroup_inputs(&spawnee.inputs);
165136

137+
let is_local_task = app.software_tasks[t].args.is_local_task;
138+
let unsafety = if is_local_task {
139+
// local tasks are only safe to call from the same executor
140+
quote! { unsafe }
141+
} else {
142+
quote! {}
143+
};
144+
166145
// Spawn caller
167146
items.push(quote!(
168147
#(#cfgs)*
169148
/// Spawns the task directly
170149
#[allow(non_snake_case)]
171150
#[doc(hidden)]
172-
pub fn #internal_spawn_ident(#(#input_args,)*) -> ::core::result::Result<(), #input_ty> {
151+
pub #unsafety fn #internal_spawn_ident(#(#input_args,)*) -> ::core::result::Result<(), #input_ty> {
173152
// SAFETY: If `try_allocate` succeeds one must call `spawn`, which we do.
174153
unsafe {
175154
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 {
204183
}
205184
));
206185

207-
module_items.push(quote!(
208-
#(#cfgs)*
209-
#[doc(inline)]
210-
pub use super::#internal_spawn_ident as spawn;
211-
));
186+
if !is_local_task {
187+
module_items.push(quote!(
188+
#(#cfgs)*
189+
#[doc(inline)]
190+
pub use super::#internal_spawn_ident as spawn;
191+
));
192+
}
193+
194+
let local_tasks_on_same_executor: Vec<_> = app
195+
.software_tasks
196+
.iter()
197+
.filter(|(_, t)| t.args.is_local_task && t.args.priority == priority)
198+
.collect();
199+
200+
if !local_tasks_on_same_executor.is_empty() {
201+
let local_spawner = util::internal_task_ident(t, "LocalSpawner");
202+
fields.push(quote! {
203+
/// Used to spawn tasks on the same executor
204+
///
205+
/// This is useful for tasks that take args which are !Send/!Sync.
206+
///
207+
/// NOTE: This only works with tasks marked `is_local_task = true`
208+
/// and which have the same priority and thus will run on the
209+
/// same executor.
210+
pub local_spawner: #local_spawner
211+
});
212+
let tasks = local_tasks_on_same_executor
213+
.iter()
214+
.map(|(ident, task)| {
215+
// Copied mostly from software_tasks.rs
216+
let internal_spawn_ident = util::internal_task_ident(ident, "spawn");
217+
let attrs = &task.attrs;
218+
let cfgs = &task.cfgs;
219+
let inputs = &task.inputs;
220+
let generics = if task.is_bottom {
221+
quote!()
222+
} else {
223+
quote!(<'a>)
224+
};
225+
let input_vals = inputs.iter().map(|i| &i.pat).collect::<Vec<_>>();
226+
let (_input_args, _input_tupled, _input_untupled, input_ty) = util::regroup_inputs(&task.inputs);
227+
quote! {
228+
#(#attrs)*
229+
#(#cfgs)*
230+
#[allow(non_snake_case)]
231+
pub(super) fn #ident #generics(&self #(,#inputs)*) -> ::core::result::Result<(), #input_ty> {
232+
// SAFETY: This is safe to call since this can only be called
233+
// from the same executor
234+
unsafe { #internal_spawn_ident(#(#input_vals,)*) }
235+
}
236+
}
237+
})
238+
.collect::<Vec<_>>();
239+
values.push(quote!(local_spawner: #local_spawner { _p: core::marker::PhantomData }));
240+
items.push(quote! {
241+
struct #local_spawner {
242+
_p: core::marker::PhantomData<*mut ()>,
243+
}
244+
245+
impl #local_spawner {
246+
#(#tasks)*
247+
}
248+
});
249+
}
212250

213251
module_items.push(quote!(
214252
#(#cfgs)*
@@ -217,6 +255,36 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 {
217255
));
218256
}
219257

258+
items.push(quote!(
259+
#(#cfgs)*
260+
/// Execution context
261+
#[allow(non_snake_case)]
262+
#[allow(non_camel_case_types)]
263+
pub struct #internal_context_name<'a> {
264+
#[doc(hidden)]
265+
__rtic_internal_p: ::core::marker::PhantomData<&'a ()>,
266+
#(#fields,)*
267+
}
268+
269+
#(#cfgs)*
270+
impl<'a> #internal_context_name<'a> {
271+
#[inline(always)]
272+
#[allow(missing_docs)]
273+
pub unsafe fn new(#core) -> Self {
274+
#internal_context_name {
275+
__rtic_internal_p: ::core::marker::PhantomData,
276+
#(#values,)*
277+
}
278+
}
279+
}
280+
));
281+
282+
module_items.push(quote!(
283+
#(#cfgs)*
284+
#[doc(inline)]
285+
pub use super::#internal_context_name as Context;
286+
));
287+
220288
if items.is_empty() {
221289
quote!()
222290
} else {

rtic-macros/src/codegen/software_tasks.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,16 @@ pub fn codegen(app: &App, analysis: &Analysis) -> TokenStream2 {
3737
let cfgs = &task.cfgs;
3838
let stmts = &task.stmts;
3939
let inputs = &task.inputs;
40-
let lifetime = if task.is_bottom { quote!('static) } else { quote!('a) };
41-
let generics = if task.is_bottom { quote!() } else { quote!(<'a>) };
40+
let lifetime = if task.is_bottom {
41+
quote!('static)
42+
} else {
43+
quote!('a)
44+
};
45+
let generics = if task.is_bottom {
46+
quote!()
47+
} else {
48+
quote!(<'a>)
49+
};
4250

4351
user_tasks.push(quote!(
4452
#(#attrs)*

rtic-macros/src/syntax/analyze.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,16 @@ pub(crate) fn app(app: &App) -> Result<Analysis, syn::Error> {
285285
for (name, spawnee) in &app.software_tasks {
286286
let spawnee_prio = spawnee.args.priority;
287287

288+
// TODO: What is this?
288289
let channel = channels.entry(spawnee_prio).or_default();
289290
channel.tasks.insert(name.clone());
290291

291-
// All inputs are send as we do not know from where they may be spawned.
292-
spawnee.inputs.iter().for_each(|input| {
293-
send_types.insert(input.ty.clone());
294-
});
292+
if !spawnee.args.is_local_task {
293+
// All inputs are send as we do not know from where they may be spawned.
294+
spawnee.inputs.iter().for_each(|input| {
295+
send_types.insert(input.ty.clone());
296+
});
297+
}
295298
}
296299

297300
// No channel should ever be empty

rtic-macros/src/syntax/ast.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,12 @@ pub struct SoftwareTaskArgs {
256256

257257
/// Shared resources that can be accessed from this context
258258
pub shared_resources: SharedResources,
259+
260+
/// Local tasks
261+
///
262+
/// Local tasks can only be spawned from the same executor.
263+
/// However they do not require Send and Sync
264+
pub is_local_task: bool,
259265
}
260266

261267
impl Default for SoftwareTaskArgs {
@@ -264,6 +270,7 @@ impl Default for SoftwareTaskArgs {
264270
priority: 0,
265271
local_resources: LocalResources::new(),
266272
shared_resources: SharedResources::new(),
273+
is_local_task: false,
267274
}
268275
}
269276
}

rtic-macros/src/syntax/parse.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ mod util;
88

99
use proc_macro2::TokenStream as TokenStream2;
1010
use syn::{
11-
braced,
12-
parse::{self, Parse, ParseStream, Parser},
13-
token::Brace,
14-
Attribute, Ident, Item, LitInt, Meta, Token,
11+
braced, parse::{self, Parse, ParseStream, Parser}, token::Brace, Attribute, Ident, Item, LitBool, LitInt, Meta, Token
1512
};
1613

1714
use crate::syntax::{
@@ -197,6 +194,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
197194
let mut shared_resources = None;
198195
let mut local_resources = None;
199196
let mut prio_span = None;
197+
let mut is_local_task = None;
200198

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

278+
"is_local_task" => {
279+
if is_local_task.is_some() {
280+
return Err(parse::Error::new(
281+
ident.span(),
282+
"argument appears more than once",
283+
));
284+
}
285+
286+
let lit: LitBool = input.parse()?;
287+
288+
is_local_task = Some(lit.value);
289+
}
290+
280291
_ => {
281292
return Err(parse::Error::new(ident.span(), "unexpected argument"));
282293
}
@@ -291,6 +302,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
291302
}
292303
let shared_resources = shared_resources.unwrap_or_default();
293304
let local_resources = local_resources.unwrap_or_default();
305+
let is_local_task = is_local_task.unwrap_or(false);
294306

295307
Ok(if let Some(binds) = binds {
296308
// Hardware tasks can't run at anything lower than 1
@@ -317,6 +329,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
317329
priority,
318330
shared_resources,
319331
local_resources,
332+
is_local_task,
320333
})
321334
})
322335
})

0 commit comments

Comments
 (0)