How to prevent changing my smart pointer, but not the data it points to?

I have this custom type called a SchemaBox, which is similar to a Box<dyn Any> in purpose, but instead of storing the TypeId of the inner type, it stores it's Schema, which is a description of the type's memory layout.

pub struct SchemaBox {
    ptr: OwningPtr<'static>,
    schema: &'static Schema,
}

I have an issue where I need to give you something like a &mut SchemaBox, but I need to prevent you from replacing that schema box with a box with a different schema.

This is feeling vaguely familiar to Pin, but I haven't put my finger on whether or not I can use it to help.

Because fundamentally I need to allow you to use any functions on SchemaBox that modify data through the pointer, but never let you replace that box with a new box that has a different pointer or schema.

I can't have a Pin<SchemaBox> because SchemaBox doesn't implement Deref, since there's no concrete type to deref to. It's almost like I need a new Pin type that lets you call methods on the type, but not let you mem::swap() or even *schema_box_1 = schema_box_2.

Any ideas?

Oh, I think I may have just figured it out.

I already have a couple types, SchemaRef<'pointer> and SchemaRefMut<'pointer>, which are essentially a borrow to a SchemaBox or any other value that has a schema. If I make sure to give out only SchemaRefMut, then it becomes impossible to change the pointer of the box.

I'm going to try that out and see how it works.

2 Likes

In general in Rust, given a T or a &mut T, the owner can choose to replace the T with another of the same type.

Therefore, if whatever you do is both sound and achieves your goal, then it will be an example of interior mutability — some mechanism whereby the caller can start with &Container and end up with &mut Contents, like RefCell. If the operation doesn't start with &, then you will likely not have succeeded in preventing the mutation you want to prevent.

3 Likes

Good point.

While exploring this with Box<dyn Any>, which is similar to what I'm trying to accomplish, I just stumbled on an interesting trick that might give me what I want, though. Trait objects which are not Sized ( that might be all of them IIUC ), cannot be swapped or re-assigned. So this doesn't work:

fn main() {
    let mut data = Box::new(0usize) as Box<dyn Any>;
    let mut data2 = Box::new(String::from("hi")) as Box<dyn Any>;
    let data_ref = &mut data as &mut dyn Any;
    let data2_ref = &mut data2 as &mut dyn Any;

    std::mem::swap(data_ref, data2_ref)
}
   Compiling playground v0.0.1 (/playground)
error[E0277]: the size for values of type `(dyn Any + 'static)` cannot be known at compilation time
 --> src/main.rs:9:5
  |
9 |     std::mem::swap(data_ref, data2_ref)
  |     ^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `(dyn Any + 'static)`
note: required by a bound in `std::mem::swap`
 --> /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/core/src/mem/mod.rs:726:1

The same happens with trying to assign to it:

error[E0277]: the size for values of type `(dyn Any + 'static)` cannot be known at compilation time
 --> src/main.rs:9:5
  |
9 |     *data_ref = *data2_ref;
  |     ^^^^^^^^^ doesn't have a size known at compile-time
  |
  = help: the trait `Sized` is not implemented for `(dyn Any + 'static)`
  = note: the left-hand-side of an assignment must have a statically known size

I'm wondering if I could just return a trait object like &mut dyn SchemaRefMutApi to grant access to the methods on SchemaRefMut, but not allow swapping or assignment to the actual value.

I'm assuming this introduces runtime overhead, though, even if I always know that the &mut dyn SchemaRefMutApi will concretely be a SchemaRefMut.

Is it possible maybe to create a type that is intentionally unsized, just to prevent swapping and assignment, without using a trait object?

Yes.

2 Likes

Wait, I just realized, I'm pretty sure I was right earlier and returning a SchemaRefMut is fine, because it's like returning a &mut T that you got from a Box<T>.

Having a mutable reference allows you to change the data that is pointed to, but you can't modify the original box which is what I'm worried about. Nothing you do without having a &mut Box<T> will allow you to change the pointer that is inside of the orignal box. It will always point to the same memory, even if it has a method that returns a "copy" of it's internal pointer in the form of a &mut T.

For example:

use std::ops::DerefMut;

fn main() {
    let mut data = Box::new(0usize);
    let mut data_ref = data.deref_mut();

    let other_data_ref = &mut 7usize;

    data_ref = other_data_ref;

    assert_eq!(*data, 0);
}

My SchemaBox has a method as_mut():

    pub fn as_mut(&mut self) -> SchemaRefMut<'_, '_> {
        SchemaRefMut {
            ptr: self.ptr.as_mut(),
            schema: self.schema,
            parent_lifetime: PhantomData,
        }
    }

It returns a new SchemaRefMut that has a pointer in it with a lifetime tied to the SchemaBox. If I give you this new SchemaRefMut, then even if you replace that SchemaRefMut with a new SchemaRefMut that has a pointer to a new type, the pointer in the SchemaBox remains unchanged.

( ignore the double-lifetime thing, it should be un-related to this discussion )


That's cool even if I don't use it. Thanks. :smiley:

Yeah. A &mut T doesn't allow you to change the address of the pointed object. (As far as I can tell, anyway.)

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.