Mutable callback container

Hi everyone,
I'm a rust beginner with extensive C++ background. To teach myself the language, I'm trying to implement an epoll reactor. One way I would do this in C++ is for the reactor to own a list of callbacks objects that can alter the reactor, to say, add a new listener/callback or remove itself.
I am running into a compile issue because I cannot simultaneously mutably borrow a reference to the callback owned by the reactor and also call the callback with a mutable reference to the reactor.
Here's a summarized version of my problem:

use std::collections::HashMap;

trait Iface {
  fn fun(&mut self, c: &mut Container);
}

struct Container {
  map: HashMap<i32, Box<dyn Iface>>,
}
impl Container {
  fn do_it(&mut self) {
    if let Some(i) = self.map.get_mut(&123) {
    // if let Some(mut i) = self.map.remove(&123) {
      i.fun(self);
      // self.map.insert(123, i);
    } else {
      panic!("unexpected");
    }
  }
}

struct Thing {}
impl Iface for Thing {
  fn fun(&mut self, c: &mut Container) {
    c.map.insert(456, Box::new(Thing{}));
  }
}

fn main() {
  let mut c = Container{map:HashMap::new()};
  c.map.insert(123, Box::new(Thing{}));
  c.do_it();
  for key in c.map.keys() {
    println!("{}", key);
  }
}

I workaround here (commented) was removing the callback from the map, then re-adding it after, but this seems clunky and inefficient.
What's an idiomatic way to do this in rust?
Thanks

So, one problem here is that anything with mutable access to the HashMap can just replace the whole HashMap / drop all the items in it, etc. This is why you can’t simply hold a reference “into” the map while offering some other place in the code mutable access.

Using Rust’s types for interior mutability (RefCell) and shared ownership (Rc) can make this work though. The Box<dyn Iface> becomes an Rc<RefCell<dyn Iface>>, this way a reference to an item in the map can be obtained by creating a new copy of the reference-counted pointer. Mutable access through RefCell implements dynamic checks, in effect “locking” items while you’re mutating them in one place. (This means that due to the RefCell, a call-back directly or indirectly calling itself will result in a panic; probably not a problem though, since in your original approach, the call-back didn’t exist in the map at all while it was running.)

I’m not sure about the performance implications when compared to the “removing and putting back in” approach, anyways, here’s an adaptation of your code example.

use std::{cell::RefCell, collections::HashMap, rc::Rc};

trait Iface {
    fn fun(&mut self, c: &mut Container);
}

struct Container {
    map: HashMap<i32, Rc<RefCell<dyn Iface>>>,
}
impl Container {
    fn do_it(&mut self) {
        if let Some(i) = self.map.get(&123) {
            let i = Rc::clone(i);
            i.borrow_mut().fun(self);
        } else {
            panic!("unexpected");
        }
    }
}

struct Thing {}
impl Iface for Thing {
    fn fun(&mut self, c: &mut Container) {
        c.map.insert(456, Rc::new(RefCell::new(Thing {})));
    }
}

fn main() {
    let mut c = Container {
        map: HashMap::new(),
    };
    c.map.insert(123, Rc::new(RefCell::new(Thing {})));
    c.do_it();
    for key in c.map.keys() {
        println!("{}", key);
    }
}

(in the playground)

Note that I’m assuming here that the &mut self argument in Iface is necessary, even though your Thing doesn’t actually use it. Without that, you could skip the RefCell. (You could still add a RefCell or Cell or similar to fields of an Iface implementor, in case you do need mutation; but it would be a more fine-grained approach.)

use std::{collections::HashMap, rc::Rc};

trait Iface {
    fn fun(&self, c: &mut Container);
}

struct Container {
    map: HashMap<i32, Rc<dyn Iface>>,
}
impl Container {
    fn do_it(&mut self) {
        if let Some(i) = self.map.get(&123) {
            let i = Rc::clone(i);
            i.fun(self);
        } else {
            panic!("unexpected");
        }
    }
}

struct Thing {}
impl Iface for Thing {
    fn fun(&self, c: &mut Container) {
        c.map.insert(456, Rc::new(Thing {}));
    }
}

fn main() {
    let mut c = Container {
        map: HashMap::new(),
    };
    c.map.insert(123, Rc::new(Thing {}));
    c.do_it();
    for key in c.map.keys() {
        println!("{}", key);
    }
}

(in the playground)


The types presented in this answer aren’t thread safe (i.e. Rust won’t let you use them across multiple threads). If you need those capabilities, there are the types Arc to replace Rc, and typically Mutex takes the role of RefCell.

1 Like

Ah! Terrific, thank you very much for this detailed explanation.
I had seen this construct before but its usefulness didn't click until I read your explanation.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.