From 36109279d9980470560d392ae2037f4f49bba989 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:33:08 +0200 Subject: [PATCH 1/5] implement #552 --- Cargo.toml | 6 +- esp-idf-macros/Cargo.toml | 17 ++++ esp-idf-macros/src/dcfg.rs | 24 +++++ esp-idf-macros/src/lib.rs | 136 +++++++++++++++++++++++++ esp-idf-macros/src/ram.rs | 197 +++++++++++++++++++++++++++++++++++++ src/lib.rs | 3 + 6 files changed, 382 insertions(+), 1 deletion(-) create mode 100644 esp-idf-macros/Cargo.toml create mode 100644 esp-idf-macros/src/dcfg.rs create mode 100644 esp-idf-macros/src/lib.rs create mode 100644 esp-idf-macros/src/ram.rs diff --git a/Cargo.toml b/Cargo.toml index 01ab174468b..2189d542872 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["esp-idf-macros"] + [package] name = "esp-idf-hal" version = "0.45.2" @@ -25,7 +28,7 @@ harness = false default = ["std", "binstart"] std = ["alloc", "esp-idf-sys/std"] alloc = [] -nightly = [] +nightly = ["esp-idf-macros/doc-cfg"] experimental = [] wake-from-isr = [] # Only enable if you plan to use the `edge-executor` crate embassy-sync = [] # For now, the dependecy on the `embassy-sync` crate is non-optional, but this might change in future @@ -66,6 +69,7 @@ enumset = { version = "1.1.4", default-features = false } log = { version = "0.4", default-features = false } atomic-waker = { version = "1.1.1", default-features = false } embassy-sync = "0.7" +esp-idf-macros = { path = "esp-idf-macros" } [build-dependencies] embuild = "0.33" diff --git a/esp-idf-macros/Cargo.toml b/esp-idf-macros/Cargo.toml new file mode 100644 index 00000000000..9428a7bb8aa --- /dev/null +++ b/esp-idf-macros/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "esp-idf-macros" +version = "0.1.0" +edition = "2021" +rust-version = "1.79" + +[lib] +proc-macro = true + +[features] +default = [] +doc-cfg = [] + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["extra-traits", "full", "fold"] } diff --git a/esp-idf-macros/src/dcfg.rs b/esp-idf-macros/src/dcfg.rs new file mode 100644 index 00000000000..abb723c186e --- /dev/null +++ b/esp-idf-macros/src/dcfg.rs @@ -0,0 +1,24 @@ +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned}; +use syn::spanned::Spanned; + +pub fn dcfg(args: TokenStream, input: TokenStream) -> syn::Result { + // This will either emit #[cfg(...)] or #[cfg(...)] #[doc(cfg(...))] depending on whether + // the "doc-cfg" feature is enabled. + + let attrs = { + #[cfg(feature = "doc-cfg")] + { + quote_spanned!(args.span() => #[cfg(#args)] #[doc(cfg(#args))]) + } + #[cfg(not(feature = "doc-cfg"))] + { + quote_spanned!(args.span() => #[cfg(#args)]) + } + }; + + Ok(quote! { + #attrs + #input + }) +} diff --git a/esp-idf-macros/src/lib.rs b/esp-idf-macros/src/lib.rs new file mode 100644 index 00000000000..575d62a7605 --- /dev/null +++ b/esp-idf-macros/src/lib.rs @@ -0,0 +1,136 @@ +use proc_macro::TokenStream; + +mod dcfg; +mod ram; + +/// This attribute places the annotated function or static variable into the internal RAM +/// of the ESP32 chip. +/// +/// # Possible issues with functions +/// +/// If a function is placed into RAM, the literals are not automatically placed into RAM as well: +/// +/// ```rust,ignore +/// #[ram] +/// fn gpio_isr_handler() -> usize { +/// let s = "I am string still stored in flash"; +/// } +/// ``` +/// +/// To store the literal in flash, one could do +/// ```rust,ignore +/// #[ram] +/// fn gpio_isr_handler() -> usize { +/// #[ram] +/// static _S: &str = "I am string still stored in flash"; +/// let s = _S; +/// } +/// ``` +/// +/// # Possible issues with statics that reference data +/// +/// If the attribute is applied to a static variable that references some data, for example +/// `#[ram] static MESSAGE: &str = "Error, invalid argument";` or `#[ram] static BUFFER: &[u8] = &[0]` +/// the referenced data might not be placed into RAM. +/// +/// For byte string and string literals, the attribute will ensure that the referenced data is placed in +/// RAM, but for arbitrary slices or references it is unable to do so. +/// +/// The first option is to declare an owned static array that is then referenced by the static variable, +/// applying the ram attribute to both: +/// +/// ```rust,ignore +/// #[ram] +/// static BUFFER_DATA: [u8; 3] = [0, 1, 2]; +/// #[ram] +/// static BUFFER: &[u8] = &BUFFER_DATA; +/// ``` +/// +/// or like this: +/// +/// ```rust,ignore +/// #[ram] +/// static BUFFER: &[u8] = { +/// #[ram] +/// static DATA: [u8; 3] = [0, 1, 2]; +/// DATA.as_slice() +/// }; +/// ``` +/// +/// If the slice value is [`Copy`], the attribute can do this automatically: +/// ```rust,ignore +/// #[ram(copy)] +/// static BUFFER: [u8; 3] = &[0, 1, 2]; +/// ``` +#[proc_macro_attribute] +pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { + ram::ram(args.into(), input.into()) + .unwrap_or_else(|err| err.to_compile_error().into()) + .into() +} + +/// This attribute forwards its arguments to `cfg`, and if the `doc-cfg` feature of this +/// crate is enabled, it will emit a `doc(cfg(...))` attribute as well. +/// +/// # Example +/// +/// For rustdoc to document that a piece of code is only available when a specific condition is met, +/// one would have to write (because it is nightly-only): +/// +/// ```rust,ignore +/// pub enum ZeroCrossMode { +// PositionZero, +// NegativeZero, +// NegativePosition, +// PositiveNegative, +// #[cfg(esp_idf_version_at_least_5_4_0)] +// #[cfg_attr(feature = "nightly", doc(cfg(esp_idf_version_at_least_5_4_0)))] +// Invalid, +// } +/// ``` +/// +/// with this attribute, one can shorten it to: +/// +/// ```rust,ignore +/// pub enum ZeroCrossMode { +// PositionZero, +// NegativeZero, +// NegativePosition, +// PositiveNegative, +// #[dcfg(esp_idf_version_at_least_5_4_0)] +// Invalid, +// } +/// ``` +/// +/// The macro will then expand to the following if the `doc-cfg` feature is enabled: +/// +/// ```rust,ignore +/// pub enum ZeroCrossMode { +/// PositionZero, +/// NegativeZero, +/// NegativePosition, +/// PositiveNegative, +/// #[cfg(esp_idf_version_at_least_5_4_0)] +/// #[doc(cfg(esp_idf_version_at_least_5_4_0))] +/// Invalid, +/// } +/// ``` +/// +/// and to the following if the `doc-cfg` feature is not enabled: +/// +/// ```rust,ignore +/// pub enum ZeroCrossMode { +/// PositionZero, +/// NegativeZero, +/// NegativePosition, +/// PositiveNegative, +/// #[cfg(esp_idf_version_at_least_5_4_0)] +/// Invalid, +/// } +/// ``` +#[proc_macro_attribute] +pub fn dcfg(args: TokenStream, input: TokenStream) -> TokenStream { + dcfg::dcfg(args.into(), input.into()) + .unwrap_or_else(|err| err.to_compile_error().into()) + .into() +} diff --git a/esp-idf-macros/src/ram.rs b/esp-idf-macros/src/ram.rs new file mode 100644 index 00000000000..00b9b61cd03 --- /dev/null +++ b/esp-idf-macros/src/ram.rs @@ -0,0 +1,197 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::spanned::Spanned; +use syn::{punctuated::Punctuated, Item, Token}; + +fn quote_link_section(name: &str, subsection: Option) -> TokenStream { + // TODO: make sure #[unsafe()] is supported on the target rustc version? + if let Some(subsection) = subsection { + quote! { + #[unsafe(link_section = ::core::concat!(#name, #subsection))] + } + } else { + quote! { + #[unsafe(link_section = #name)] + } + } +} + +/// In the following code: +/// +/// ``` +/// #[link_section = ".dram1"] +/// static VARIABLE: &str = "Hello"; +/// ``` +/// +/// only a pointer to the data + length will be stored in `.dram1`. The actual bytes +/// of the string will be stored in .flash.rodata +/// +/// To do anything useful with the string, it will still have to access the flash... +/// The same problem applies to any static slices. +/// +/// This function will rewrite the expression to explicitly define an owned buffer +/// that will be linked to .dram as well and the original expression will be a pointer +/// to that buffer. +/// +/// To build the buffer, the type stored in the buffer must be [`Copy`], if not, +/// the macro will not be able to create the buffer. +/// +/// There is no way to check whether a type is [`Copy`] in a proc-macro, which is why +/// this function has a `rewrite_any_expr` argument. If that is set to `true`, +/// the function will try to rewrite any expression, if not, it will only rewrite +/// string and byte string literals. +fn rewrite_static_expr( + syn::ItemStatic { + attrs, + vis, + ident, + ty, + mutability, + expr, + .. + }: syn::ItemStatic, + rewrite_any_expr: bool, +) -> syn::ItemStatic { + let mut expr = (*expr).clone(); + + let link_attr = quote_link_section(".dram1", Some(unique_section([&ident]))); + + if let syn::Expr::Lit(syn::ExprLit { attrs, lit }) = &expr { + let mut bytes: Option> = None; + let mut from_buffer_expr: Option = None; + + match lit { + syn::Lit::Str(lit_str) => { + bytes = Some(lit_str.value().into_bytes()); + from_buffer_expr = Some(syn::parse_quote! { + unsafe { ::core::str::from_utf8_unchecked(&_BUFFER) } + }); + } + syn::Lit::ByteStr(lit_byte_str) => { + bytes = Some(lit_byte_str.value()); + from_buffer_expr = Some(syn::parse_quote!(_BUFFER.as_slice())); + } + _ => {} + } + + if let (Some(bytes), Some(from_buffer_expr)) = (bytes, from_buffer_expr) { + let buffer_len = bytes.len(); + let buffer_expr: syn::ExprArray = syn::parse_quote!([#(#bytes),*]); + + expr = syn::parse_quote!(#(#attrs)* { + #link_attr + static _BUFFER: [u8; #buffer_len] = #buffer_expr; + + #from_buffer_expr + }); + } + } + + if rewrite_any_expr { + // Check if it is an &[T] where T = inner_type + if let syn::Type::Reference(syn::TypeReference { elem, .. }) = &*ty { + if let syn::Type::Slice(syn::TypeSlice { elem, .. }) = &**elem { + expr = syn::parse_quote_spanned!(elem.span() => { + const VALUE: #ty = #expr; + const SLICE_EXPR: &[#elem] = #expr; + + #link_attr + static _BUFFER: [#elem; SLICE_EXPR.len()] = { + let mut buf: [::core::mem::MaybeUninit<#elem>; SLICE_EXPR.len()] = + unsafe { ::core::mem::MaybeUninit::uninit().assume_init() }; + + let mut i = 0; + while i < buf.len() { + buf[i] = ::core::mem::MaybeUninit::new(SLICE_EXPR[i]); + i += 1; + } + + unsafe { ::core::mem::transmute::<_, [#elem; SLICE_EXPR.len()]>(buf) } + }; + + _BUFFER.as_slice() + }); + } + } + } + + syn::parse_quote!( + #(#attrs)* + #link_attr + #vis static #mutability #ident: #ty = #expr; + ) +} + +struct Arguments { + is_copy: bool, +} + +impl syn::parse::Parse for Arguments { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let items = Punctuated::::parse_terminated(&input).unwrap(); + if items.len() == 1 { + if let syn::Meta::Path(path) = &items[0] { + if path.is_ident("copy") { + return Ok(Self { is_copy: true }); + } + } + } else if items.is_empty() { + return Ok(Self { is_copy: false }); + } + + return Err(syn::Error::new( + input.span(), + "The only supported argument to the `#[ram]` attribute is `copy`", + )); + } +} + +pub fn ram(args: TokenStream, input: TokenStream) -> syn::Result { + let args_span = args.span(); + let attr_args = syn::parse2::(args)?; + + let input_span = input.span(); + let item = syn::parse2::(input)?; + match &item { + Item::Static(item_static) => { + Ok(rewrite_static_expr(item_static.clone(), attr_args.is_copy).into_token_stream()) + } + Item::Fn(syn::ItemFn { + sig: syn::Signature { ident, .. }, + .. + }) => { + // For now, only support empty attribute arguments + if attr_args.is_copy { + return Err(syn::Error::new( + args_span, + "Invalid argument to the `#[ram]` attribute on functions", + )); + } + + let link_attr = quote_link_section(".iram1", Some(unique_section([ident]))); + + Ok(quote! { + #link_attr + #[inline(never)] + #item + }) + } + _ => Err(syn::Error::new( + input_span, + "The `#[ram]` attribute can not be applied to this item", + )), + } +} + +fn unique_section(idents: impl IntoIterator) -> syn::Expr { + let separator = quote!("_",); + let iter = idents.into_iter(); + syn::parse_quote!(::core::concat!( + ".", + #(::core::stringify!(#iter))(#separator)*, + "_", + ::core::line!(), + "_", + ::core::column!() + )) +} diff --git a/src/lib.rs b/src/lib.rs index ca35b36e866..187a37bd085 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,9 @@ extern crate std; #[macro_use] extern crate alloc; +pub use esp_idf_macros::dcfg; +pub use esp_idf_macros::ram; + pub mod adc; pub mod can; pub mod cpu; From a7792415e29f67142b351210626caccf110b513f Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 20 Oct 2025 19:34:20 +0200 Subject: [PATCH 2/5] minor updates to code --- esp-idf-macros/src/lib.rs | 92 ++++++++++++++++++++++----------------- esp-idf-macros/src/ram.rs | 45 +++++++++---------- src/task.rs | 7 +-- 3 files changed, 80 insertions(+), 64 deletions(-) diff --git a/esp-idf-macros/src/lib.rs b/esp-idf-macros/src/lib.rs index 575d62a7605..e2215d050df 100644 --- a/esp-idf-macros/src/lib.rs +++ b/esp-idf-macros/src/lib.rs @@ -6,11 +6,12 @@ mod ram; /// This attribute places the annotated function or static variable into the internal RAM /// of the ESP32 chip. /// -/// # Possible issues with functions +/// This macro is not a magic bullet, there are several caveats that have to be considered +/// when using it. /// /// If a function is placed into RAM, the literals are not automatically placed into RAM as well: /// -/// ```rust,ignore +/// ``` /// #[ram] /// fn gpio_isr_handler() -> usize { /// let s = "I am string still stored in flash"; @@ -18,7 +19,7 @@ mod ram; /// ``` /// /// To store the literal in flash, one could do -/// ```rust,ignore +/// ``` /// #[ram] /// fn gpio_isr_handler() -> usize { /// #[ram] @@ -26,20 +27,27 @@ mod ram; /// let s = _S; /// } /// ``` -/// -/// # Possible issues with statics that reference data +/// The macro does not can not place called functions into RAM automatically. This **must** +/// be done manually by annotationing the called functions with `#[ram]` where they are declared. +/// The same applies to any functions that are transitively called. /// /// If the attribute is applied to a static variable that references some data, for example -/// `#[ram] static MESSAGE: &str = "Error, invalid argument";` or `#[ram] static BUFFER: &[u8] = &[0]` -/// the referenced data might not be placed into RAM. +/// `#[ram] static DATA: &[u8] = &OTHER_VARIABLE;` the referenced data might not be placed into RAM. /// -/// For byte string and string literals, the attribute will ensure that the referenced data is placed in -/// RAM, but for arbitrary slices or references it is unable to do so. +/// For byte string (`b"..."`) and string literals (`"..."`), the attribute will ensure that +/// the referenced data is placed in RAM, but for other expressions the caller has to ensure that. /// -/// The first option is to declare an owned static array that is then referenced by the static variable, -/// applying the ram attribute to both: +/// If the expressions is referencing a slice, and the value is [`Copy`], +/// the variable can be annotated with `#[ram(copy)]` like this: +/// ``` +/// #[ram(copy)] +/// static BUFFER: [u8; 3] = &[0, 1, 2]; +/// ``` /// -/// ```rust,ignore +/// This will instruct to generate a constant expression that will ensure that the data is placed into RAM. +/// +/// If that is not an option, one can also write: +/// ``` /// #[ram] /// static BUFFER_DATA: [u8; 3] = [0, 1, 2]; /// #[ram] @@ -48,7 +56,7 @@ mod ram; /// /// or like this: /// -/// ```rust,ignore +/// ``` /// #[ram] /// static BUFFER: &[u8] = { /// #[ram] @@ -57,15 +65,21 @@ mod ram; /// }; /// ``` /// -/// If the slice value is [`Copy`], the attribute can do this automatically: -/// ```rust,ignore -/// #[ram(copy)] -/// static BUFFER: [u8; 3] = &[0, 1, 2]; -/// ``` +/// +///
+/// +/// If the code is supposed to run while the flash is disabled, it is **strongly** recommended to +/// inspect the generated binary to ensure that the entire code and data is placed into RAM. +/// It might be necessary to implement that code in C or manually call the esp-idf functions +/// through `esp-idf-sys` to ensure that no flash functions are called. +/// +/// For more information, refer to +/// +///
#[proc_macro_attribute] pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { ram::ram(args.into(), input.into()) - .unwrap_or_else(|err| err.to_compile_error().into()) + .unwrap_or_else(|err| err.to_compile_error()) .into() } @@ -77,34 +91,34 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { /// For rustdoc to document that a piece of code is only available when a specific condition is met, /// one would have to write (because it is nightly-only): /// -/// ```rust,ignore +/// ``` /// pub enum ZeroCrossMode { -// PositionZero, -// NegativeZero, -// NegativePosition, -// PositiveNegative, -// #[cfg(esp_idf_version_at_least_5_4_0)] -// #[cfg_attr(feature = "nightly", doc(cfg(esp_idf_version_at_least_5_4_0)))] -// Invalid, -// } +/// PositionZero, +/// NegativeZero, +/// NegativePosition, +/// PositiveNegative, +/// #[cfg(esp_idf_version_at_least_5_4_0)] +/// #[cfg_attr(feature = "nightly", doc(cfg(esp_idf_version_at_least_5_4_0)))] +/// Invalid, +/// } /// ``` /// /// with this attribute, one can shorten it to: /// -/// ```rust,ignore +/// ``` /// pub enum ZeroCrossMode { -// PositionZero, -// NegativeZero, -// NegativePosition, -// PositiveNegative, -// #[dcfg(esp_idf_version_at_least_5_4_0)] -// Invalid, -// } +/// PositionZero, +/// NegativeZero, +/// NegativePosition, +/// PositiveNegative, +/// #[dcfg(esp_idf_version_at_least_5_4_0)] +/// Invalid, +/// } /// ``` /// /// The macro will then expand to the following if the `doc-cfg` feature is enabled: /// -/// ```rust,ignore +/// ``` /// pub enum ZeroCrossMode { /// PositionZero, /// NegativeZero, @@ -118,7 +132,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { /// /// and to the following if the `doc-cfg` feature is not enabled: /// -/// ```rust,ignore +/// ``` /// pub enum ZeroCrossMode { /// PositionZero, /// NegativeZero, @@ -131,6 +145,6 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn dcfg(args: TokenStream, input: TokenStream) -> TokenStream { dcfg::dcfg(args.into(), input.into()) - .unwrap_or_else(|err| err.to_compile_error().into()) + .unwrap_or_else(|err| err.to_compile_error()) .into() } diff --git a/esp-idf-macros/src/ram.rs b/esp-idf-macros/src/ram.rs index 00b9b61cd03..c32e44628fb 100644 --- a/esp-idf-macros/src/ram.rs +++ b/esp-idf-macros/src/ram.rs @@ -3,15 +3,20 @@ use quote::{quote, ToTokens}; use syn::spanned::Spanned; use syn::{punctuated::Punctuated, Item, Token}; -fn quote_link_section(name: &str, subsection: Option) -> TokenStream { - // TODO: make sure #[unsafe()] is supported on the target rustc version? +fn quote_link_section(name: &str, subsection: Option) -> TokenStream { + // NOTE: With rustc 1.82 the link_section attribute should be marked unsafe like this: + // #[unsafe(link_section = ::core::concat!(#name, #subsection))] + // With the 2024 edition this is mandatory. + // + // As of now, esp-idf-hal is targeting rustc 1.79 + // See https://doc.rust-lang.org/nightly/edition-guide/rust-2024/unsafe-attributes.html if let Some(subsection) = subsection { quote! { - #[unsafe(link_section = ::core::concat!(#name, #subsection))] + #[link_section = ::core::concat!(#name, #subsection)] } } else { quote! { - #[unsafe(link_section = #name)] + #[link_section = #name] } } } @@ -56,6 +61,7 @@ fn rewrite_static_expr( let link_attr = quote_link_section(".dram1", Some(unique_section([&ident]))); + // Check if the expression is a string or byte string literal which are always `Copy` if let syn::Expr::Lit(syn::ExprLit { attrs, lit }) = &expr { let mut bytes: Option> = None; let mut from_buffer_expr: Option = None; @@ -76,11 +82,10 @@ fn rewrite_static_expr( if let (Some(bytes), Some(from_buffer_expr)) = (bytes, from_buffer_expr) { let buffer_len = bytes.len(); - let buffer_expr: syn::ExprArray = syn::parse_quote!([#(#bytes),*]); expr = syn::parse_quote!(#(#attrs)* { #link_attr - static _BUFFER: [u8; #buffer_len] = #buffer_expr; + static _BUFFER: [u8; #buffer_len] = [#(#bytes),*]; #from_buffer_expr }); @@ -97,16 +102,15 @@ fn rewrite_static_expr( #link_attr static _BUFFER: [#elem; SLICE_EXPR.len()] = { - let mut buf: [::core::mem::MaybeUninit<#elem>; SLICE_EXPR.len()] = - unsafe { ::core::mem::MaybeUninit::uninit().assume_init() }; + let mut buf: [#elem; SLICE_EXPR.len()] = [SLICE_EXPR[0]; SLICE_EXPR.len()]; let mut i = 0; while i < buf.len() { - buf[i] = ::core::mem::MaybeUninit::new(SLICE_EXPR[i]); + buf[i] = SLICE_EXPR[i]; i += 1; } - unsafe { ::core::mem::transmute::<_, [#elem; SLICE_EXPR.len()]>(buf) } + buf }; _BUFFER.as_slice() @@ -128,21 +132,20 @@ struct Arguments { impl syn::parse::Parse for Arguments { fn parse(input: syn::parse::ParseStream) -> syn::Result { - let items = Punctuated::::parse_terminated(&input).unwrap(); + let items = Punctuated::::parse_terminated(input)?; + if items.is_empty() { + return Ok(Self { is_copy: false }); + } + if items.len() == 1 { if let syn::Meta::Path(path) = &items[0] { if path.is_ident("copy") { return Ok(Self { is_copy: true }); } } - } else if items.is_empty() { - return Ok(Self { is_copy: false }); } - return Err(syn::Error::new( - input.span(), - "The only supported argument to the `#[ram]` attribute is `copy`", - )); + Err(syn::Error::new(input.span(), "Unknown argument")) } } @@ -162,10 +165,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> syn::Result { }) => { // For now, only support empty attribute arguments if attr_args.is_copy { - return Err(syn::Error::new( - args_span, - "Invalid argument to the `#[ram]` attribute on functions", - )); + return Err(syn::Error::new(args_span, "Unknown argument")); } let link_attr = quote_link_section(".iram1", Some(unique_section([ident]))); @@ -178,11 +178,12 @@ pub fn ram(args: TokenStream, input: TokenStream) -> syn::Result { } _ => Err(syn::Error::new( input_span, - "The `#[ram]` attribute can not be applied to this item", + "The attribute can not be applied to this item", )), } } +/// This function generates a likely unique subsection name based on the provided identifiers. fn unique_section(idents: impl IntoIterator) -> syn::Expr { let separator = quote!("_",); let iter = idents.into_iter(); diff --git a/src/task.rs b/src/task.rs index fcf9be55f58..1befeb15840 100644 --- a/src/task.rs +++ b/src/task.rs @@ -955,6 +955,7 @@ pub mod queue { use esp_idf_sys::{EspError, TickType_t, ESP_FAIL}; + use crate::ram; use crate::sys; /// Thin wrapper on top of the FreeRTOS queue. @@ -1026,7 +1027,7 @@ pub mod queue { /// it will return true if a higher priority task was awoken. /// In non-ISR contexts, the function will always return `false`. /// In this case the interrupt should call [`crate::task::do_yield`]. - #[inline] + #[ram] pub fn send_back(&self, item: T, timeout: TickType_t) -> Result { self.send_generic(item, timeout, 0) } @@ -1051,7 +1052,7 @@ pub mod queue { /// it will return true if a higher priority task was awoken. /// In non-ISR contexts, the function will always return `false`. /// In this case the interrupt should call [`crate::task::do_yield`]. - #[inline] + #[ram] pub fn send_front(&self, item: T, timeout: TickType_t) -> Result { self.send_generic(item, timeout, 1) } @@ -1075,7 +1076,7 @@ pub mod queue { /// it will return true if a higher priority task was awoken. /// In non-ISR contexts, the function will always return `false`. /// In this case the interrupt should call [`crate::task::do_yield`]. - #[inline] + #[ram] fn send_generic( &self, item: T, From e2d9a59d5ca29ec50f16560571902b3132fed2ed Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 20 Oct 2025 19:36:08 +0200 Subject: [PATCH 3/5] revert accidentally committed code --- src/task.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/task.rs b/src/task.rs index 1befeb15840..fcf9be55f58 100644 --- a/src/task.rs +++ b/src/task.rs @@ -955,7 +955,6 @@ pub mod queue { use esp_idf_sys::{EspError, TickType_t, ESP_FAIL}; - use crate::ram; use crate::sys; /// Thin wrapper on top of the FreeRTOS queue. @@ -1027,7 +1026,7 @@ pub mod queue { /// it will return true if a higher priority task was awoken. /// In non-ISR contexts, the function will always return `false`. /// In this case the interrupt should call [`crate::task::do_yield`]. - #[ram] + #[inline] pub fn send_back(&self, item: T, timeout: TickType_t) -> Result { self.send_generic(item, timeout, 0) } @@ -1052,7 +1051,7 @@ pub mod queue { /// it will return true if a higher priority task was awoken. /// In non-ISR contexts, the function will always return `false`. /// In this case the interrupt should call [`crate::task::do_yield`]. - #[ram] + #[inline] pub fn send_front(&self, item: T, timeout: TickType_t) -> Result { self.send_generic(item, timeout, 1) } @@ -1076,7 +1075,7 @@ pub mod queue { /// it will return true if a higher priority task was awoken. /// In non-ISR contexts, the function will always return `false`. /// In this case the interrupt should call [`crate::task::do_yield`]. - #[ram] + #[inline] fn send_generic( &self, item: T, From 950541c326dd75b78a4ab460f3c0dc8b985de25a Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 20 Oct 2025 19:40:49 +0200 Subject: [PATCH 4/5] replace all `#[link_section = "..."]` with `#[ram]` macro --- src/cpu.rs | 5 +++-- src/interrupt.rs | 26 ++++++++++---------------- src/task.rs | 13 +++++-------- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index 8fb6fdf2a68..279dd80ffd2 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -5,6 +5,8 @@ use esp_idf_sys::*; use enumset::EnumSetType; +use crate::ram; + /// Returns the number of cores supported by the esp32* chip pub const CORES: u32 = SOC_CPU_CORES_NUM; @@ -46,8 +48,7 @@ impl From for Core { /// On dual-core systems like esp32 and esp32s3 this function returns: /// 0 - when the active core is the PRO CPU /// 1 - when the active core is the APP CPU -#[inline(always)] -#[link_section = ".iram1.cpu_core"] +#[ram] pub fn core() -> Core { #[cfg(any(esp32c3, esp32s2, esp32c2, esp32h2, esp32c5, esp32c6))] let core = 0; diff --git a/src/interrupt.rs b/src/interrupt.rs index efdaba3d87f..c2ad22765ec 100644 --- a/src/interrupt.rs +++ b/src/interrupt.rs @@ -2,6 +2,8 @@ use enumset::{EnumSet, EnumSetType}; use esp_idf_sys::*; +use crate::ram; + /// For backwards compatibility pub type IntrFlags = InterruptType; @@ -83,8 +85,7 @@ impl From for u32 { pub(crate) static CS: IsrCriticalSection = IsrCriticalSection::new(); /// Returns true if the currently active core is executing an ISR request -#[inline(always)] -#[link_section = ".iram1.interrupt_active"] +#[ram] pub fn active() -> bool { unsafe { xPortInIsrContext() != 0 } } @@ -116,8 +117,7 @@ unsafe fn do_yield_signal(arg: *mut ()) { static mut ISR_YIELDER: Option<(unsafe fn(*mut ()), *mut ())> = None; #[allow(clippy::type_complexity)] -#[inline(always)] -#[link_section = ".iram1.interrupt_get_isr_yielder"] +#[ram] pub(crate) unsafe fn get_isr_yielder() -> Option<(unsafe fn(*mut ()), *mut ())> { if active() { free(|| { @@ -144,8 +144,7 @@ pub(crate) unsafe fn get_isr_yielder() -> Option<(unsafe fn(*mut ()), *mut ())> /// ISR handler so as to reastore the yield function which was valid before the /// ISR handler was invoked. #[allow(clippy::type_complexity)] -#[inline(always)] -#[link_section = ".iram1.interrupt_set_isr_yielder"] +#[ram] pub unsafe fn set_isr_yielder( yielder: Option<(unsafe fn(*mut ()), *mut ())>, ) -> Option<(unsafe fn(*mut ()), *mut ())> { @@ -170,8 +169,7 @@ pub struct IsrCriticalSection(core::cell::UnsafeCell); pub struct IsrCriticalSection(core::marker::PhantomData<*const ()>); #[cfg(not(any(esp32, esp32s2, esp32s3, esp32p4)))] -#[inline(always)] -#[link_section = ".iram1.interrupt_enter"] +#[ram] fn enter(_cs: &IsrCriticalSection) { unsafe { vPortEnterCritical(); @@ -179,8 +177,7 @@ fn enter(_cs: &IsrCriticalSection) { } #[cfg(any(esp32, esp32s2, esp32s3, esp32p4))] -#[inline(always)] -#[link_section = ".iram1.interrupt_enter"] +#[ram] fn enter(cs: &IsrCriticalSection) { unsafe { xPortEnterCriticalTimeout(cs.0.get(), portMUX_NO_TIMEOUT); @@ -188,8 +185,7 @@ fn enter(cs: &IsrCriticalSection) { } #[cfg(not(any(esp32, esp32s2, esp32s3, esp32p4)))] -#[inline(always)] -#[link_section = ".iram1.interrupt_exit"] +#[ram] fn exit(_cs: &IsrCriticalSection) { unsafe { vPortExitCritical(); @@ -197,8 +193,7 @@ fn exit(_cs: &IsrCriticalSection) { } #[cfg(any(esp32, esp32s2, esp32s3, esp32p4))] -#[inline(always)] -#[link_section = ".iram1.interrupt_exit"] +#[ram] fn exit(cs: &IsrCriticalSection) { unsafe { vPortExitCritical(cs.0.get()); @@ -274,8 +269,7 @@ impl Drop for IsrCriticalSectionGuard<'_> { } /// Executes closure f in an interrupt-free context -#[inline(always)] -#[link_section = ".iram1.interrupt_free"] +#[ram] pub fn free(f: impl FnOnce() -> R) -> R { let _guard = CS.enter(); diff --git a/src/task.rs b/src/task.rs index fcf9be55f58..decd0395282 100644 --- a/src/task.rs +++ b/src/task.rs @@ -13,6 +13,7 @@ use esp_idf_sys::*; use crate::cpu::Core; use crate::interrupt; +use crate::ram; #[cfg(not(any( esp_idf_version_major = "4", @@ -83,8 +84,7 @@ pub unsafe fn destroy(task: TaskHandle_t) { vTaskDelete(task) } -#[inline(always)] -#[link_section = ".iram1.interrupt_task_do_yield"] +#[ram] pub fn do_yield() { if interrupt::active() { unsafe { @@ -114,8 +114,7 @@ pub fn do_yield() { } } -#[inline(always)] -#[link_section = ".iram1.interrupt_task_current"] +#[ram] pub fn current() -> Option { if interrupt::active() { None @@ -463,8 +462,7 @@ pub struct CriticalSection(Cell>>, AtomicBool); // Not available in the esp-idf-sys bindings const QUEUE_TYPE_RECURSIVE_MUTEX: u8 = 4; -#[inline(always)] -#[link_section = ".iram1.cs_enter"] +#[ram] fn enter(cs: &CriticalSection) { if !cs.1.load(Ordering::SeqCst) { interrupt::free(|| { @@ -484,8 +482,7 @@ fn enter(cs: &CriticalSection) { } } -#[inline(always)] -#[link_section = ".iram1.cs_exit"] +#[ram] fn exit(cs: &CriticalSection) { if !cs.1.load(Ordering::SeqCst) { panic!("Called exit() without matching enter()"); From b5cffa94978d1452ff8270920f3b434559f55c26 Mon Sep 17 00:00:00 2001 From: Luro02 <24826124+Luro02@users.noreply.github.com> Date: Mon, 20 Oct 2025 19:48:27 +0200 Subject: [PATCH 5/5] refactor code to support future addition of other macros with different sections --- esp-idf-macros/src/lib.rs | 2 +- esp-idf-macros/src/ram.rs | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/esp-idf-macros/src/lib.rs b/esp-idf-macros/src/lib.rs index e2215d050df..75ec7572f87 100644 --- a/esp-idf-macros/src/lib.rs +++ b/esp-idf-macros/src/lib.rs @@ -78,7 +78,7 @@ mod ram; /// #[proc_macro_attribute] pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream { - ram::ram(args.into(), input.into()) + ram::link_to_section(".iram1", ".dram1", args.into(), input.into()) .unwrap_or_else(|err| err.to_compile_error()) .into() } diff --git a/esp-idf-macros/src/ram.rs b/esp-idf-macros/src/ram.rs index c32e44628fb..9d9bbae8b37 100644 --- a/esp-idf-macros/src/ram.rs +++ b/esp-idf-macros/src/ram.rs @@ -46,6 +46,7 @@ fn quote_link_section(name: &str, subsection: Option) -> TokenStream /// the function will try to rewrite any expression, if not, it will only rewrite /// string and byte string literals. fn rewrite_static_expr( + section: &str, syn::ItemStatic { attrs, vis, @@ -59,7 +60,7 @@ fn rewrite_static_expr( ) -> syn::ItemStatic { let mut expr = (*expr).clone(); - let link_attr = quote_link_section(".dram1", Some(unique_section([&ident]))); + let link_attr = quote_link_section(section, Some(unique_section([&ident]))); // Check if the expression is a string or byte string literal which are always `Copy` if let syn::Expr::Lit(syn::ExprLit { attrs, lit }) = &expr { @@ -149,7 +150,12 @@ impl syn::parse::Parse for Arguments { } } -pub fn ram(args: TokenStream, input: TokenStream) -> syn::Result { +pub fn link_to_section( + function_section: &str, + data_section: &str, + args: TokenStream, + input: TokenStream, +) -> syn::Result { let args_span = args.span(); let attr_args = syn::parse2::(args)?; @@ -157,7 +163,10 @@ pub fn ram(args: TokenStream, input: TokenStream) -> syn::Result { let item = syn::parse2::(input)?; match &item { Item::Static(item_static) => { - Ok(rewrite_static_expr(item_static.clone(), attr_args.is_copy).into_token_stream()) + Ok( + rewrite_static_expr(data_section, item_static.clone(), attr_args.is_copy) + .into_token_stream(), + ) } Item::Fn(syn::ItemFn { sig: syn::Signature { ident, .. }, @@ -168,7 +177,7 @@ pub fn ram(args: TokenStream, input: TokenStream) -> syn::Result { return Err(syn::Error::new(args_span, "Unknown argument")); } - let link_attr = quote_link_section(".iram1", Some(unique_section([ident]))); + let link_attr = quote_link_section(function_section, Some(unique_section([ident]))); Ok(quote! { #link_attr