Constness when interfacing with foreign code

I'm interfacing Rust with C++ (using cxx) and faced the following situation:

The Rust code calls an adapter class, and this adapter class holds a (non-const) std::unique_ptr to the object implementing the functionality.

As the adapter class has only this pointer as state, and it's never mutated, all adapter methods are marked as const (which is correct by C++'s understanding of constness: no member variable is changed.)

Therefore the interface to call the adapter methods is made using only self: &T, and this compiles correctly.

I know that the pontee of the std::unique_ptr has state and mutates it when the adapter calls some of its methods (it does so, however, using a mutex, so concurrent access would not be a problem.)

That is, we have something like:

Rust Code -- self: &T --> Adapter C++ Class (const) -- std::unique_ptr --> C++ business logic (mutating)

My question is: From Rust's perspective is it fine to ignore that there're state changes happening on the C++ side? Or could it be that the compiler could perform some optimization based on the expectation of constness, that could trigger some UB?

In other words, as long as the Adapter is const, that's all that Rust needs to know about the C++ interface, or if there's mutability down the road then I should let Rust know about it, say, by wrapping the Adapter into an UnsafeCell?

i am fairly confident rust does not assume calls through ffi to be guaranteed to return the same results each time so if rust only ever interacts with the adapter and the adapter itself does not change then rust is fine to have it behind a & because that is the extent to witch it expects the invariant to be upheld.

just know that you cannot convert the unique_ptr itself into a & since the data pointed by it does indeed change

If it's fine to call self.foo several times, and if it's not going to cause UB if you call it reentrantly (i.e. from a previous self.foo() call), then just a shared reference should be OK. Otherwise, use exclusive one which is &mut.

If your not dereferencing in Rust it will have no exposure to any internal mutation. Using UnsafeCellwould be unnecessary, similar to how Pin can be unnecessary when you full control heap content (eg from raw Box) that your internal code depends on being in a fixed location.

In general, there are three ways to have mutable state behind an & reference:

  1. Hold the data inline in UnsafeCell.
  2. Hold the data behind a pointer, such that no pointer to it has aliasing rules (no &, &mut, Box, etc.).
  3. Hold the data outside of memory entirely (e.g. a file or pipe).

Your case is the second. From what you’ve written, your C++ code is roughly equivalent to this Rust code:

struct Adapter {
    data: *mut Implementation,
}
struct Implementation {
    value: u32,
}

impl Adapter {
    pub fn new() -> Self {
        Adapter {
            data: Box::into_raw(Box::new(Implementation { value: 0 })),
        }
    }
    pub fn increment(&self) {
        unsafe { (*self.data).value += 1 }
    }
    pub fn get(&self) -> u32 {
        unsafe { (*self.data).value }
    }
}

impl Drop for Adapter {
    fn drop(&mut self) {
        drop(unsafe { Box::from_raw(self.data) });
    }
}

fn main() {
    let adapter = Adapter::new();
    adapter.increment();
    assert_eq!(adapter.get(), 1);
}

You can run this Rust code under Miri in the playground and see that it has no objection. (More care would be needed if Adapter: Sync were wanted, but it is automatically !Sync by default.)