Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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!
39 changes: 39 additions & 0 deletions examples/lm3s6965/examples/spawn_local.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#![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 not all args are Send and Sync
(Shared {}, Local {})
}

#[task(priority = 1)]
async fn task1(cx: task1::Context) {
hprintln!("Hello from task1!");
cx.local_spawner.task2(Default::default()).unwrap();
}

// Task where some args are !Send/!Sync
#[task(priority = 1)]
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>);
4 changes: 2 additions & 2 deletions examples/lm3s6965/examples/wait-queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ mod app {
fn init(cx: init::Context) -> (Shared, Local) {
Mono::start(cx.core.SYST, 12_000_000);

incrementer::spawn(cx.local.wait_queue).ok().unwrap();
waiter::spawn(cx.local.wait_queue).ok().unwrap();
incrementer::spawn(&*cx.local.wait_queue).ok().unwrap();
waiter::spawn(&*cx.local.wait_queue).ok().unwrap();
Comment on lines -31 to +32
Copy link
Author

Choose a reason for hiding this comment

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

This is a breaking change. For some reason the compiler can not auto demote &mut T to &T when generics are involved (not sure of the rules here).

Without the above fix, we get

error[E0271]: type mismatch resolving `<&mut DoublyLinkedList<Waker> as Dummy>::T == &DoublyLinkedList<Waker>`
  --> examples/wait-queue.rs:31:28
   |
31 |         incrementer::spawn(cx.local.wait_queue).ok().unwrap();
   |         ------------------ ^^^^^^^^^^^^^^^^^^^ type mismatch resolving `<&mut DoublyLinkedList<Waker> as Dummy>::T == &DoublyLinkedList<Waker>`
   |         |
   |         required by a bound introduced by this call
   |
note: expected this to be `&'static DoublyLinkedList<Waker>`
  --> examples/wait-queue.rs:11:1

Copy link
Author

Choose a reason for hiding this comment

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


let count = 0;

Expand Down
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.
- Allow tasks with !Send/!Sync to be spawned from the same priority level task using local spawner

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

Expand Down
136 changes: 100 additions & 36 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 @@ -158,18 +129,21 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 {
};

let internal_spawn_ident = util::internal_task_ident(name, "spawn");
let internal_spawn_helper_ident = util::internal_task_ident(name, "spawn_helper");
let internal_waker_ident = util::internal_task_ident(name, "waker");
let from_ptr_n_args = util::from_ptr_n_args_ident(spawnee.inputs.len());
let (input_args, input_tupled, input_untupled, input_ty) =
let (generic_input_args, input_args, input_tupled, input_untupled, input_ty) =
util::regroup_inputs(&spawnee.inputs);

// Spawn caller
items.push(quote!(
#(#cfgs)*
/// Spawns the task directly
/// Spawns the task without checking if the spawner and spawnee are the same priority
///
/// SAFETY: The caller needs to check that the spawner and spawnee are the same priority
#[allow(non_snake_case)]
#[doc(hidden)]
pub fn #internal_spawn_ident(#(#input_args,)*) -> ::core::result::Result<(), #input_ty> {
pub unsafe fn #internal_spawn_helper_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 All @@ -183,6 +157,14 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 {
}
}
}

/// Spawns the task directly
#[allow(non_snake_case)]
#[doc(hidden)]
pub fn #internal_spawn_ident(#(#generic_input_args,)*) -> ::core::result::Result<(), #input_ty> {
// SAFETY: The generic args require Send + Sync
unsafe { #internal_spawn_helper_ident(#(#input_untupled.to()),*) }
}
));

// Waker
Expand All @@ -204,11 +186,63 @@ pub fn codegen(ctxt: Context, app: &App, analysis: &Analysis) -> TokenStream2 {
}
));

module_items.push(quote!(
module_items.push(quote!{
#(#cfgs)*
#[doc(inline)]
pub use super::#internal_spawn_ident as spawn;
));
});

let tasks_on_same_executor: Vec<_> = app
.software_tasks
.iter()
.filter(|(_, t)| t.args.priority == priority)
.collect();

if !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.
pub local_spawner: #local_spawner
});
let tasks = tasks_on_same_executor
.iter()
.map(|(ident, task)| {
// Copied mostly from software_tasks.rs
let internal_spawn_ident = util::internal_task_ident(ident, "spawn_helper");
let attrs = &task.attrs;
let cfgs = &task.cfgs;
let generics = if task.is_bottom {
quote!()
} else {
quote!(<'a>)
};

let (_generic_input_args, 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 #(,#input_args)*) -> ::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_untupled,)*) }
}
}
})
.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 +251,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
8 changes: 7 additions & 1 deletion rtic-macros/src/codegen/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub fn link_section_uninit() -> TokenStream2 {
pub fn regroup_inputs(
inputs: &[PatType],
) -> (
// Generic args e.g. &[`_0: impl Dummy<T=i32>`, `_1: impl Dummy<T=i64>`]
Vec<TokenStream2>,
// args e.g. &[`_0`], &[`_0: i32`, `_1: i64`]
Vec<TokenStream2>,
// tupled e.g. `_0`, `(_0, _1)`
Expand All @@ -48,12 +50,14 @@ pub fn regroup_inputs(
let ty = &inputs[0].ty;

(
vec![quote!(_0: impl rtic::export::dummy::Dummy<T=#ty> + Send + Sync)],
vec![quote!(_0: #ty)],
quote!(_0),
vec![quote!(_0)],
quote!(#ty),
)
} else {
let mut generic_args = vec![];
let mut args = vec![];
let mut pats = vec![];
let mut tys = vec![];
Expand All @@ -62,6 +66,8 @@ pub fn regroup_inputs(
let i = Ident::new(&format!("_{i}"), Span::call_site());
let ty = &input.ty;

generic_args.push(quote!(#i: impl rtic::export::dummy::Dummy<T=#ty> + Send + Sync));

args.push(quote!(#i: #ty));

pats.push(quote!(#i));
Expand All @@ -74,7 +80,7 @@ pub fn regroup_inputs(
quote!((#(#pats,)*))
};
let ty = quote!((#(#tys,)*));
(args, tupled, pats, ty)
(generic_args, args, tupled, pats, ty)
}
}

Expand Down
5 changes: 0 additions & 5 deletions rtic-macros/src/syntax/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,11 +287,6 @@ pub(crate) fn app(app: &App) -> Result<Analysis, syn::Error> {

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());
});
}

// No channel should ever be empty
Expand Down
1 change: 1 addition & 0 deletions rtic/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Example:

### Added

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

### Changed
Expand Down
1 change: 1 addition & 0 deletions rtic/src/export.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub use critical_section::CriticalSection;
pub use portable_atomic as atomic;

pub mod dummy;
pub mod executor;

// Cortex-M target (any)
Expand Down
15 changes: 15 additions & 0 deletions rtic/src/export/dummy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// TODO: What should we name this?

/// Dummy trait which will only ever be implemented where type T is Self
pub trait Dummy {
Copy link
Author

Choose a reason for hiding this comment

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

Any suggestions for what to call this?

Copy link
Author

Choose a reason for hiding this comment

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

This is used because we can not have a function with a where clause which requires a concrete type to be Send if that type is not Send. The compiler will error out on us.
However hiding that behind a dummy trait which is only implemented for that same type then we defer the error to when the user tries to call the function

Copy link
Author

Choose a reason for hiding this comment

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

I went with TaskArg for now

/// This should always be same as `Self`
type T;
fn to(self) -> Self::T;
}

impl<T> Dummy for T {
type T = T;
fn to(self) -> T {
self
}
}
28 changes: 28 additions & 0 deletions rtic/ui/spawn-non-send-different-exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#![no_main]

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

#[shared]
struct Shared {}

#[local]
struct Local {}

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

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

#[task(priority = 2)]
async fn bar(cx: bar::Context) {
cx.local_spawner.foo(Default::default()).ok();
}
}

#[derive(Default, Debug)]
struct NotSendNotSync(core::marker::PhantomData<*mut u8>);
8 changes: 8 additions & 0 deletions rtic/ui/spawn-non-send-different-exec.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
error[E0599]: no method named `foo` found for struct `__rtic_internal_bar_LocalSpawner` in the current scope
--> ui/spawn-non-send-different-exec.rs:23:26
|
3 | #[rtic::app(device = lm3s6965, dispatchers = [SSI0, GPIOA])]
| ------------------------------------------------------------ method `foo` not found for this struct
...
23 | cx.local_spawner.foo(Default::default()).ok();
| ^^^ method not found in `__rtic_internal_bar_LocalSpawner`
24 changes: 24 additions & 0 deletions rtic/ui/spawn-non-send-from-init.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#![no_main]

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

#[shared]
struct Shared {}

#[local]
struct Local {}

#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
foo::spawn(NotSendNotSync::default()).ok();
(Shared {}, Local {})
}

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

#[derive(Default, Debug)]
struct NotSendNotSync(core::marker::PhantomData<*mut u8>);
Loading