UB or not UB? MaybeUninit in an unusual setting

I'm writing a procedural macro that initializes fields of some struct based on the list of key, values. Something like this:

#[derive(FromKeyValue)]
struct SomeStruct {
  field1: Field,
  field2: Vec<Field>,
}

fn main() {
  let some = SomeStruct::from_key_values(..).
}

The issue here is that my procedural macro needs to distinguish Vec from everything else (the rules are that for Vec repeating keys are appended, for non-Vec, it's an error). Ideally, I will be able to implement some trait FromKeyValue for Vec<Field> and for Field. However, for various reasons, it's not that easy. For example, my Value type in key value list is not the same as Field, but rather Value: Into<Field>, which makes type challenges way too complicated.

I would like to have a trait "specialization" based on the target field type: if it's Vec, go Vec path, otherwise go plain path. However, to use @dtolnay's trick, I need to dispatch on the target type. But I don't have an instance of it yet, because I'm about to construct it!

If Field is Default, this becomes trivial: just make a Default::default() instance, dispatch on it, then update it with the proper value.

However, in my case, it is not Default (not always, anyways). I came up with the following trick to trick compiler into inferring the type without actually initializing the value: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=056dfb560cc93c65f6fd9b89b42694f4

And it works! :dancer:t2:

However, my concern here, is it UB or not? The code inside the if false { .. } block would be an UB, if it ever runs (since it will dereference pointer that was not initialized). However, that blocks never runs, so it should be okay?

P.S. I think, this question was asked after the discussion I had with other folks, and yes, typeof would have helped here (I would still had to create some "fake" instance for "specialization" to work, but it would have been something like MaybeUninit<typeof(struct.field)>::uninit().as_ptr()).

P.P.S. This better be not a UB... :sweat_smile: I already converted our procedural macro and I was able to remove all the type sniffing based on the syntax (checking that fields are marked by certain attributes, checking literal type names, etc). This autoref specialization is simply genius. It fits macros very nicely since those "tags" are completely different types, and whatever you do on them has to only match syntactically (not on the type system level) -- reminds me of SFINAE in C++.

3 Likes

Your code is not UB, but it would be better if it were implemented safely. Here is one way:

#[inline(always)]
fn phantomret<U: ?Sized, T: ?Sized>(_: fn(&U) -> &T) -> PhantomData<T> {
    PhantomData
}
let field1_sniffer = phantomret(|unknown: &Unknown| &unknown.field1);
let field2_sniffer = phantomret(|unknown: &Unknown| &unknown.field2);

with appropriate impls on PhantomData instead of *const T:

impl<T> Generic for &PhantomData<T> {}
impl<T: SomeTrait> Specific for PhantomData<T> {}

Also you should be able to skip the tag business and dispatch directly on the PhantomData this way.

// library (hidden)
trait Generic: Sized {
    fn dispatch(self, value: &str) -> String {
        eprintln!("generic dispatch");
        value.into()
    }
}
impl<T> Generic for &PhantomData<T> {}

// generated
let field1 = phantomret(|unknown: &Unknown| &unknown.field1)
    .dispatch("hello");
5 Likes

Oh, thank you so much!

I like tags since they separate "detection" part from "acting" part (the latter in our case is tricky due to lots of type stuff and also "ad-hoc" interfaces where macro expects structs with certain fields, but doesn't really know the struct type itself other than these fields have certain relations to other types...).

We also need ownership of value, so, I think, we have to use tags.

1 Like