Could this transmute cause undefined behavior?

use core::mem::transmute;

struct Foo<Data, DataRef> {
    data: Data,
    call: Box<dyn Fn(DataRef)>
}

impl<T> Foo<T, &T> {
    fn call(&self) {
        // Could this transmute cause undefined behavior?
        (self.call)(unsafe { transmute(&self.data) });

        // How can I make this call compile without using unsafe?
        // (self.call)(&self.data);
    }
}

fn main() {
    Foo {
        data: 42,
        call: Box::new(|n| println!("{n}")),
    }.call()
}

(Playground)

Output:

42

If you know that DataRef = &Data always then change call to:

call: Box<dyn for<'a> Fn(&'a Data)>

And get rid of DataRef.

3 Likes

And if I want to keep DataRef?

Well, as it is right now, there's nothing linking DataRef and Data. And in fact, the only things which it'd be safe to transmute a &'a Data to are &'a Data or &'b Data where 'a: 'b or usize if Data: Sized.

The issue also stems from the fact that you need call's Fn type to take any reference, hence why I explicitly stated the for<'a> HRTB in my previous post, however DataRef is a specific &'a T for some specific 'a, which isn't necessarily related to the lifetime for &self (which would be the liftetime associated with &self.data).

You might be better off doing what I said in the previous post and reworking your api elsewhere, or instead provide some method for users to provide a function which converts &'a Data to DataRef somehow.


Overall, transmute is almost certainly not the correct way to go about this since you're playing with lifetimes and all safe casts should be doable via implicit casts (for lifetime shrinking) or as casts.

5 Likes

This easily cause UB. Rust Playground.

6 Likes

I would say that instead of using transmute you need to explicitly write all lifetimes and try to convince borrow checker you are right. If you fail there probably is problem like one pointed out by @zirconium-n. Example of all explicit lifetimes:

struct Foo<Data, DataRef> {
    data: Data,
    call: Box<dyn Fn(DataRef)>
}

impl<'storage, 'data, T> Foo<T, &'data T>
    where Self: 'storage
{
    fn call<'rf>(&'rf self)
        where 'rf: 'data, 'data: 'rf, 'rf: 'storage, 'storage: 'rf
    {
        (self.call)(&self.data);
    }
}

fn main() {
    let foo = Foo {
        data: 42,
        call: Box::new(|n| println!("{n}")),
    };
    foo.call()
}

. I tried to use this approach and the best I got is foo not living long enough error (with above code) or some other errors. Wondering why though, I thought that with all lifetimes equal I would either get some other error or actually succeed.

1 Like

You're running into a variation of this.

I don't think there's a way around it without higher ranked types or similar.

3 Likes

I'll leave this here:

1 Like

The safe version of your code needs to define the function as Box<dyn Fn(&Data)>. If you need the data to be long-lived, you must use data: &Data in the struct. Otherwise you will have self-referential structs and explosion of lifetimes that will never make sense. Two parameters on the trait don't really make sense, since &T is the only thing you can make.

Transmuting between arbitrary user-supplied types will be invalid and can actually cause vulnerabilities and crashes.

If you need hold data by reference, and hold on to it in multiple places, the correct type for it is Arc<T>. data: Arc<T> and Fn(Arc<T>) can be both satisfied, and they need zero lifetime annotations.

Rust's references are not for general-purpose storing of data "by reference". They're a very specific mechanism for scope-limited short-term loans, and specifically not for keeping the reference around for longer.