I have two traits, Foo
and Bar
, each with start
and stop
methods. Some objects implement Foo
, some implement Bar
, and some implement both Foo
and Bar
.
Given the traits take in &self
, the following works to hold multiple references to the same concrete object (playpen):
use std::rc::Rc;
trait Foo {
fn do_this(&self);
fn stop_this(&self);
}
trait Bar {
fn do_that(&self);
fn stop_that(&self);
}
struct A;
impl Foo for A {
fn do_this(&self) {
println!("doing this");
}
fn stop_this(&self) {
println!("stopping this");
}
}
impl Bar for A {
fn do_that(&self) {
println!("doing that");
}
fn stop_that(&self) {
println!("stopping that");
}
}
fn main() {
let mut foo_list: Vec<Rc<Foo>> = Vec::new();
let mut bar_list: Vec<Rc<Bar>> = Vec::new();
for _ in 0..2 {
let a = Rc::new(A);
foo_list.push(a.clone());
bar_list.push(a);
}
for foo in &foo_list {
foo.do_this();
}
for bar in &bar_list {
bar.do_that();
}
for bar in &bar_list {
bar.stop_that();
}
for foo in &foo_list {
foo.stop_this();
}
}
However, I now have a requirement that some state is created and can be transferred across the start
and stop
methods. For example, start_this
starts a thread and saves the join handle, and stop_this
requests the thread to stop and waits for it.
To save the join handle or any other state and refer to it in stop
, there must be mutable state. So in short, what is a good way forward? I can see the following options:
-
&self
changes to &mut self
in both start and stop, which means wrapping each Foo or Bar in a mutex
- Use a private RefCell for the shared state inside Foo / Bar, and
.borrow_mut()
Is there a better way? I'm leaning towards the second bullet point right now
I came up with something that did not need mutability after all. See examples/stdio.rs@6269cb
app_menu::Output
and app_menu::Input
are my Foo
and Bar
traits from the original question.
The "activity" bits (object that uses app_menu::Output
and app_menu::Input
):
- There is a
MenuActivity
which requests any app_menu::Output
s to display the menu, and app_menu::Input
s to feed it input -- what menu item to select, or exit the menu.
- Receiving input / displaying output should not block, so that if there are multiple
Output
s or Input
s, each can be notified when they should stop displaying / providing input.
- Input to the menu activity is via a channel, let's call it ActivityChannel. Through this, the activity defines what kind of input it accepts.
-
StdioView
implements both traits.
Stdin Input related bits:
-
StdinEndpoint
runs on its own thread, which blocks on io::stdin().read_line
, and writes to a channel, let's call it StdinChannel. It writes Some(input)
where input
is what the user types -- so it just wraps it in an Option
-
StdinListener
also runs on its own thread. This listens to StdinChannel.
- When the
MenuActivity
requests for input (the Input::listen
method), StdioView
spawns a StdinListener
which takes in a clone of the StdinChannel's receiver, as well as ActivityChannel's sender. The StdinListener
acquires the stdin_rx mutex lock.
- When the
MenuActivity
no longer wants input (the Input::silence
method), StdioView
writes a None
to StdinChannel so that the StdinListener
stops listening to Stdin (and releases the stdin_rx mutex lock).
I have the following questions come up from this approach:
- Is using a
Mutex
the right approach to letting the StdinChannel receiver cross the Sync
boundary?
- It feels "wrong" to hijack the StdinChannel using an
Option
, and injecting a None
via the StdioView
to make the StdinListener
know "oh I should stop now", but I'm not sure what approach would be better. Maybe Condvar?
- Is this "sane"? Being serious, I am trying to come up with a program that can run both interactively and non-interactively, headlessly or not.
Hopefully this doesn't diverge too much from the original question, which is kind of a "here's what I came up with, please critique the design".