This came out a bit longer that I expected, feel free to ignore it if you don't want to bother lol
I'm somewhat inexperienced with Rust and trying my first bigger project. I'm trying to figure out how to structure a pattern where many parts of the code need to refer to a "context" to perform their operations. Let me try to illustrate my problem with a MWE. Say there's a type UserSet
that maps usernames to ids.
struct UserSet;
impl UserSet {
pub fn add(&mut self, user: String) {}
pub fn get(&self, user: &str) -> u32 {}
}
Several other structs need a UserSet
as a "context" with respect to which to call get
to obtain an id to work with. This is not a singleton so different values use different UserSet
s. So I would have something like
struct Foo<'ctx> {
user_set: &'ctx UserSet,
// ...
}
impl Foo<'_> {
pub fn do_something(&mut self, user: &str) {
let user_id = self.user_set.get(user);
// do stuff with user_id
}
}
i.e. when creating these I need to pass a reference to a UserSet; then the struct's methods can call self.user_set.get(...)
whenever they need.
But this doesn't work: now these types lock in a shared reference, so I can no longer call add
for any UserSet
which is "acquired" by a Foo
or a Bar
(since add
requires a mutable reference to self
). So code like
let mut user_set = UserSet;
user_set.add("alice".to_owned());
let foo = Foo { user_set: &user_set, /* ... */ };
user_set.add("bob".to_owned());
foo.do_something("alice");
the last line doesn't compile. However, there should technically be no problem (at least in a single-threaded context), because the shared ref to user_set
is only being used while inside the function call (if I'm making myself understood, sorry if my explanation is a bit fuzzy).
Solution 1: Pass &UserSet
explicitly
Just express what I just said above in code, and pass the shared ref to functions that need it. Then the compiler can see that the So Foo
would be
struct Foo {
// ...
}
impl Foo {
pub fn do_something(&mut self, user: &str, user_set: &UserSet) {
let user_id = user_set.get(user);
// do stuff with user_id
}
}
and the code
let mut user_set = UserSet;
user_set.add("alice".to_owned());
let foo = Foo { /* ... */ };
user_set.add("bob".to_owned());
foo.do_something("alice", &user_set);
would happily compile.
Problem: It's less ergonomic I guess, but the worst part is that I have to explicitly pick and pass the correct UserSet
for each Foo
every time I call do_something
. But part of the correctness of Foo
is that it is working wrt a UserSet
, the same one every time. It becomes very error prone to require the caller to never make a mistake and manually pass the correct UserSet
every time, when it's much more natural to pass it only once when Foo
is created (and have the reference lifetime ensure it stays alive at least as long as the Foo
does).
Solution 2: Use RefCell
Like I said let's forget about Sync
and say we're working in a single-threaded context. We can just wrap user_set
in a RefCell
to get mutable access through a shared reference.
struct Foo<'users> {
user_set: &'users RefCell<UserSet>,
// ...
}
impl Foo<'_> {
pub fn do_something(&mut self, user: &str) {
let user_id = self.user_set.borrow().get(user);
// do stuff with user_id
}
}
and
let mut user_set = RefCell::new(UserSet);
user_set.borrow_mut().add("alice".to_owned());
let foo = Foo { user_set: &user_set, /* ... */ };
user_set.borrow_mut().add("bob".to_owned());
foo.do_something("alice");
Problem: Works I guess, as long as there is only one thread having references to this (enforced by the Foo
s not being Send
), but the performance hit is bad for something that should be statically known to be okay.
Solution 3: Use UnsafeCell
This leaves me UnsafeCell
. Basically the interior mutability of Solution 2 but no runtime checks. As long as I promise that the usage of mutable references doesn't overlap with the usage of shared refs (by ensuring those shared refs are only used in the scope of e.g. do_something
, and other functions of Foo
), I should be fine?
struct Foo<'users> {
user_set: &'users UnsafeCell<UserSet>,
// ...
}
impl Foo<'_> {
pub fn do_something(&mut self, user: &str) {
let user_id = unsafe{&*self.user_set.get()}.get(user);
// do stuff with user_id
}
}
let mut user_set = UnsafeCell::new(UserSet);
unsafe{&mut *user_set.get()}.add("alice".to_owned());
let foo = Foo { user_set: &user_set, /* ... */ };
unsafe{&mut *user_set.get()}.add("bob".to_owned());
foo.do_something("alice");
Problem: It is ugly, and unsafe is scary so I'm not really certain this is even correct.
Question
The question is just how best to structure this sort of pattern in Rust. Any of the 3 solutions? A better one? To be clear: the Foo
s only need &
(shared) access, and only 1 other &mut
(exclusive) reference exists, and they don't overlap.
Thanks for reading!