Copy-able handle type with shared back reference

#1

Trying to build a subset of Symbolic C++ in Rust.

My current approach is to make the variables light-weight handle classes that implement Copy, so that I can write x + y without moving the variables into the expression, so that I can (re)use them to build multiple expressions. Now those handles need a back-reference to the machinery, but Rc is impossible, because it is not Copy.

Bare bones example:

#[derive(Copy, Clone)]
struct Symbolic<'s> {
    store: &'s SymbolicStore,
}

struct SymbolicStore {
    // machinery
}

// this needs lifetime annotation saying that SymbolicStore must outlive Symbolic
impl SymbolicStore {
    fn make(&mut self) -> Symbolic {
        Symbolic {
            store: &self,
        }
    }
}

(Playground)

I have tried various forms of lifetime annotation, up to and including

impl<s': n', s'> SymbolicStore<s'> {
    fn make(&mut self) -> Symbolic<'n> {
        Symbolic {
            store: &self,
        }
    }
}

and get very detailed error messages, but still fail to understand how to make myself understood by the compiler. Will the whole idea even work without using unsafe? :roll_eyes:

#2

&mut is for exclusive access, so by definition there can exist only one of it, so it’s not Copy either.

You could try using shared references (plain &) and then Atomic or Cell inside to get interior mutability.

Also you could use Rc, and rely on Add, etc. creating clones internally.

#3

Aha! So the fundamental flaw is that I cannot borrow self while holding a mutable borrow? Unfortunately I will need that &mut for reasons not apparent in the minimal example.

Would you think the handle approach is salvageable? Coming from C++, Rust seems more restrictive about copying objects.

#4

&mut is playing the borrow checker on hard mode. That’s the most restrictive type, and it’s best used only for small/well-defined scopes.

Apart from aforementioned interior mutability and refcounting, another option could be to use unchecked pointers *mut Self, but still use lifetime annotations to ensure they’re valid (attach them to PhantomData). This way Rust will prevent use-after-free for you, and you will be responsible for upholding the shared XOR mutable guarantee.

#5

:rofl:

Alright, many thanks for the pointers, I’ll dig deeper into those; only skimmed them so far, was hoping to get away without needing them for the time being… :disappointed:

#6

You may want to look at using some form of an arena to hold the symbol info, and then Symbolic is a cheap copyable reference into the arena (could be a usize-like index) where the meat of the symbol is stored.

If you need make() to take a &mut self then you can’t return anything with a lifetime derived from the borrow of self - it’ll extend the mutable borrow of self, and you won’t be able to call make() until that borrow dies. This implies Symbolic shouldn’t have that lifetime parameter, and thus reference back to the symbol store. So instead you may want to structure things like (pseudocode):

// usize is index into symbol store
#[derive(Clone, Copy, Debug)]
struct Symbolic(usize);

struct SymbolStore {
   syms: Vec<Symbol>,
}

impl SymbolStore {
   fn make(&mut self) -> Symbolic {
       let symbol = Symbol::new(...);
       let idx = self.syms.len();
       self.syms.push(symbol);
       Symbolic(idx)
   }
}

Then wherever you need to evaluate the symbols or otherwise get the data from the symbol store, you pass the symbol store as an explicit reference to those places. Or something like that.

Also, take a look at https://crates.io/crates/generational-arena to help you with arena usage, and this blog from Catherine West that I think will be applicable to you (and figures into the generational arena crate above).

2 Likes