Skip to content

Commit a1777f5

Browse files
usbalbinkorken89
authored andcommitted
Add the concept of local tasks with an example
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. 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. 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 Fix spawn-local-no-send-sync Rename `is_local_task` to just `local_task` and allow omitting the value Move example and remove success test Update tests for - Rename `is_local_task` to just `local_task` and allow omitting the value Move tests to rtic/ui Revert fmt Add exit to example Fix example Fix example Update changelog
1 parent b107c3c commit a1777f5

File tree

12 files changed

+243
-43
lines changed

12 files changed

+243
-43
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Hello from task1!
2+
Hello from task2!
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#![no_main]
2+
#![no_std]
3+
4+
use panic_semihosting as _;
5+
6+
#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
7+
mod app {
8+
use cortex_m_semihosting::{debug, hprintln};
9+
use super::*;
10+
11+
#[shared]
12+
struct Shared {}
13+
14+
#[local]
15+
struct Local {}
16+
17+
#[init]
18+
fn init(_cx: init::Context) -> (Shared, Local) {
19+
task1::spawn().unwrap();
20+
//task2::spawn(Default::default()).ok(); <--- This is rejected since it is a local task
21+
(Shared {}, Local {})
22+
}
23+
24+
#[task(priority = 1)]
25+
async fn task1(cx: task1::Context) {
26+
hprintln!("Hello from task1!");
27+
cx.local_spawner.task2(Default::default()).unwrap();
28+
}
29+
30+
#[task(priority = 1, local_task = true)]
31+
async fn task2(_cx: task2::Context, _nsns: NotSendNotSync) {
32+
hprintln!("Hello from task2!");
33+
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
34+
}
35+
}
36+
37+
#[derive(Default, Debug)]
38+
struct NotSendNotSync(core::marker::PhantomData<*mut u8>);

rtic-macros/CHANGELOG.md

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

1616
- Outer attributes applied to RTIC app module are now forwarded to the generated code.
17+
- Add attribute `local_task` for tasks that may take args that are !Send/!Sync and can only be spawned from same executor
1718

1819
## [v2.2.0] - 2025-06-22
1920

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 local_task = app.software_tasks[t].args.local_task;
138+
let unsafety = if 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 !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.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 `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/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.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 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+
local_task: false,
267274
}
268275
}
269276
}

rtic-macros/src/syntax/parse.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use syn::{
1111
braced,
1212
parse::{self, Parse, ParseStream, Parser},
1313
token::Brace,
14-
Attribute, Ident, Item, LitInt, Meta, Token,
14+
Attribute, Ident, Item, LitBool, LitInt, Meta, Token,
1515
};
1616

1717
use crate::syntax::{
@@ -197,6 +197,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
197197
let mut shared_resources = None;
198198
let mut local_resources = None;
199199
let mut prio_span = None;
200+
let mut local_task = None;
200201

201202
loop {
202203
if input.is_empty() {
@@ -208,7 +209,27 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
208209
let ident_s = ident.to_string();
209210

210211
// Handle equal sign
211-
let _: Token![=] = input.parse()?;
212+
let eq = input.parse::<Token![=]>();
213+
214+
// Only local_task supports omitting the value
215+
if &*ident_s == "local_task" {
216+
if local_task.is_some() {
217+
return Err(parse::Error::new(
218+
ident.span(),
219+
"argument appears more than once",
220+
));
221+
}
222+
223+
if eq.is_ok() {
224+
let lit: LitBool = input.parse()?;
225+
local_task = Some(lit.value);
226+
} else {
227+
local_task = Some(true); // Default to true
228+
}
229+
break;
230+
} else if let Err(e) = eq {
231+
return Err(e);
232+
};
212233

213234
match &*ident_s {
214235
"binds" => {
@@ -291,6 +312,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
291312
}
292313
let shared_resources = shared_resources.unwrap_or_default();
293314
let local_resources = local_resources.unwrap_or_default();
315+
let local_task = local_task.unwrap_or(false);
294316

295317
Ok(if let Some(binds) = binds {
296318
// Hardware tasks can't run at anything lower than 1
@@ -317,6 +339,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
317339
priority,
318340
shared_resources,
319341
local_resources,
342+
local_task,
320343
})
321344
})
322345
})

rtic/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Example:
2323
### Added
2424

2525
- Outer attributes applied to RTIC app module are now forwarded to the generated code.
26+
- Add attribute `local_task` for tasks that may take args that are !Send/!Sync and can only be spawned from same executor
2627

2728
### Changed
2829

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#![no_main]
2+
3+
#[rtic::app(device = lm3s6965, dispatchers = [SSI0, GPIOA])]
4+
mod app {
5+
#[shared]
6+
struct Shared {}
7+
8+
#[local]
9+
struct Local {}
10+
11+
#[init]
12+
fn init(_cx: init::Context) -> (Shared, Local) {
13+
(Shared {}, Local {})
14+
}
15+
16+
#[task(priority = 1, local_task)]
17+
async fn foo(_cx: foo::Context) {}
18+
19+
#[task(priority = 2)]
20+
async fn bar(cx: bar::Context) {
21+
cx.local_spawner.foo().ok();
22+
}
23+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
error[E0609]: no field `local_spawner` on type `__rtic_internal_bar_Context<'_>`
2+
--> ui/spawn-local-different-exec.rs:21:12
3+
|
4+
21 | cx.local_spawner.foo().ok();
5+
| ^^^^^^^^^^^^^ unknown field
6+
|
7+
= note: available field is: `__rtic_internal_p`

0 commit comments

Comments
 (0)