zerocopy icon indicating copy to clipboard operation
zerocopy copied to clipboard

Implement Inline Size Assertion Annotations

Open zoo868e opened this issue 1 year ago • 5 comments
trafficstars

Implem the procedural macro inline_assert_size_eq to check the size of a type at compile time. Additionally, add the test file inline_assert_size_eq_failed.rs to ensure that the macro tirggers a compile-time error when the size is not as expected Fixes #1329

zoo868e avatar Jun 09 '24 09:06 zoo868e

Codecov Report

All modified and coverable lines are covered by tests :white_check_mark:

Project coverage is 87.71%. Comparing base (f1f0184) to head (905143c). Report is 470 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1405   +/-   ##
=======================================
  Coverage   87.71%   87.71%           
=======================================
  Files          15       15           
  Lines        5138     5138           
=======================================
  Hits         4507     4507           
  Misses        631      631           

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

:rocket: New features to boost your workflow:
  • :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

codecov-commenter avatar Jun 09 '24 12:06 codecov-commenter

Thanks for being so responsive to feedback! Just a heads up: It might be awhile before we're ready to merge this. We'll need to be absolutely certain about both the syntax and its interaction with generic types before we merge, and we are currently prioritizing the backlog of issues that are blocking the 0.8 release.

jswrenn avatar Jun 14 '24 20:06 jswrenn

Hi @jswrenn, I'm currently learning about derive macros and am trying to figure out how to make them work with generic types.

I'm struggling with how to capture and parse attributes such as offset = 0, size = 2 * core::mem::size_of::<T>(). I'm not entirely sure what to refer to these as or how to manage them, so I've been searching for some tutorials to help me out.

From the video A Practical Introduction to Derive Macros with Attributes suggests that to parse attributes, one should use the deluxe crate and create a struct that derives from deluxe::ExtractAttributes. This struct would then declare the attributes as fields. However, I'm stumped by expressions like 2 * core::mem::size_of::<T>() because I don't know their type, making it challenging to declare them in the struct.

Am I on the right track here? Could you offer any advice or guidance?

zoo868e avatar Jun 15 '24 20:06 zoo868e

I think we'd like to avoid taking additional dependencies for this. To do it with syn, you'd leverage its parse module, like so:

use syn::{
    Attribute,
    Expr,
    parse::{Parse, ParseStream},
    parse_quote,
    Token,
    punctuated::Punctuated,
};

/// Declarations the qualities of a layout.
///
/// These function both as checked assertions, and as hints to `KnownLayout`.
struct Hints {
    hints: Punctuated<Hint, Token![,]>,
}

impl Parse for Hints {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        Ok(Self {
            hints: input.parse_terminated(Hint::parse, Token![,])?,
        })
    }
}

/// A declaration about type/field layout qualities.
struct Hint {
    kind: HintKind,
    colon: Token![:],
    expr: Expr,
}

impl Parse for Hint {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        Ok(Self {
            kind: input.parse()?,
            colon: input.parse()?,
            expr: input.parse()?,
        })
    }
}

mod keywords {
    syn::custom_keyword!(align);
    syn::custom_keyword!(offset);
    syn::custom_keyword!(size);
}

/// The layout quality a hint is about.
///
/// A hint is either about alignment, offset or size.
enum HintKind {
    /// The alignment of a type or field.
    Align(keywords::align),
    /// The offset of a field.
    Offset(keywords::offset),
    /// The size of a type or field.
    Size(keywords::size),
}

impl Parse for HintKind {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let lookahead = input.lookahead1();
        if lookahead.peek(keywords::align) {
            input.parse().map(HintKind::Align)
        } else if lookahead.peek(keywords::offset) {
            input.parse().map(HintKind::Offset)
        } else if lookahead.peek(keywords::size) {
            input.parse().map(HintKind::Size)
        } else {
            Err(lookahead.error())
        }
    }
}

fn main() -> syn::Result<()> {
    let attr: Attribute = parse_quote! {
        #[hint(
            align: 4,
            offset: 8,
            size: 0,
        )]
    };
    
    if attr.path().is_ident("hint") {
        let hints: Hints = attr.parse_args()?;
        // do something with `hints`
    }

    Ok(())
}

An idea I've reflected in the above code is that I think we should call these 'hints', not assertions, because they can serve the dual roles of both being checkable at parse time, but also these can be leveraged by KnownLayout so we can derive it on more types.

Well, that's the idea at least. There are a lot of particulars to figure out here.

jswrenn avatar Jun 17 '24 18:06 jswrenn

Then, to siphon the hints out of the AST, you'll want to leverage the visit module, like so (playground):

use syn::{
    parse,
    visit::{self, Visit},
    DeriveInput, Field, Generics, Ident,
};

/// Visits the AST and collects layout hints.
#[derive(Debug, Default)]
struct HintCollector<'ast> {
    hints: Vec<TargetedHint<'ast>>,
    errors: Vec<parse::Error>,
}

/// A hint and its target (i.e., type or field).
#[derive(Debug)]
struct TargetedHint<'ast> {
    target: HintTarget<'ast>,
    hints: Hints,
}

/// The target of a hint.
#[derive(Copy, Clone, Debug)]
enum HintTarget<'ast> {
    Type(&'ast DeriveInput),
    Field(&'ast Field),
}

impl<'ast> HintCollector<'ast> {
    /// Collect the `Hints` for the given target from its given attribute list.
    fn visit_attributes(&mut self, target: HintTarget<'ast>, attrs: &'ast Vec<Attribute>) {
        let hints = attrs.iter().filter(|attr| attr.path().is_ident("hint"));
        for attr in hints {
            match attr.parse_args::<Hints>() {
                Ok(hints) => {
                    self.hints.push(TargetedHint { target, hints });
                }
                Err(err) => {
                    self.errors.push(err);
                }
            }
        }
    }
}

impl<'ast> Visit<'ast> for HintCollector<'ast> {
    fn visit_derive_input(&mut self, i: &'ast DeriveInput) {
        self.visit_attributes(HintTarget::Type(i), &i.attrs);
        visit::visit_derive_input(self, i);
    }

    fn visit_field(&mut self, i: &'ast Field) {
        self.visit_attributes(HintTarget::Field(i), &i.attrs);
        visit::visit_field(self, i);
    }
}

fn main() -> syn::Result<()> {
    use syn::DeriveInput;

    let input: DeriveInput = parse_quote! {
        struct Foo<T, const N: usize> {
            #[hint(
                align: 2,
                offset: 0,
                size: 2,
            )]
            a: u16,
            #[hint(
                align: core::mem::align_of::<T>(),
                offset: 4,
                size: core::mem::size_of::<T>() * N,
            )]
            b: [T; N]
        }
    };

    let mut collector = HintCollector::default();
    collector.visit_derive_input(&input);

    println!("{:#?}", collector);

    Ok(())
}

jswrenn avatar Jun 17 '24 19:06 jswrenn