Design around mutability and shared trait references


#1

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


#2

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::Outputs to display the menu, and app_menu::Inputs 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 Outputs or Inputs, 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”.