Skip to content

Commit

Permalink
Merge pull request #49 from mkroening/derive
Browse files Browse the repository at this point in the history
Add `#[derive(Volatile)]` for easy, access-limited field-based access to structs
  • Loading branch information
phil-opp committed Apr 21, 2024
2 parents 73e154b + 582e118 commit d0d2215
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 0 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,3 +30,6 @@ pre-release-commit-message = "Release version {{version}}"

[package.metadata.docs.rs]
features = ["unstable"]

[workspace]
members = ["volatile-macro"]
65 changes: 65 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
///
/// <div class="warning">
///
/// 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>`.
///
/// </div>
///
/// # 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;

Expand Down
18 changes: 18 additions & 0 deletions volatile-macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "volatile-macro"
version = "0.5.2"
authors = ["Martin Kröning <[email protected]>"]
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"] }
26 changes: 26 additions & 0 deletions volatile-macro/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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(),
}
}
215 changes: 215 additions & 0 deletions volatile-macro/src/volatile.rs
Original file line number Diff line number Diff line change
@@ -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::<Ident>()?;
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<Attribute>,
vis: Visibility,
trait_ident: Ident,
struct_ident: Ident,
method_attrs: Vec<Vec<Attribute>>,
sigs: Vec<Signature>,
}

fn parse_input(input: &ItemStruct) -> Result<ParsedInput> {
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<Vec<Item>> {
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(())
}
}

0 comments on commit d0d2215

Please sign in to comment.