Hook when address of object is changed

Is it possible in Rust to add some hook that is called on object unique address:

let obj = MyStruct { ... };
...
get_struct(obj); // Get struct by value that mean that address of this object on stack could be changed


impl AddrHook for MyStruct {
    fn address(&self, addr: *const Self) {
        // Do some handling or save somewhere
    }
}

Is it possible to do with Rust ?

No, there is no way to hook into moves in any way.

Maybe we should discuss it on https://internals.rust-lang.org/ ?

I guess, but this has been discussed before, and the conclusion that not having custom moves is far better than any benefits that custom moves can provide. This because it makes unsafe code far easier to check. Just imagine if let y = x; could panic!

Then there are similar, but different ideas

3 Likes

Additionally, we would have to modify every existing collection to handle these custom moves, i.e. Vec<_> would have to call the custom moves on resize. This has some far reaching effects that would make it very hard to implement.

2 Likes

It is not custom move that I asked ...

I just need to know the new address of object:

impl AddrHook for MyStruct {
    fn address(&self, addr: *const Self) {
        // Do some handling or save somewhere
    }
}

Without moving object ...

I need it to improve performance of FerrisGC, because currently for registering root pointer I have pointer to GcInternal/GcCellInternal and GcPtr ...

... this mean that I have also two allocation on heap and it is not very good for performance ...

That's what custom moves provide. The idea is something like this, (although I don't think I saw this exact variant)

unsafe trait OnMove {
    unsafe fn on_move(&mut self, new: &mut MaybeUninit<Self>);
}

Then whenever something is moved,

let x: T =  ...;

let y = x;

// will be translate to something like

let y: T;
unsafe { OnMove::on_move(&mut x, &mut y); }

Even if you just need the address, it has all the same concerns as full blown custom moves because it would need to be applied in all the same places. So what happens if AddrHook panics or otherwise? Would it be safe to not call AddrHook when something moves? If no, then this means we can no longer blindly copy things around like we do in say Vec<_> on reallocation.

There may be another way around this, so this sounds like an X-Y problem.

1 Like
impl OnMove for MyStruct {
    fn on_move(&self, addr: *const Self) {
        // Do some handling or save somewhere
    }
}

No, it is possible to implement it in such way:

let x: T =  ...;

let y = x;

// will be translate to something like

let y: T = x;
y.on_move(&y);

Actually it is possible even to optimize the trait:

impl OnMove for MyStruct {
    fn on_move(&self) {
        let new_pointer = self as *const MyStruct;
        // Do some stuff ...
    }
}

and:

let x: T =  ...;

let y = x;

// will be translate to something like

let y: T = x;
y.on_move();

That's not the point. You still have to call some custom behavior on every move.

let mut v = Vec::with_capacity(1);
v.push(MyStruct { ... });
// this second push reallocates, do we call `on_move` for the first element of the vec?
v.push(MyStruct { ... });

If yes, then is means we would have to rework all collections.

2 Likes

Yes, I think yes we should call on_move after object changed location ... but it is not an issue !!

With specialization we can just add additional implementaion:

impl Vec<T> {
  ...
}


impl Vec<T> where T: OnMove {
  ...
}

Two separate implementations ...

(We would have to change all existing collections, not just those in std). This means, ArrayVec, SmallVec, SmallBox, hashbrown, etc. This would almost certainly result in an ecosystem split. That's how fundamental this is.

3 Likes

Okay, this could be an issue ...

But what if we just introduce the feature inline trait bound:

impl Vec<T> {
    pub fn push(&mut self, value: T) {
        // This will panic or abort if we would allocate > isize::MAX bytes
        // or if the length increment would overflow for zero-sized types.
        if self.len == self.buf.capacity() {
            self.reserve(1);
            if <T: OnMove> {
              // Call on_move traits
            }
        }
        unsafe {
            let end = self.as_mut_ptr().add(self.len);
            ptr::write(end, value);
            self.len += 1;
        }
    }
}

It would be helpful even for other kind of programming ...

It is similar to C++17 if constexpr

That's another can of worms. :slight_smile: Sorry, I don't mean to discourage, it's just these are really fundamental parts of Rust.

We can already do something like this with specialization, but that's potentially unsound (because specializing lifetimes is unsound).

#![feature(specialization)]

trait MaybeGetAddr {
    fn maybe_get_addr(&self);
}

impl<T: ?Sized> MaybeGetAddr for T {
    default fn maybe_get_addr(&self) {}
}

impl<T: ?Sized + GetAddr> MaybeGetAddr for T {
    fn maybe_get_addr(&self) { GetAddr::get_addr(self) }
}

edit:

Okay, but this mean it is possible to implement such feature in compiler related to OnMove semantic ?

Seems like it is useful, because other folks also asked about similar feature ?

Let's add the following trait:

#![feature(specialization)]

trait MaybeNewAddr {
    fn maybe_new_addr(&self);
}

impl<T: ?Sized> MaybeNewAddr for T {
    default fn maybe_new_addr(&self) {}
}

impl<T: ?Sized + NewAddr> MaybeNewAddr for T {
    fn maybe_new_addr(&self) { NewAddr::new_addr(self) }
}

Just because it is possible to implement doesn't mean it's a good idea to implement. Right now, almost all unsafe code assumes that let y = x; has no side-effects, and relies upon this for safety, so adding this feature would break a large majority of unsafe code just by existing.

This makes it nigh impossible to check the safety of unsafe code because we need to check for these invisible moves.

4 Likes

But there will be no hidden move, as side-effect will be just safe callback:

trait NewAddr {
    fn new_addr(&self);
}
or
trait Move {
    fn on_move(#[no_mutation] &self);
}

Allow only reading from self and also disable Interior Mutability ...

Can this code panic?

std::mem::forget(x);

If yes, then we have a hidden side-effect move.

It is not possible to implement this for values.

Current semantics of = (as well as passing objects to functions, struct literals, etc.) clearly define that no type will ever have any custom move logic. This is a hard guarantee given by Rust 1.x. Changing that would be a major backwards-compatibility break, and Rust isn't planning such breaking changes.

It couldn't even work with an opt-in for some types only, because such types wouldn't be allowed to touch any code that uses =, function calls or struct literals. They would be useless.

The closest you can do is use Pin for types behind a pointer (where Rust's moves are equivalent to copying the pointer around), and provide a custom method for moving to another Pin.

5 Likes

Okay, I agree ...

Seems like this feature could break safety in Rust and I do not want to do this ...

What if instead Rust will provide the unique handle to each object ?

let x: T =  ...;
let handle: ^T = &x;  // Similar to pointer
let y = x;
let z = unsafe { *handle };

If the type was behind a pointer, then you could have something like that. BTW: it can't be using &, because borrows are for preventing moves.

let x = Pin::new(MaybeNewAddr::new(x));
let z: MaybeUninit::uninit();
x.move_and_notify(z.as_mut());
let z: Pin<MaybeNewAddr<_>> = z.assume_init();

There were some discussions about having umovable types in Rust. AFAIK currently there's no viable design for it, and Pin is a kinda a library-only partial substitute for this.

If Rust ever gets umovable types and placement new or &out pointers, then maybe it could have more syntax sugar for equivalent of the MaybeUninit<Pin<MaybeNewAddr<T>>>> dance.

1 Like