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.
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
}
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 bothf32::try_from_reference and i64::try_from_reference returning Some(…) for the same value, so it’s a bit of a moot point.
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!