Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions ci/expected/lm3s6965/spawn_local.run
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Hello from task1!
Hello from task2!
38 changes: 38 additions & 0 deletions examples/lm3s6965/examples/spawn_local.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#![no_main]
#![no_std]

use panic_semihosting as _;

#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
use super::*;

#[shared]
struct Shared {}

#[local]
struct Local {}

#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
task1::spawn().unwrap();
//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) {
hprintln!("Hello from task1!");
cx.local_spawner.task2(Default::default()).unwrap();
}

#[task(priority = 1, local_task = true)]
async fn task2(_cx: task2::Context, _nsns: NotSendNotSync) {
hprintln!("Hello from task2!");
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}

#[derive(Default, Debug)]
struct NotSendNotSync(core::marker::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.
- Add attribute `local_task` for tasks that may take args that are !Send/!Sync and can only be spawned from 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 local_task = app.software_tasks[t].args.local_task;
let unsafety = if 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 !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.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 `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
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.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());
});
}
Comment on lines -291 to +297
Copy link
Author

Choose a reason for hiding this comment

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

@korken89

As I see it, the only optimization this does is that you can only generate the local spawn when is_local_task = true, and not generate the global one.
My thought is that instead: for all tasks generate local spawn to all other tasks of the same prio AND keep the current global spawn. Then you should not need the is_local_task parameter.

They fill different use-cases. Global spawn for example is so a driver can start something from the depths of a driver given only an function pointer, where local spawns are to support better interoperability with drivers that assume you run at the same priority.

As is now, we need the attribute to know if it is safe to skip the check for Send/Sync. The global spawn would be unsafe to call with !Send/!Sync args. But I get the feeling that I do not quite understand :)

Copy link
Author

Choose a reason for hiding this comment

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

As for always populating the local spawner with tasks of same prio, sure absolutely! Should the task also have itself in its local spawner?

Copy link
Collaborator

Choose a reason for hiding this comment

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

The local spawn will have !Send, while the global spawn will always require Send - as global spawn can never know which one is calling it.
So the only ones that can alleviate the Send requirements will be the local spawns.

Copy link
Author

Choose a reason for hiding this comment

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

[...] the global spawn will always require Send [...]

Yes, and right now this is enforced by this assert which is emitted for all send_types from above, right?

This PR removes the assert and the global spawn only for the tasks marked with the attribute. If we remove the attribute then how will the macro know when it should skip generating the assert and global spawn?

We could emit where clauses on the global spawn function which checks that all the arguments impl Send + Sync like this:

fn spawn(x: u8, y: NotSendSync)
    where u8: Send + Sync, NotSendSync: Send + Sync { ... }

However that fails to compile with !Send/Sync even if not calling the types since we are using concrete types.

When creating dummy trait as done in my comment. This defers the compile error to when the function is called. This would, I think, do the same thing as the assert but only at the call site. This way we can replace the asserts with where clauses and then just always emit all the global spawn functions since it will be impossible to call them with !Send/!Sync types.

Or do you have something different in mind?

}

// 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 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(),
local_task: false,
}
}
}
Expand Down
27 changes: 25 additions & 2 deletions rtic-macros/src/syntax/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use syn::{
braced,
parse::{self, Parse, ParseStream, Parser},
token::Brace,
Attribute, Ident, Item, LitInt, Meta, Token,
Attribute, Ident, Item, LitBool, LitInt, Meta, Token,
};

use crate::syntax::{
Expand Down Expand Up @@ -197,6 +197,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 local_task = None;

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

// Handle equal sign
let _: Token![=] = input.parse()?;
let eq = input.parse::<Token![=]>();

// Only local_task supports omitting the value
if &*ident_s == "local_task" {
if local_task.is_some() {
return Err(parse::Error::new(
ident.span(),
"argument appears more than once",
));
}

if eq.is_ok() {
let lit: LitBool = input.parse()?;
local_task = Some(lit.value);
} else {
local_task = Some(true); // Default to true
}
break;
} else if let Err(e) = eq {
return Err(e);
};

match &*ident_s {
"binds" => {
Expand Down Expand Up @@ -291,6 +312,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 local_task = 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 +339,7 @@ fn task_args(tokens: TokenStream2) -> parse::Result<Either<HardwareTaskArgs, Sof
priority,
shared_resources,
local_resources,
local_task,
})
})
})
Expand Down
2 changes: 1 addition & 1 deletion rtic-monotonics/src/systick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ impl SystickBackend {
/// Use the prelude macros instead.
pub fn _start(mut systick: SYST, sysclk: u32, timer_hz: u32) {
assert!(
(sysclk % timer_hz) == 0,
sysclk.is_multiple_of(timer_hz),
"timer_hz cannot evenly divide sysclk! Please adjust the timer or sysclk frequency."
);
let reload = sysclk / timer_hz - 1;
Expand Down
1 change: 1 addition & 0 deletions rtic/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Example:
### Added

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

### Changed

Expand Down
23 changes: 23 additions & 0 deletions rtic/ui/spawn-local-different-exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#![no_main]

#[rtic::app(device = lm3s6965, dispatchers = [SSI0, GPIOA])]
mod app {
#[shared]
struct Shared {}

#[local]
struct Local {}

#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
(Shared {}, Local {})
}

#[task(priority = 1, local_task)]
async fn foo(_cx: foo::Context) {}

#[task(priority = 2)]
async fn bar(cx: bar::Context) {
cx.local_spawner.foo().ok();
}
}
7 changes: 7 additions & 0 deletions rtic/ui/spawn-local-different-exec.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
error[E0609]: no field `local_spawner` on type `__rtic_internal_bar_Context<'_>`
--> ui/spawn-local-different-exec.rs:21:12
|
21 | cx.local_spawner.foo().ok();
| ^^^^^^^^^^^^^ unknown field
|
= note: available field is: `__rtic_internal_p`
Loading
Loading