Skip to content

Implement a proc-macros threads creation #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
15 changes: 3 additions & 12 deletions samples/philosophers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use alloc::boxed::Box;
use alloc::vec::Vec;
use zephyr::time::{sleep, Duration, Tick};
use zephyr::{
kobj_define, printkln,
printkln,
sync::{Arc, Mutex},
sys::uptime_get,
};
Expand Down Expand Up @@ -75,12 +75,7 @@ extern "C" fn rust_main() {
printkln!("Pre fork");

for (i, syncer) in (0..NUM_PHIL).zip(syncers.into_iter()) {
let thread = PHIL_THREADS[i]
.init_once(PHIL_STACKS[i].init_once(()).unwrap())
.unwrap();
thread.spawn(move || {
phil_thread(i, syncer, stats);
});
phil_thread(i, syncer, stats).start();
}

let delay = Duration::secs_at_least(10);
Expand Down Expand Up @@ -129,6 +124,7 @@ fn get_syncer() -> Vec<Arc<dyn ForkSync>> {
get_channel_syncer()
}

#[zephyr::thread(stack_size = PHIL_STACK_SIZE, pool_size = NUM_PHIL)]
fn phil_thread(n: usize, syncer: Arc<dyn ForkSync>, stats: &'static Mutex<Stats>) {
printkln!("Child {} started: {:?}", n, syncer);

Expand Down Expand Up @@ -219,8 +215,3 @@ impl Stats {
}

static STAT_MUTEX: Mutex<Stats> = Mutex::new(Stats::new());

kobj_define! {
static PHIL_THREADS: [StaticThread; NUM_PHIL];
static PHIL_STACKS: [ThreadStack<PHIL_STACK_SIZE>; NUM_PHIL];
}
15 changes: 15 additions & 0 deletions zephyr-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "zephyr-macros"
version = "0.1.0"
edition = "2024"
license = "MIT OR Apache-2.0"
descriptions = "Macros for managing tasks and work queues in Zephyr"

[lib]
proc-macro = true

[dependencies]
syn = { version = "2.0.85", features = ["full", "visit"] }
quote = "1.0.37"
proc-macro2 = "1.0.86"
darling = "0.20.1"
36 changes: 36 additions & 0 deletions zephyr-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Zephyr macros

use proc_macro::TokenStream;

mod task;

/// Declares a Zephyr thread (or pool of threads) that can be spawned.
///
/// There are some restrictions on this:
/// - All arguments to the function must be Send.
/// - The function must not use generics.
/// - The optional `pool_size` attribute must be 1 or greater.
/// - The `stack_size` must be specified, and will set the size of the pre-defined stack for _each_
/// task in the pool.
///
/// ## Examples
///
/// Declaring a task with a simple argument:
///
/// ```rust
/// #[zephyr::thread(stack_size = 1024)]
/// fn mytask(arg: u32) {
/// // Function body.
/// }
/// ```
///
/// The result will be a function `mytask` that takes this argument, and returns a `ReadyThread`. A
/// simple use case is to call `.start()` on this, to start the Zephyr thread.
///
/// Threads can be reused after they have exited. Calling the `mytask` function before the thread
/// has exited will result in a panic. The `RunningThread`'s `join` method can be used to wait for
/// thread termination.
#[proc_macro_attribute]
pub fn thread(args: TokenStream, item: TokenStream) -> TokenStream {
task::run(args.into(), item.into()).into()
}
274 changes: 274 additions & 0 deletions zephyr-macros/src/task.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
//! Expansion of `#[zephyr::task(...)]`.

use std::{ffi::CString, fmt::Display};

use darling::FromMeta;
use darling::export::NestedMeta;
use proc_macro2::{Literal, Span, TokenStream};
use quote::{ToTokens, format_ident, quote};
use syn::{
Expr, ExprLit, ItemFn, Lit, LitInt, ReturnType, Type,
visit::{self, Visit},
};

#[derive(Debug, FromMeta, Default)]
struct Args {
#[darling(default)]
pool_size: Option<syn::Expr>,
#[darling(default)]
stack_size: Option<syn::Expr>,
}

pub fn run(args: TokenStream, item: TokenStream) -> TokenStream {
let mut errors = TokenStream::new();

// If any of the steps for this macro fail, we still want to expand to an item that is as close
// to the expected output as possible. This helps out IDEs such that completions and other
// related features keep working.
let f: ItemFn = match syn::parse2(item.clone()) {
Ok(x) => x,
Err(e) => return token_stream_with_error(item, e),
};

let args = match NestedMeta::parse_meta_list(args) {
Ok(x) => x,
Err(e) => return token_stream_with_error(item, e),
};

let args = match Args::from_list(&args) {
Ok(x) => x,
Err(e) => {
errors.extend(e.write_errors());
Args::default()
}
};

let pool_size = args.pool_size.unwrap_or(Expr::Lit(ExprLit {
attrs: vec![],
lit: Lit::Int(LitInt::new("1", Span::call_site())),
}));

let stack_size = args.stack_size.unwrap_or(Expr::Lit(ExprLit {
attrs: vec![],
// TODO: Instead of a default, require this.
lit: Lit::Int(LitInt::new("2048", Span::call_site())),
}));

if !f.sig.asyncness.is_none() {
error(&mut errors, &f.sig, "thread function must not be async");
}

if !f.sig.generics.params.is_empty() {
error(&mut errors, &f.sig, "thread function must not be generic");
}

if !f.sig.generics.where_clause.is_none() {
error(
&mut errors,
&f.sig,
"thread function must not have `where` clauses",
);
}

if !f.sig.abi.is_none() {
error(
&mut errors,
&f.sig,
"thread function must not have an ABI qualifier",
);
}

if !f.sig.variadic.is_none() {
error(&mut errors, &f.sig, "thread function must not be variadic");
}

match &f.sig.output {
ReturnType::Default => {}
ReturnType::Type(_, ty) => match &**ty {
Type::Tuple(tuple) if tuple.elems.is_empty() => {}
Type::Never(_) => {}
_ => error(
&mut errors,
&f.sig,
"thread functions must either not return a value, return (), or return `!`",
),
},
}

let mut args = Vec::new();
let mut fargs = f.sig.inputs.clone();
let mut inner_calling = Vec::new();
let mut inner_args = Vec::new();

for arg in fargs.iter_mut() {
match arg {
syn::FnArg::Receiver(_) => {
error(
&mut errors,
arg,
"thread functions must not have `self` arguments",
);
}
syn::FnArg::Typed(t) => {
check_arg_ty(&mut errors, &t.ty);
match t.pat.as_mut() {
syn::Pat::Ident(id) => {
id.mutability = None;
args.push((id.clone(), t.attrs.clone()));
inner_calling.push(quote! {
data.#id,
});
inner_args.push(quote! {#id,});
}
_ => {
error(
&mut errors,
arg,
"pattern matching in task arguments is not yet supported",
);
}
}
}
}
}

let thread_ident = f.sig.ident.clone();
let thread_inner_ident = format_ident!("__{}_thread", thread_ident);

let mut thread_inner = f.clone();
let visibility = thread_inner.vis.clone();
thread_inner.vis = syn::Visibility::Inherited;
thread_inner.sig.ident = thread_inner_ident.clone();

// Assemble the original input arguments.
let mut full_args = Vec::new();
for (arg, cfgs) in &args {
full_args.push(quote! {
#(#cfgs)*
#arg
});
}

let thread_name = Literal::c_string(&CString::new(thread_ident.to_string()).unwrap());

let mut thread_outer_body = quote! {
const _ZEPHYR_INTERNAL_STACK_SIZE: usize = zephyr::thread::stack_len(#stack_size);
const _ZEPHYR_INTERNAL_POOL_SIZE: usize = #pool_size;
struct _ZephyrInternalArgs {
// This depends on the argument syntax being valid as a struct definition, which should
// be the case with the above constraints.
#fargs
}

static THREAD: [zephyr::thread::ThreadData<_ZephyrInternalArgs>; _ZEPHYR_INTERNAL_POOL_SIZE]
= [const { zephyr::thread::ThreadData::new() }; _ZEPHYR_INTERNAL_POOL_SIZE];
#[unsafe(link_section = ".noinit.TODO_STACK")]
static STACK: [zephyr::thread::ThreadStack<_ZEPHYR_INTERNAL_STACK_SIZE>; _ZEPHYR_INTERNAL_POOL_SIZE]
= [const { zephyr::thread::ThreadStack::new() }; _ZEPHYR_INTERNAL_POOL_SIZE];

extern "C" fn startup(
arg0: *mut ::core::ffi::c_void,
_: *mut ::core::ffi::c_void,
_: *mut ::core::ffi::c_void,
) {
let init = unsafe { &mut *(arg0 as *mut ::zephyr::thread::InitData<_ZephyrInternalArgs>) };
let init = init.0.get();
match unsafe { init.replace(None) } {
None => {
::core::panic!("Incorrect thread initialization");
}
Some(data) => {
#thread_inner_ident(#(#inner_calling)*);
}
}
}

zephyr::thread::ThreadData::acquire(
&THREAD,
&STACK,
_ZephyrInternalArgs { #(#inner_args)* },
Some(startup),
0,
#thread_name,
)
};

let thread_outer_attrs = thread_inner.attrs.clone();

if !errors.is_empty() {
thread_outer_body = quote! {
#[allow(unused_variables, unreachable_code)]
let _x: ::zephyr::thread::ReadyThread = ::core::todo!();
_x
};
}

// Copy the generics + where clause to avoid more spurious errors.
let generics = &f.sig.generics;
let where_clause = &f.sig.generics.where_clause;

quote! {
// This is the user's thread function, renamed.
#[doc(hidden)]
#thread_inner

#(#thread_outer_attrs)*
#visibility fn #thread_ident #generics (#fargs) -> ::zephyr::thread::ReadyThread #where_clause {
#thread_outer_body
}

#errors
}
}

// Taken from embassy-executor-macros.
fn check_arg_ty(errors: &mut TokenStream, ty: &Type) {
struct Visitor<'a> {
errors: &'a mut TokenStream,
}

impl<'a, 'ast> Visit<'ast> for Visitor<'a> {
fn visit_type_reference(&mut self, i: &'ast syn::TypeReference) {
// only check for elided lifetime here. If not elided, it is checked by
// `visit_lifetime`.
if i.lifetime.is_none() {
error(
self.errors,
i.and_token,
"Arguments for threads must live forever. Try using the `'static` lifetime.",
);
}
visit::visit_type_reference(self, i);
}

fn visit_lifetime(&mut self, i: &'ast syn::Lifetime) {
if i.ident.to_string() != "static" {
error(
self.errors,
i,
"Arguments for threads must live forever. Try using the `'static` lifetime.",
);
}
}

fn visit_type_impl_trait(&mut self, i: &'ast syn::TypeImplTrait) {
error(
self.errors,
i,
"`impl Trait` is not allowed in thread arguments. It is syntax sugar for generics, and threads cannot be generic.",
);
}
}

Visit::visit_type(&mut Visitor { errors }, ty);
}

// Utility borrowed from embassy-executor-macros.
pub fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenStream {
tokens.extend(error.into_compile_error());
tokens
}

pub fn error<A: ToTokens, T: Display>(s: &mut TokenStream, obj: A, msg: T) {
s.extend(syn::Error::new_spanned(obj.into_token_stream(), msg).into_compile_error())
}
1 change: 1 addition & 0 deletions zephyr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Functionality for Rust-based applications that run on Zephyr.

[dependencies]
zephyr-sys = { version = "0.1.0", path = "../zephyr-sys" }
zephyr-macros = { version = "0.1.0", path = "../zephyr-macros" }

# Although paste is brought in, it is a compile-time macro, and is not linked into the application.
paste = "1.0"
Expand Down
4 changes: 4 additions & 0 deletions zephyr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pub mod object;
pub mod simpletls;
pub mod sync;
pub mod sys;
pub mod thread;
pub mod time;
#[cfg(CONFIG_RUST_ALLOC)]
pub mod timer;
Expand All @@ -101,6 +102,9 @@ pub use logging::set_logger;
/// Re-exported for local macro use.
pub use paste::paste;

/// Re-export the proc macros.
pub use zephyr_macros::thread;

// Bring in the generated kconfig module
pub mod kconfig {
//! Zephyr Kconfig values.
Expand Down
Loading