Skip to content

Commit 406b628

Browse files
committed
zephyr: Add proc macro for thread declaration
Implement a proc macro that allows a declaration like: ```rust fn mythread(arg: usize, arg2: &'static Thing) { .. } ``` With this change, creation of threads, with arbitrary "Send" arguments can be done without needing allocation. The macros reserves a static area alongside the `k_thread` structure to use for handing the threads over. This creates a function `mythread` with the given args that returns a ReadyThread. This has a `start()` method which will begin execution of the thread. This results in a RunningThread, which has a join method that can be used to wait for termination. Signed-off-by: David Brown <[email protected]>
1 parent bcc9ee4 commit 406b628

File tree

6 files changed

+654
-0
lines changed

6 files changed

+654
-0
lines changed

zephyr-macros/Cargo.toml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "zephyr-macros"
3+
version = "0.1.0"
4+
edition = "2024"
5+
license = "MIT OR Apache-2.0"
6+
descriptions = "Macros for managing tasks and work queues in Zephyr"
7+
8+
[lib]
9+
proc-macro = true
10+
11+
[dependencies]
12+
syn = { version = "2.0.85", features = ["full", "visit"] }
13+
quote = "1.0.37"
14+
proc-macro2 = "1.0.86"
15+
darling = "0.20.1"

zephyr-macros/src/lib.rs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//! Zephyr macros
2+
3+
use proc_macro::TokenStream;
4+
5+
mod task;
6+
7+
/// Declares a Zephyr thread (or pool of threads) that can be spawned.
8+
///
9+
/// There are some restrictions on this:
10+
/// - All arguments to the function must be Send.
11+
/// - The function must not use generics.
12+
/// - The optional `pool_size` attribute must be 1 or greater.
13+
/// - The `stack_size` must be specified, and will set the size of the pre-defined stack for _each_
14+
/// task in the pool.
15+
///
16+
/// ## Examples
17+
///
18+
/// Declaring a task with a simple argument:
19+
///
20+
/// ```rust
21+
/// #[zephyr::thread(stack_size = 1024)]
22+
/// fn mytask(arg: u32) {
23+
/// // Function body.
24+
/// }
25+
/// ```
26+
///
27+
/// The result will be a function `mytask` that takes this argument, and returns a `ReadyThread`. A
28+
/// simple use case is to call `.start()` on this, to start the Zephyr thread.
29+
///
30+
/// Threads can be reused after they have exited. Calling the `mytask` function before the thread
31+
/// has exited will result in a panic. The `RunningThread`'s `join` method can be used to wait for
32+
/// thread termination.
33+
#[proc_macro_attribute]
34+
pub fn thread(args: TokenStream, item: TokenStream) -> TokenStream {
35+
task::run(args.into(), item.into()).into()
36+
}

zephyr-macros/src/task.rs

+271
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
//! Expansion of `#[zephyr::task(...)]`.
2+
3+
use std::fmt::Display;
4+
5+
use darling::FromMeta;
6+
use darling::export::NestedMeta;
7+
use proc_macro2::{Span, TokenStream};
8+
use quote::{ToTokens, format_ident, quote};
9+
use syn::{
10+
Expr, ExprLit, ItemFn, Lit, LitInt, ReturnType, Type,
11+
visit::{self, Visit},
12+
};
13+
14+
#[derive(Debug, FromMeta, Default)]
15+
struct Args {
16+
#[darling(default)]
17+
pool_size: Option<syn::Expr>,
18+
#[darling(default)]
19+
stack_size: Option<syn::Expr>,
20+
}
21+
22+
pub fn run(args: TokenStream, item: TokenStream) -> TokenStream {
23+
let mut errors = TokenStream::new();
24+
25+
// If any of the steps for this macro fail, we still want to expand to an item that is as close
26+
// to the expected output as possible. This helps out IDEs such that completions and other
27+
// related features keep working.
28+
let f: ItemFn = match syn::parse2(item.clone()) {
29+
Ok(x) => x,
30+
Err(e) => return token_stream_with_error(item, e),
31+
};
32+
33+
let args = match NestedMeta::parse_meta_list(args) {
34+
Ok(x) => x,
35+
Err(e) => return token_stream_with_error(item, e),
36+
};
37+
38+
let args = match Args::from_list(&args) {
39+
Ok(x) => x,
40+
Err(e) => {
41+
errors.extend(e.write_errors());
42+
Args::default()
43+
}
44+
};
45+
46+
let pool_size = args.pool_size.unwrap_or(Expr::Lit(ExprLit {
47+
attrs: vec![],
48+
lit: Lit::Int(LitInt::new("1", Span::call_site())),
49+
}));
50+
51+
let stack_size = args.stack_size.unwrap_or(Expr::Lit(ExprLit {
52+
attrs: vec![],
53+
// TODO: Instead of a default, require this.
54+
lit: Lit::Int(LitInt::new("2048", Span::call_site())),
55+
}));
56+
57+
if !f.sig.asyncness.is_none() {
58+
error(&mut errors, &f.sig, "thread function must not be async");
59+
}
60+
61+
if !f.sig.generics.params.is_empty() {
62+
error(&mut errors, &f.sig, "thread function must not be generic");
63+
}
64+
65+
if !f.sig.generics.where_clause.is_none() {
66+
error(
67+
&mut errors,
68+
&f.sig,
69+
"thread function must not have `where` clauses",
70+
);
71+
}
72+
73+
if !f.sig.abi.is_none() {
74+
error(
75+
&mut errors,
76+
&f.sig,
77+
"thread function must not have an ABI qualifier",
78+
);
79+
}
80+
81+
if !f.sig.variadic.is_none() {
82+
error(&mut errors, &f.sig, "thread function must not be variadic");
83+
}
84+
85+
match &f.sig.output {
86+
ReturnType::Default => {}
87+
ReturnType::Type(_, ty) => match &**ty {
88+
Type::Tuple(tuple) if tuple.elems.is_empty() => {}
89+
Type::Never(_) => {}
90+
_ => error(
91+
&mut errors,
92+
&f.sig,
93+
"thread functions must either not return a value, return (), or return `!`",
94+
),
95+
},
96+
}
97+
98+
let mut args = Vec::new();
99+
let mut fargs = f.sig.inputs.clone();
100+
let mut inner_calling = Vec::new();
101+
let mut inner_args = Vec::new();
102+
103+
for arg in fargs.iter_mut() {
104+
match arg {
105+
syn::FnArg::Receiver(_) => {
106+
error(
107+
&mut errors,
108+
arg,
109+
"thread functions must not have `self` arguments",
110+
);
111+
}
112+
syn::FnArg::Typed(t) => {
113+
check_arg_ty(&mut errors, &t.ty);
114+
match t.pat.as_mut() {
115+
syn::Pat::Ident(id) => {
116+
id.mutability = None;
117+
args.push((id.clone(), t.attrs.clone()));
118+
inner_calling.push(quote! {
119+
data.#id,
120+
});
121+
inner_args.push(quote! {#id,});
122+
}
123+
_ => {
124+
error(
125+
&mut errors,
126+
arg,
127+
"pattern matching in task arguments is not yet supported",
128+
);
129+
}
130+
}
131+
}
132+
}
133+
}
134+
135+
let thread_ident = f.sig.ident.clone();
136+
let thread_inner_ident = format_ident!("__{}_thread", thread_ident);
137+
138+
let mut thread_inner = f.clone();
139+
let visibility = thread_inner.vis.clone();
140+
thread_inner.vis = syn::Visibility::Inherited;
141+
thread_inner.sig.ident = thread_inner_ident.clone();
142+
143+
// Assemble the original input arguments.
144+
let mut full_args = Vec::new();
145+
for (arg, cfgs) in &args {
146+
full_args.push(quote! {
147+
#(#cfgs)*
148+
#arg
149+
});
150+
}
151+
152+
let mut thread_outer_body = quote! {
153+
const _ZEPHYR_INTERNAL_STACK_SIZE: usize = zephyr::thread::stack_len(#stack_size);
154+
const _ZEPHYR_INTERNAL_POOL_SIZE: usize = #pool_size;
155+
struct _ZephyrInternalArgs {
156+
// This depends on the argument syntax being valid as a struct definition, which should
157+
// be the case with the above constraints.
158+
#fargs
159+
}
160+
161+
static THREAD: [zephyr::thread::ThreadData<_ZephyrInternalArgs>; _ZEPHYR_INTERNAL_POOL_SIZE]
162+
= [const { zephyr::thread::ThreadData::new() }; _ZEPHYR_INTERNAL_POOL_SIZE];
163+
#[unsafe(link_section = ".noinit.TODO_STACK")]
164+
static STACK: [zephyr::thread::ThreadStack<_ZEPHYR_INTERNAL_STACK_SIZE>; _ZEPHYR_INTERNAL_POOL_SIZE]
165+
= [const { zephyr::thread::ThreadStack::new() }; _ZEPHYR_INTERNAL_POOL_SIZE];
166+
167+
extern "C" fn startup(
168+
arg0: *mut ::core::ffi::c_void,
169+
_: *mut ::core::ffi::c_void,
170+
_: *mut ::core::ffi::c_void,
171+
) {
172+
let init = unsafe { &mut *(arg0 as *mut ::zephyr::thread::InitData<_ZephyrInternalArgs>) };
173+
let init = init.0.get();
174+
match unsafe { init.replace(None) } {
175+
None => {
176+
::core::panic!("Incorrect thread initialization");
177+
}
178+
Some(data) => {
179+
#thread_inner_ident(#(#inner_calling)*);
180+
}
181+
}
182+
}
183+
184+
zephyr::thread::ThreadData::acquire(
185+
&THREAD,
186+
&STACK,
187+
_ZephyrInternalArgs { #(#inner_args)* },
188+
Some(startup),
189+
0,
190+
)
191+
};
192+
193+
let thread_outer_attrs = thread_inner.attrs.clone();
194+
195+
if !errors.is_empty() {
196+
thread_outer_body = quote! {
197+
#[allow(unused_variables, unreachable_code)]
198+
let _x: ::zephyr::thread::ReadyThread = ::core::todo!();
199+
_x
200+
};
201+
}
202+
203+
// Copy the generics + where clause to avoid more spurious errors.
204+
let generics = &f.sig.generics;
205+
let where_clause = &f.sig.generics.where_clause;
206+
207+
quote! {
208+
// This is the user's thread function, renamed.
209+
#[doc(hidden)]
210+
#thread_inner
211+
212+
#(#thread_outer_attrs)*
213+
#visibility fn #thread_ident #generics (#fargs) -> ::zephyr::thread::ReadyThread #where_clause {
214+
#thread_outer_body
215+
}
216+
217+
#errors
218+
}
219+
}
220+
221+
// Taken from embassy-executor-macros.
222+
fn check_arg_ty(errors: &mut TokenStream, ty: &Type) {
223+
struct Visitor<'a> {
224+
errors: &'a mut TokenStream,
225+
}
226+
227+
impl<'a, 'ast> Visit<'ast> for Visitor<'a> {
228+
fn visit_type_reference(&mut self, i: &'ast syn::TypeReference) {
229+
// only check for elided lifetime here. If not elided, it is checked by
230+
// `visit_lifetime`.
231+
if i.lifetime.is_none() {
232+
error(
233+
self.errors,
234+
i.and_token,
235+
"Arguments for threads must live forever. Try using the `'static` lifetime.",
236+
);
237+
}
238+
visit::visit_type_reference(self, i);
239+
}
240+
241+
fn visit_lifetime(&mut self, i: &'ast syn::Lifetime) {
242+
if i.ident.to_string() != "static" {
243+
error(
244+
self.errors,
245+
i,
246+
"Arguments for threads must live forever. Try using the `'static` lifetime.",
247+
);
248+
}
249+
}
250+
251+
fn visit_type_impl_trait(&mut self, i: &'ast syn::TypeImplTrait) {
252+
error(
253+
self.errors,
254+
i,
255+
"`impl Trait` is not allowed in thread arguments. It is syntax sugar for generics, and threads cannot be generic.",
256+
);
257+
}
258+
}
259+
260+
Visit::visit_type(&mut Visitor { errors }, ty);
261+
}
262+
263+
// Utility borrowed from embassy-executor-macros.
264+
pub fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenStream {
265+
tokens.extend(error.into_compile_error());
266+
tokens
267+
}
268+
269+
pub fn error<A: ToTokens, T: Display>(s: &mut TokenStream, obj: A, msg: T) {
270+
s.extend(syn::Error::new_spanned(obj.into_token_stream(), msg).into_compile_error())
271+
}

zephyr/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Functionality for Rust-based applications that run on Zephyr.
1111

1212
[dependencies]
1313
zephyr-sys = { version = "0.1.0", path = "../zephyr-sys" }
14+
zephyr-macros = { version = "0.1.0", path = "../zephyr-macros" }
1415

1516
# Although paste is brought in, it is a compile-time macro, and is not linked into the application.
1617
paste = "1.0"

zephyr/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ pub mod object;
8888
pub mod simpletls;
8989
pub mod sync;
9090
pub mod sys;
91+
pub mod thread;
9192
pub mod time;
9293
#[cfg(CONFIG_RUST_ALLOC)]
9394
pub mod timer;
@@ -101,6 +102,9 @@ pub use logging::set_logger;
101102
/// Re-exported for local macro use.
102103
pub use paste::paste;
103104

105+
/// Re-export the proc macros.
106+
pub use zephyr_macros::thread;
107+
104108
// Bring in the generated kconfig module
105109
pub mod kconfig {
106110
//! Zephyr Kconfig values.

0 commit comments

Comments
 (0)