Comparing Addresses Between Fat and Thin Pointers

I'm trying to write a function to determine whether a Box<T> and *const U point to the same thing, but in a context where T & U: ?Sized (psuedo-code). All I care about is the pointed-to-addresses being equal or not.

Reading online it seems like using std::ptr::eq is a no go, because for fat pointers it checks both the data pointer and vtable pointer for equality, and vtable addresses aren't stable (see https://github.com/rust-lang/rust/issues/69757).

But, all that matters is whether the data addresses match, I don't care about the vtable or the type of the pointer. So I was wondering if anyone could help clear up 2 things for me : v)

  1. Is it always safe to transmute a fat pointer to a (usize, usize), and will they always be in the order of (data, vtable) when dealing with DSTs? (and outside of const contexts).

  2. Is there a better way to perform such a check without transmuting? All of this sounds pretty reasonable in my head but so have lots of other bad ideas! Somewhere along this rabbit hole someone used something special like *const () to thin-ify the pointer and discard it's typing or something, but I can't seem to find where I saw that, and trying to search "const ()" is... difficult...

Anyways! Thanks for any help, and feel free to lambaste any of the insanities my question is predicated on!!

Just do a cast pointer as *const () (or pointer.cast::<()>() should do the same) for both pointers, and compare the resulting pointers. It doesn't need to be a *const () specifically, casting e g. as *const u8 works just as well. Really, for comparing the addresses the type does not matter very much.

(Using transmute the way you described would likely result in undefined behavior, or at least in relying on unstable layout of types, so compilation could become UB or could at least break with future compiler versions.)

4 Likes

The layout of fat pointers is unspecified, so relying upon the representation of DST pointers results in undefined behavior. as mentioned previously, casting to *const () is fine.

If you really want to go the "screw sane approaches I want to rely upon unstable compiler/standard library implementation details" route, trait object pointers are currently stored as this:

#[repr(C)]
struct TraitObject{
    ptr: *const (),
    vtable: &'static VTable,
}

Of course, that's subject to change at any time.

1 Like

It doesn't need to be a *const () specifically

Well now I just feel dumb :slight_smile:
I thought there was some special syntactical magic around *const (), I didn't realize it was literally just the unit type!

or at least in relying on unstable layout of types

I figured the transmute was the wrong way to go, but was curious about whether the layout of DST pointers was considered 'stable' or just an implementation detail. Good to know it's the former!

Anyways, thanks for the information and recommendation!! I guess I'll go with the *const () even though like you say it shouldn't really matter. Hopefully it'll better signal that the types are completely irrelevant here.

trait object pointers are currently stored as this

You know, I actually did stumble across that! But thankfully for the other people stuck with me, we're using stable, and it's a nightly-only API (for good reason)!

Thanks for confirming the instability of the layout, and also pointing me in the direction of sanity: *const ().

Not only the layout of fat pointers but also the layout of tuples is unstable.

2 Likes

Given your description, your reply may be "I know and I don't care", but I'll throw this out there anyway in case.

When you're comparing addresses, especially across types and when dealing with DSTs (wide pointers), you may get false positives (as in "same address but not the same thing") from situations like

  • Compared a struct to the leading field (transitively)
  • Comparison involved a zero-sized type
  • Comparison involved a zero-length slice
  • Compared a slices of different length
  • Etc.
3 Likes

Thanks, I'll always take any additional information I can get!
Buttttt, I'm afraid you already guessed my response :slight_smile:

The actual type bound is T: Element + ?Sized, where Element is an internal trait implemented by a handful of structs.

None of these structs are zero-sized, contain slices, or one another (so no leading field concern). So they should skirt the edge concerns you list, but if there's one I've overlooked; like I said, I'll always appreciate information!

1 Like

Can you elaborate further about these cases why it can be false positive?

fn pointer_address_equal<T: ?Sized, U: ?Sized>(x: &T, y: &U) -> bool {
    (x as *const T).cast::<()>() == (y as *const U).cast::<()>()
}

fn main() {
    struct Foo(u16);
    #[repr(C)]
    struct Bar {
        foo: Foo,
        baz: f64,
    }

    let v = Bar {
        foo: Foo(1234),
        baz: 3.141592653589793,
    };

    let r1 = &v.foo; let r2 = &v; // not referencing the same “thing”, but:
    assert!(pointer_address_equal(r1, r2)); // passes

    // ==============================================================================

    let array = [(), (), ()];

    let r1 = &array[0]; let r2 = &array[1]; // not referencing the same “thing”, but:
    assert!(pointer_address_equal(r1, r2)); // passes

    // ==============================================================================

    let a = [1, 2, 3, 4, 5, 6];

    let r1 = &a[2..3]; let r2 = &a[2..5]; // not referencing the same “thing”, but:
    assert!(pointer_address_equal(r1, r2)); // passes

    // ==============================================================================

    let v = ([1, 2, 3], [4, 5, 6]);
    let r1 = &v.0[3..3]; let r2 = &v.1[0..2]; // not referencing the same “thing”, but:
    assert!(pointer_address_equal(r1, r2)); // passes (not actually guaranteed though)

    // ==============================================================================

    trait FooTrait {
        fn method(&self) {}
    }

    struct A;
    impl FooTrait for A {
        fn method(&self) { println!("I am a!" )}
    }

    trait BarTrait {
        fn bar_method(&self) {}
    }

    struct B;
    impl BarTrait for B {
        fn bar_method(&self) { println!("Mooo!?" )}
    }

    let x = Box::new(A) as Box<dyn FooTrait>;

    let y = Box::new(B) as Box<dyn BarTrait>;

    let r1 = &*x; let r2 = &*y; // completely different things conceptually, but:
    assert!(pointer_address_equal(r1, r2)); // passes (probably not guaranteed either)
}

Rust Playground

2 Likes

Another playground.

Oh and another, are these "the same thing"? Depends on your use case.

let s = String;
let one_thing: &dyn Display = &s;
let another_thing: &dyn Debug = &s;
2 Likes

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.