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…