I've got a reasonably complex app that is subdivided into a small number of reasonably complex, highly-interdependent structs. Right now we just have Arc
s and Weak
s spread around pretty haphazardly. I'd love to remove them, and assume there's a better way to accomplish what I'm trying to do, but haven't quite found a good solution yet.
All of the sub-parts in question have interior-mutability, so don't need to worry about mut
here. However, each of the sub-parts interact with each other through a central App
that is instantiated at startup, and destroyed at shutdown. The only exception to this is in integration tests, where multiple App
s might exist at the same time, and be destroyed before the runtime completes.
This was originally just a giant static/global singleton, which made things simple, but didn't work well for tests.
The current structure is (roughly) as follows:
struct App {
things: ThingManager,
stuff: StuffManager,
actions: ActionManager,
}
impl App {
fn new() -> Arc<App> {
// App instantiates each of the Thing/Stuff/Actions, and gives them a weak reference to itself
let app = Arc::new(App { ThingManager::new(), StuffManager::new, ActionManager::new() })
app.things.set_app(&app)
app.stuff.set_app(&app)
app.actions.set_app(&app)
}
}
// Each of the Manager's look like:
struct ThingManager {
app: OnceCell<Weak<App>>,
// Other fields
}
impl ThingManager {
fn set_app(&self, app: &Arc<App>) {
self.app.set(Arc::downgrade(app))
}
fn get_app(&self) -> Arc<App> {
self.app.get().upgrade()
}
// And unfortunately touch other parts of the app
fn complex_logic(&self) {
let app = self.get_app();
let some_stuff = app.stuff.find_specific_stuff();
let action = app.action.do_something(some_stuff);
// etc
}
}
I've looked into using things like arena allocators (bumpalo, generational-arena), but every time I start implementing I end up polluting my structs with <'app>
lifetimes, and they very quickly spread everywhere in the code (lots of functions take an App
, and need the lifetimes specified, etc).
I know my ThingManager will live exactly as long as my App, and when App is dropped, I'd like to drop the managers as well, how do I represent this cleanly? I'm not super excited to use unsafe blocks, but if they were limited in scope (manual setup/teardown) and were "safe" assuming my constraints were maintained (Managers won't outlive an App, etc), I wouldn't be too opposed...
I don't know your use-case in detail but it looks like the following design may be a reasonable one:
struct App {
things: ThingManagerInner,
stuff: StuffManagerInner,
actions: ActionManagerInner,
}
impl App {
fn new() -> App {
App {
things: ...,
stuff: ...,
actions: ...,
}
}
fn things(&self) -> ThingManager<'_> {
ThingManager { app: self }
}
}
// Each of the Manager's look like:
struct ThingManagerInner {
// Other fields
other_field: usize,
}
struct ThingManager<'a> {
app: &'a App,
// optionally add other references here as necessary if they are conditional based on app, deeply nested, inside a Vec...
}
impl Deref for ThingManager<'_> {
type Target = ThingManagerInner;
fn deref(&self) -> Self::Target {
&self.app.things
}
}
impl ThingManager<'_> {
// And unfortunately touch other parts of the app
fn complex_logic(&self) {
let some_stuff = self.app.stuff().find_specific_stuff(self.other_field);
let action = self.app.action().do_something(some_stuff);
// etc
}
}
In Rust, you usually want to orient your struct construction around data rather than behavior. One can then construct behavior layers on top.
As a rule of thumb, you don't need unsafe
for anything (besides FFI), and it's generally more a footgun than anything else, given the additional guarantees that one has to provide when writing unsafe in Rust compared to C (aliasing because of noalias optimizations, covariance/invariance,contravariance...) and the fact that there's always just a better design that preserves maximum performance while staying fully safe.