diff --git a/Cargo.toml b/Cargo.toml index 1059706ad..f1e859ecf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "src/riot-rs-boards/nrf52840dk", "src/riot-rs-boards/nucleo-f401re", "src/riot-rs-chips", + "src/riot-rs-macros", "src/lib/*", "examples/*", ] diff --git a/examples/laze.yml b/examples/laze.yml index f1464cb70..a7a53c748 100644 --- a/examples/laze.yml +++ b/examples/laze.yml @@ -18,3 +18,4 @@ subdirs: - riot-app - rust-gcoap - riot-wrappers-mutex + - threading diff --git a/examples/threading/Cargo.toml b/examples/threading/Cargo.toml new file mode 100644 index 000000000..c04b13541 --- /dev/null +++ b/examples/threading/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "threading" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +publish = false + +[dependencies] +riot-rs = { path = "../../src/riot-rs", features = ["threading"] } +riot-rs-boards = { path = "../../src/riot-rs-boards" } diff --git a/examples/threading/README.md b/examples/threading/README.md new file mode 100644 index 000000000..a09967500 --- /dev/null +++ b/examples/threading/README.md @@ -0,0 +1,13 @@ +# threading + +## About + +This application demonstrates basic threading. + +## How to run + +In this folder, run + + laze build -b nrf52840dk run + +The application will start two threads and print a message from each thread. diff --git a/examples/threading/laze.yml b/examples/threading/laze.yml new file mode 100644 index 000000000..8621bf13e --- /dev/null +++ b/examples/threading/laze.yml @@ -0,0 +1,4 @@ +apps: + - name: threading + selects: + - ?release diff --git a/examples/threading/src/main.rs b/examples/threading/src/main.rs new file mode 100644 index 000000000..6d975dfc8 --- /dev/null +++ b/examples/threading/src/main.rs @@ -0,0 +1,24 @@ +#![no_main] +#![no_std] +#![feature(type_alias_impl_trait)] +#![feature(used_with_arg)] + +use riot_rs::rt::debug::println; + +#[riot_rs::thread] +fn thread0() { + println!("Hello from thread 0"); +} + +#[riot_rs::thread(stacksize = 4096, priority = 2)] +fn thread1() { + println!("Hello from thread 1"); +} + +#[no_mangle] +fn riot_main() { + println!( + "Hello from riot_main()! Running on a {} board.", + riot_rs::buildinfo::BOARD + ); +} diff --git a/src/riot-rs-macros/Cargo.toml b/src/riot-rs-macros/Cargo.toml new file mode 100644 index 000000000..f9f0ebcf3 --- /dev/null +++ b/src/riot-rs-macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "riot-rs-macros" +version.workspace = true +authors.workspace = true +edition.workspace = true +repository.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro-crate = "3.1.0" +quote = "1.0.35" +syn = { version = "2.0.47", features = ["full"] } + +[lib] +proc-macro = true diff --git a/src/riot-rs-macros/src/lib.rs b/src/riot-rs-macros/src/lib.rs new file mode 100644 index 000000000..9357f4e08 --- /dev/null +++ b/src/riot-rs-macros/src/lib.rs @@ -0,0 +1,3 @@ +#![deny(clippy::pedantic)] + +include!("thread.rs"); diff --git a/src/riot-rs-macros/src/thread.rs b/src/riot-rs-macros/src/thread.rs new file mode 100644 index 000000000..63fb4671f --- /dev/null +++ b/src/riot-rs-macros/src/thread.rs @@ -0,0 +1,164 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{meta::ParseNestedMeta, ItemFn, LitInt}; + +const RIOT_RS_CRATE_NAME: &str = "riot-rs"; + +// TODO: document default values (which may be platform-dependent) +// TODO: document valid values +/// Runs the function decorated with this attribute macro as a separate thread. +/// +/// # Parameters +/// +/// - `stacksize`: (*optional*) the size of the stack allocated to the thread (in bytes) +/// - `priority`: (*optional*) the thread's priority +/// +/// # Examples +/// +/// This starts a thread with default values: +/// +/// ```ignore +/// #[riot_rs::thread] +/// fn print_hello_world() { +/// println!("Hello world!"); +/// } +/// ``` +/// +/// This starts a thread with a stack size of 1024 bytes and a priority of 2: +/// +/// ```ignore +/// #[riot_rs::thread(stacksize = 1024, priority = 2)] +/// fn print_hello_world() { +/// println!("Hello world!"); +/// } +/// ``` +/// +/// # Panics +/// +/// This macro panics when the `riot-rs` crate cannot be found as a dependency of the crate where +/// this macro is used. +#[proc_macro_attribute] +pub fn thread(args: TokenStream, item: TokenStream) -> TokenStream { + let mut attrs = ThreadAttributes::default(); + let thread_parser = syn::meta::parser(|meta| attrs.parse(&meta)); + syn::parse_macro_input!(args with thread_parser); + + let thread_function = syn::parse_macro_input!(item as ItemFn); + + let no_mangle_attr = if attrs.no_mangle { + quote! {#[no_mangle]} + } else { + quote! {} + }; + + let fn_name = thread_function.sig.ident.clone(); + let slice_fn_name_ident = format_ident!("__start_thread_{fn_name}"); + let ThreadParameters { + stack_size, + priority, + } = ThreadParameters::from(attrs); + + let this_crate = proc_macro_crate::crate_name(RIOT_RS_CRATE_NAME) + .unwrap_or_else(|_| panic!("{RIOT_RS_CRATE_NAME} should be present in `Cargo.toml`")); + let this_crate = match this_crate { + proc_macro_crate::FoundCrate::Itself => { + panic!( + "{} cannot be used as a dependency of itself", + env!("CARGO_CRATE_NAME"), + ); + } + proc_macro_crate::FoundCrate::Name(this_crate) => format_ident!("{}", this_crate), + }; + + let expanded = quote! { + #no_mangle_attr + #[inline(always)] + #thread_function + + #[#this_crate::linkme::distributed_slice(#this_crate::thread::THREAD_FNS)] + #[linkme(crate = #this_crate::linkme)] + fn #slice_fn_name_ident() { + fn trampoline(_arg: ()) { + #fn_name(); + } + let stack = #this_crate::static_cell::make_static!([0u8; #stack_size as usize]); + #this_crate::thread::thread_create(trampoline, (), stack, #priority); + } + }; + + TokenStream::from(expanded) +} + +struct ThreadParameters { + stack_size: u64, + priority: u8, +} + +impl Default for ThreadParameters { + fn default() -> Self { + // TODO: proper values + Self { + stack_size: 2048, + priority: 1, + } + } +} + +impl From for ThreadParameters { + fn from(attrs: ThreadAttributes) -> Self { + let default = Self::default(); + + let stack_size = attrs.stack_size.map_or(default.stack_size, |l| { + parse_base10_or_panic(&l, "stack_size") + }); + + let priority = attrs + .priority + .map_or(default.priority, |l| parse_base10_or_panic(&l, "priority")); + + Self { + stack_size, + priority, + } + } +} + +fn parse_base10_or_panic(lit_int: &LitInt, attr: &str) -> I +where + I: core::str::FromStr, + ::Err: std::fmt::Display, +{ + if let Ok(int) = lit_int.base10_parse() { + assert!( + lit_int.suffix().is_empty(), + "`{attr}` must be a base-10 integer without a suffix", + ); + int + } else { + panic!("`{attr}` must be a base-10 integer"); + } +} + +#[derive(Default)] +struct ThreadAttributes { + stack_size: Option, + priority: Option, + no_mangle: bool, +} + +impl ThreadAttributes { + fn parse(&mut self, meta: &ParseNestedMeta) -> syn::Result<()> { + if meta.path.is_ident("stacksize") { + self.stack_size = Some(meta.value()?.parse()?); + Ok(()) + } else if meta.path.is_ident("priority") { + self.priority = Some(meta.value()?.parse()?); + Ok(()) + } else if meta.path.is_ident("no_mangle") { + self.no_mangle = true; + Ok(()) + } else { + Err(meta.error("unsupported parameter")) + } + } +} diff --git a/src/riot-rs-rt/linkme.x b/src/riot-rs-rt/linkme.x index 919979bbc..f97a8fcef 100644 --- a/src/riot-rs-rt/linkme.x +++ b/src/riot-rs-rt/linkme.x @@ -3,6 +3,7 @@ SECTIONS { linkm2_INIT_FUNCS : { *(linkm2_INIT_FUNCS) } > FLASH linkme_EMBASSY_TASKS : { *(linkme_EMBASSY_TASKS) } > FLASH linkm2_EMBASSY_TASKS : { *(linkm2_EMBASSY_TASKS) } > FLASH + linkm2_THREAD_FNS : { *(linkm2_THREAD_FNS) } > FLASH } INSERT AFTER .rodata diff --git a/src/riot-rs-rt/src/lib.rs b/src/riot-rs-rt/src/lib.rs index 3e957af69..1d7bad61f 100644 --- a/src/riot-rs-rt/src/lib.rs +++ b/src/riot-rs-rt/src/lib.rs @@ -76,8 +76,10 @@ fn startup() -> ! { #[cfg(feature = "threading")] { - // start threading - threading::init(); + // SAFETY: this function must not be called more than once + unsafe { + threading::start(); + } } #[cfg(not(feature = "threading"))] diff --git a/src/riot-rs-rt/src/threading.rs b/src/riot-rs-rt/src/threading.rs index 22eedce6d..8846099cd 100644 --- a/src/riot-rs-rt/src/threading.rs +++ b/src/riot-rs-rt/src/threading.rs @@ -1,21 +1,33 @@ -use riot_rs_threads::{start_threading, thread_create}; +use riot_rs_threads::{start_threading, thread_create, THREAD_FNS}; -static mut MAIN_STACK: [u8; 2048] = [0; 2048]; +const MAIN_STACK_SIZE: usize = 2048; extern "Rust" { fn riot_main(); } fn main_trampoline(_arg: usize) { + // SAFETY: FFI call to a Rust function unsafe { riot_main(); } } -pub(crate) fn init() -> ! { +/// # Safety +/// +/// The caller must ensure that this function is only called once. +pub unsafe fn start() -> ! { + for thread_fn in THREAD_FNS { + thread_fn(); + } + + let mut main_stack: [u8; MAIN_STACK_SIZE] = [0; MAIN_STACK_SIZE]; + thread_create(main_trampoline, 0, &mut main_stack, 0); + + // SAFETY: this function must only be called once, enforced by caller unsafe { - thread_create(main_trampoline, 0, &mut MAIN_STACK, 0); start_threading(); } + loop {} } diff --git a/src/riot-rs-threads/Cargo.toml b/src/riot-rs-threads/Cargo.toml index dcf505fc3..1661c7591 100644 --- a/src/riot-rs-threads/Cargo.toml +++ b/src/riot-rs-threads/Cargo.toml @@ -10,6 +10,7 @@ repository.workspace = true [dependencies] cfg-if.workspace = true critical-section.workspace = true +linkme = { workspace = true } riot-rs-runqueue.workspace = true [target.'cfg(context = "cortex-m")'.dependencies] diff --git a/src/riot-rs-threads/src/lib.rs b/src/riot-rs-threads/src/lib.rs index 68ead82f9..f52fb143e 100644 --- a/src/riot-rs-threads/src/lib.rs +++ b/src/riot-rs-threads/src/lib.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(test), no_std)] #![feature(inline_const)] #![feature(naked_functions)] +#![feature(used_with_arg)] use critical_section::CriticalSection; @@ -31,6 +32,11 @@ pub const THREADS_NUMOF: usize = 16; pub(crate) static THREADS: EnsureOnce = EnsureOnce::new(Threads::new()); +pub type ThreadFn = fn(); + +#[linkme::distributed_slice] +pub static THREAD_FNS: [ThreadFn] = [..]; + /// Struct holding all scheduler state pub struct Threads { /// global thread runqueue diff --git a/src/riot-rs/Cargo.toml b/src/riot-rs/Cargo.toml index 114978529..ab616db04 100644 --- a/src/riot-rs/Cargo.toml +++ b/src/riot-rs/Cargo.toml @@ -5,12 +5,15 @@ authors.workspace = true edition.workspace = true [dependencies] +linkme = { workspace = true } riot-build = { path = "../riot-build", features = [ "riot-rs-core"], optional = true } riot-rs-rt = { path = "../riot-rs-rt" } riot-rs-threads = { path = "../riot-rs-threads", optional = true } riot-rs-boards = { path = "../riot-rs-boards" } riot-rs-buildinfo = { path = "../riot-rs-buildinfo" } riot-rs-embassy = { path = "../riot-rs-embassy" } +riot-rs-macros = { path = "../riot-rs-macros" } +static_cell = { workspace = true } [features] newlib = [ "riot-build", "riot-build/newlib" ] diff --git a/src/riot-rs/src/lib.rs b/src/riot-rs/src/lib.rs index 9acac4ba2..9b893d907 100644 --- a/src/riot-rs/src/lib.rs +++ b/src/riot-rs/src/lib.rs @@ -8,8 +8,14 @@ pub use riot_rs_buildinfo as buildinfo; pub use riot_rs_embassy::{self as embassy, define_peripherals}; pub use riot_rs_rt as rt; +#[cfg(feature = "threading")] +pub use riot_rs_macros::thread; #[cfg(feature = "threading")] pub use riot_rs_threads as thread; +// These are used by proc-macros we provide +pub use linkme; +pub use static_cell; + // ensure this gets linked use riot_rs_boards as _;