(un)safely casting a pointer to the same type

Hi all,

I'm using an external crate to query values from an external service. This service responds with Values that may be one of several types, such as

enum Value {
    String(String),
    Int(i64),
    Float(f32)
}

It is possible to downcast from a Value to an Option of a concrete type. Unfortunately, the external service sometimes casts floats to integers if it sees that that's possible without loosing too much precision (a questionable design choice, but one that I cannot change).

In my code, I abstract over the concrete type in a generic method that further processes these values. However, inside that generic function, I want to do the following:

  • If I'm looking for a f32 and get None from the service, query again for an i64
  • If I get an i64, cast it and return Some.
  • If it's neither an f32 nor an i64, return None.

As i understand it, what i actually need is specialization, which rust does not yet support. However, I managed to get something to work using just a little tiiiiny bit of unsafe. It seems to do the correct thing, but I'm now living in fear it will one day burn down my house:

    let mut new_val : Option<T> = T::try_from_reference(val);
 
    if type_equals::<T, f32>() && new_val.is_none() {
        // try to get i64 and try to cast it to f32
        let new_val_from_int = i64::try_from_reference(val).map(|i| i as f32);
        // if we got something back, convert our f32 to a T
        if let Some(new_val_from_int_ref) = new_val_from_int.as_ref() {
            let new_val_from_int_ref: &T =
                // the unsafe in question
                unsafe { &*(new_val_from_int_ref as *const f32 as *const T) };
            new_val = Some(new_val_from_int_ref.clone());
        }
    }

Complete minimal runnable example with a mocked version of the external service:

Is this sound? Can I safely cast *const f32 to *const T after I checked that T and f32 have the same TypeId? Does new_val_from_int live long enough for the .clone() to be valid? Did I forget something?

No comment on the soundness of the conversion, but instead of using type ids inside of generic code, you could have a specific function handling the case of "get a number", and either have one for int and one for float, or have it return an IntOrFloat enum that then itself supports being converted to int or float as needed.

2 Likes

Nothing jumps out at me immediately as obviously unsound, and your code passes Miri

I agree that avoiding the unsafe entirely would probably be the simplest fix though

2 Likes

You can let Any do the heavy unsafe lifting for you:

fn get_with_cast<T : Clone + TryFromReference<Value> + 'static>(val : &Value) -> Option<T> {
    let mut new_val : Option<T> = T::try_from_reference(val);
 
    if let Some(&mut ref mut opt_f32 @ None) = (&mut new_val as &mut dyn Any).downcast_mut::<Option<f32>>() {
        // try to get i64 and try to cast it to f32
        if let Some(new_val_from_int) = i64::try_from_reference(val) {
            *opt_f32 = Some(new_val_from_int as f32);
        }
    }

    new_val
}
3 Likes

Wow, thank you, that's some impressive use of pattern matching operators there! I'll use this one, as it allows me to keep the API.

What does the @ None inside the first pattern match do? The code still seems to do the same thing when I remove it.

The base pattern that’s actually being matched here is Some(&mut None), and the ref mut opt_f32 @ adds a binding to the part that matched None. So, the difference between the two versions is in whether or not i64::try_from_reference gets an opportunity to overwrite a successful initial conversion of an f32.

In practice, it sounds like you’ll never get both f32::try_from_reference and i64::try_from_reference returning Some(…) for the same value, so it’s a bit of a moot point.

1 Like

Aaahh, it's actually pattern-matching against the None, that makes sense! While I can never get both an f32 and an i64, my actual use case requires querying these values from an external service, so I can save one service call by leaving the @ None for the case where the value is already an f32. Thanks again!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.