Inferring type of field

I am writing a macro that can be used like offset_of!, passing a struct name and a field. The macro expands to a function call, which could be something like this:

pub const fn size_of_field_<T>(_p: *const T) -> usize
{
    std::mem::size_of::<T>()
}

macro_rules! size_of_field {
     ($typ:ty, $field:tt) => {
         {
             let container__ = ::std::mem::MaybeUninit::<$typ>::uninit();
             size_of_field_(unsafe {
                 ::std::ptr::addr_of!((*container__.as_ptr()).$field)
             })
         }
     }
}

pub struct Foo(u16);
static TEST: usize = size_of_field!(Foo, 0);

#[test]
fn test()
{
    assert_eq!(TEST, 2);
}

The only difference with the real code is that I am returning <T as SomeTrait>::SOMETHING instead of sizeof.

Anyhow the above test code works, including passing Miri, but it requires a relatively new version of Rust; ideally I would support Debian bookworm's distro rustc (that's 1.63.0 or at least 1.70.0). Other possibilities that I considered are NonNull::dangling() or even std::mem::align_of::<$typ>() as *const $typ (yuck); all of these are fine for Miri but do not work on older versions of Rust.

I wonder if I am just complicating things and there's an easy idiom that I missed and that would make it easier to infer the field type (here u16) and retrieve the associated const on the type.

1 Like
use core::marker::PhantomData;
pub const fn phantom_<T>(_: &T) -> PhantomData<T> {
    PhantomData
}
pub const fn size_of_field_<T>(_: PhantomData<T>) -> usize {
    core::mem::size_of::<T>()
}

macro_rules! size_of_field {
    ($typ:ty, $field:tt) => {{
        let __size = $crate::size_of_field_;
        #[allow(unreachable_code)]
        loop {
            break;
            let __value: $typ;
            __size($crate::phantom_(&__value.$field));
        }
        __size(::core::marker::PhantomData)
    }};
}
8 Likes

I wonder how type inference feels about being manipulated like this.

6 Likes

Wow I didn't even know you could use let like this. I will wrap the magic into a call_with_field_type macro.

Yeah, unreachable code gets to skip borrow checking and variable initialization checks, but does play a role in type inference.

While in a general sense, inference changes aren't considered to be major breaking, I don't think this one could easily go away. Removing the ability of unreachable code to pass borrow check by dint of being unreachable would be a major breaking change, and the function Item has to monomorphize the same way.

It could theoretically happen if the function item became higher ranked over T, so that the function item no longer has to monomorphize the same way at the two call sites. But that would also be a breaking change for turbofishable functions (i.e. not APIT), as far as I can imagine.

Further reading:

https://rust-lang.github.io/impl-trait-initiative/RFCs/named-function-types.html#late-bound-argument-position-impl-trait-and-turbofish

(Future alternative: TAIT or return type notation can probably be used to create some form of typeof.)

1 Like

Ah, one can easily do without assigning the function item…

…I just did that anyway in an attempt to do a call_with_field_type-style macro, anyway:

Rust Playground

In the context of my first version of the code, the change would just look like this

macro_rules! size_of_field {
    ($typ:ty, $field:tt) => {{
        $crate::size_of_field_(
            #[allow(unreachable_code)]
            loop {
                break ::core::marker::PhantomData;
                let __value: $typ;
                break $crate::phantom_(&__value.$field);
            }
        )
    }};
}
4 Likes

That would still be an edition break though?

Also, are you aware of any crates that do a similar trick? I can imagine other people that did serialization/deserialization of binary formats needing something like that to access type properties.

Personally I think it won't happen for non-APIT (i.e. turbofishable) type parameters, basically.