From 582e118f00677961601ec89dba35b32a4cd9172c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Kr=C3=B6ning?= Date: Sat, 20 Apr 2024 20:07:23 +0200 Subject: [PATCH] Add `#[derive(Volatile)]` for easy, access-limited field-based access to structs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Martin Kröning --- Cargo.toml | 5 + src/lib.rs | 65 ++++++++++ volatile-macro/Cargo.toml | 18 +++ volatile-macro/src/lib.rs | 26 ++++ volatile-macro/src/volatile.rs | 215 +++++++++++++++++++++++++++++++++ 5 files changed, 329 insertions(+) create mode 100644 volatile-macro/Cargo.toml create mode 100644 volatile-macro/src/lib.rs create mode 100644 volatile-macro/src/volatile.rs diff --git a/Cargo.toml b/Cargo.toml index c9e2255..aa51dc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,10 @@ repository = "https://github.com/rust-osdev/volatile" edition = "2021" [dependencies] +volatile-macro = { version = "=0.5.2", optional = true, path = "volatile-macro" } [features] +derive = ["dep:volatile-macro"] # Enable unstable features; requires Rust nightly; might break on compiler updates unstable = [] # Enable unstable and experimental features; requires Rust nightly; might break on compiler updates @@ -28,3 +30,6 @@ pre-release-commit-message = "Release version {{version}}" [package.metadata.docs.rs] features = ["unstable"] + +[workspace] +members = ["volatile-macro"] diff --git a/src/lib.rs b/src/lib.rs index 297034b..ce55687 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,71 @@ #![doc(test(attr(allow(dead_code))))] #![doc(test(attr(allow(unused_variables))))] +/// A derive macro for method-based accesses to volatile structures. +/// +/// This macro allows you to access the fields of a volatile structure via methods that enforce access limitations. +/// It is also more easily chainable than [`map_field`]. +/// +///
+/// +/// This macro generates and implements a new `{T}VolatileFieldAccess` trait, that you have to import if used from other modules. +/// Currently, the trait is only implemented for `VolatilePtr<'_, _, ReadWrite>`. +/// +///
+/// +/// # Examples +/// +/// ``` +/// use volatile::access::ReadOnly; +/// use volatile::{VolatileFieldAccess, VolatilePtr, VolatileRef}; +/// +/// #[repr(C)] +/// #[derive(VolatileFieldAccess, Default)] +/// pub struct DeviceConfig { +/// feature_select: u32, +/// #[access(ReadOnly)] +/// feature: u32, +/// } +/// +/// let mut device_config = DeviceConfig::default(); +/// let mut volatile_ref = VolatileRef::from_mut_ref(&mut device_config); +/// let mut volatile_ptr = volatile_ref.as_mut_ptr(); +/// +/// volatile_ptr.feature_select().write(42); +/// assert_eq!(volatile_ptr.feature_select().read(), 42); +/// +/// // This does not compile, because we specified `#[access(ReadOnly)]` for this field. +/// // volatile_ptr.feature().write(42); +/// +/// // A real device might have changed the value, though. +/// assert_eq!(volatile_ptr.feature().read(), 0); +/// ``` +/// +/// # Details +/// +/// This macro generates a new trait (`{T}VolatileFieldAccess`) and implements it for `VolatilePtr<'a, T, ReadWrite>`. +/// The example above results in (roughly) the following code: +/// +/// ``` +/// pub trait DeviceConfigVolatileFieldAccess<'a> { +/// fn feature_select(self) -> VolatilePtr<'a, u32, ReadWrite>; +/// +/// fn feature(self) -> VolatilePtr<'a, u32, ReadOnly>; +/// } +/// +/// impl<'a> DeviceConfigVolatileFieldAccess<'a> for VolatilePtr<'a, DeviceConfig, ReadWrite> { +/// fn feature_select(self) -> VolatilePtr<'a, u32, ReadWrite> { +/// map_field!(self.feature_select).restrict() +/// } +/// +/// fn feature(self) -> VolatilePtr<'a, u32, ReadOnly> { +/// map_field!(self.feature).restrict() +/// } +/// } +/// ``` +#[cfg(feature = "derive")] +pub use volatile_macro::VolatileFieldAccess; + pub use volatile_ptr::VolatilePtr; pub use volatile_ref::VolatileRef; diff --git a/volatile-macro/Cargo.toml b/volatile-macro/Cargo.toml new file mode 100644 index 0000000..24c4ba2 --- /dev/null +++ b/volatile-macro/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "volatile-macro" +version = "0.5.2" +authors = ["Martin Kröning "] +edition = "2021" +description = "Procedural macros for the volatile crate." +repository = "https://github.com/rust-osdev/volatile" +license = "MIT OR Apache-2.0" +keywords = ["volatile"] +categories = ["no-std", "no-std::no-alloc"] + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full"] } diff --git a/volatile-macro/src/lib.rs b/volatile-macro/src/lib.rs new file mode 100644 index 0000000..8e2e175 --- /dev/null +++ b/volatile-macro/src/lib.rs @@ -0,0 +1,26 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::ToTokens; +use syn::parse_macro_input; + +macro_rules! bail { + ($span:expr, $($tt:tt)*) => { + return Err(syn::Error::new_spanned($span, format!($($tt)*))) + }; +} + +mod volatile; + +#[proc_macro_derive(VolatileFieldAccess, attributes(access))] +pub fn derive_volatile(item: TokenStream) -> TokenStream { + match volatile::derive_volatile(parse_macro_input!(item)) { + Ok(items) => { + let mut tokens = TokenStream2::new(); + for item in &items { + item.to_tokens(&mut tokens); + } + tokens.into() + } + Err(e) => e.to_compile_error().into(), + } +} diff --git a/volatile-macro/src/volatile.rs b/volatile-macro/src/volatile.rs new file mode 100644 index 0000000..2bcc104 --- /dev/null +++ b/volatile-macro/src/volatile.rs @@ -0,0 +1,215 @@ +use quote::format_ident; +use syn::{ + parse_quote, Attribute, Fields, Ident, Item, ItemImpl, ItemStruct, ItemTrait, Path, Result, + Signature, Visibility, +}; + +fn validate_input(input: &ItemStruct) -> Result<()> { + if !matches!(&input.fields, Fields::Named(_)) { + bail!( + &input.fields, + "#[derive(VolatileFieldAccess)] can only be used on structs with named fields" + ); + } + + if !input.generics.params.is_empty() { + bail!( + &input.generics, + "#[derive(VolatileFieldAccess)] cannot be used with generic structs" + ); + } + + let mut valid_repr = false; + for attr in &input.attrs { + if attr.path().is_ident("repr") { + let ident = attr.parse_args::()?; + if ident == "C" || ident == "transparent" { + valid_repr = true; + } + } + } + if !valid_repr { + bail!( + &input.ident, + "#[derive(VolatileFieldAccess)] structs must be `#[repr(C)]` or `#[repr(transparent)]`" + ); + } + + Ok(()) +} + +struct ParsedInput { + attrs: Vec, + vis: Visibility, + trait_ident: Ident, + struct_ident: Ident, + method_attrs: Vec>, + sigs: Vec, +} + +fn parse_input(input: &ItemStruct) -> Result { + let mut attrs = vec![]; + for attr in &input.attrs { + if attr.path().is_ident("doc") { + attrs.push(attr.clone()); + } + } + + let mut method_attrs = vec![]; + for field in &input.fields { + let mut attrs = vec![]; + for attr in &field.attrs { + if attr.path().is_ident("doc") { + attrs.push(attr.clone()); + } + } + method_attrs.push(attrs); + } + + let mut sigs = vec![]; + for field in &input.fields { + let ident = field.ident.as_ref().unwrap(); + let ty = &field.ty; + + let mut access: Path = parse_quote! { ::volatile::access::ReadWrite }; + for attr in &field.attrs { + if attr.path().is_ident("access") { + access = attr.parse_args()?; + } + } + + let sig = parse_quote! { + fn #ident(self) -> ::volatile::VolatilePtr<'a, #ty, #access> + }; + sigs.push(sig); + } + + Ok(ParsedInput { + attrs, + vis: input.vis.clone(), + trait_ident: format_ident!("{}VolatileFieldAccess", input.ident), + struct_ident: input.ident.clone(), + method_attrs, + sigs, + }) +} + +fn emit_trait( + ParsedInput { + attrs, + vis, + trait_ident, + method_attrs, + sigs, + .. + }: &ParsedInput, +) -> ItemTrait { + parse_quote! { + #(#attrs)* + #[allow(non_camel_case_types)] + #vis trait #trait_ident <'a> { + #( + #(#method_attrs)* + #sigs; + )* + } + } +} + +fn emit_impl( + ParsedInput { + trait_ident, + struct_ident, + sigs, + .. + }: &ParsedInput, +) -> ItemImpl { + let fields = sigs.iter().map(|sig| &sig.ident); + + parse_quote! { + #[automatically_derived] + impl<'a> #trait_ident<'a> for ::volatile::VolatilePtr<'a, #struct_ident, ::volatile::access::ReadWrite> { + #( + #sigs { + ::volatile::map_field!(self.#fields).restrict() + } + )* + } + } +} + +pub fn derive_volatile(input: ItemStruct) -> Result> { + validate_input(&input)?; + let parsed_input = parse_input(&input)?; + let item_trait = emit_trait(&parsed_input); + let item_impl = emit_impl(&parsed_input); + Ok(vec![Item::Trait(item_trait), Item::Impl(item_impl)]) +} + +#[cfg(test)] +mod tests { + use quote::{quote, ToTokens}; + + use super::*; + + #[test] + fn test_derive() -> Result<()> { + let input = parse_quote! { + /// Struct documentation. + /// + /// This is a wonderful struct. + #[repr(C)] + #[derive(VolatileFieldAccess, Default)] + pub struct DeviceConfig { + feature_select: u32, + + /// Feature. + /// + /// This is a good field. + #[access(ReadOnly)] + feature: u32, + } + }; + + let result = derive_volatile(input)?; + + let expected_trait = quote! { + /// Struct documentation. + /// + /// This is a wonderful struct. + #[allow(non_camel_case_types)] + pub trait DeviceConfigVolatileFieldAccess<'a> { + fn feature_select(self) -> ::volatile::VolatilePtr<'a, u32, ::volatile::access::ReadWrite>; + + /// Feature. + /// + /// This is a good field. + fn feature(self) -> ::volatile::VolatilePtr<'a, u32, ReadOnly>; + } + }; + + let expected_impl = quote! { + #[automatically_derived] + impl<'a> DeviceConfigVolatileFieldAccess<'a> for ::volatile::VolatilePtr<'a, DeviceConfig, ::volatile::access::ReadWrite> { + fn feature_select(self) -> ::volatile::VolatilePtr<'a, u32, ::volatile::access::ReadWrite> { + ::volatile::map_field!(self.feature_select).restrict() + } + + fn feature(self) -> ::volatile::VolatilePtr<'a, u32, ReadOnly> { + ::volatile::map_field!(self.feature).restrict() + } + } + }; + + assert_eq!( + expected_trait.to_string(), + result[0].to_token_stream().to_string() + ); + assert_eq!( + expected_impl.to_string(), + result[1].to_token_stream().to_string() + ); + + Ok(()) + } +}