I am trying to implement something which Rust's ownership rules almost seem designed to make impossible. But it's very simple: I want a framework containing a hashmap where the values are a class, TextDocument
, which has a reference to the framework.
That itself is fairly tricky to do, but with RefCell<Weak<Framework<'a>>>,
it is possible.
However, I also want to be able to reach out from inside methods of the TextDocument
to modify things in the framework!
MRE:
use std::collections::HashMap;
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[allow(dead_code)]
pub fn str_type_of<T>(_: &T) -> &str {
std::any::type_name::<T>()
}
fn main() {
let mut framework = Framework::new();
framework.populate_map();
println!("framework.map {:?}", framework.map);
// now set up the pointer to the framework for the TextDocuments
let rc = Rc::new(framework);
for (key, td) in rc.map.iter(){
*td.framework.borrow_mut() = Rc::downgrade(&rc);
println!("key {} td {:?}", key, td );
// at this point obtaining a reference to Framework using td.framework is possible:
let weak_ref_to_framework = &*td.framework.borrow();
let inner_ref_to_framework = weak_ref_to_framework.upgrade().unwrap();
println!("inner_ref_to_framework {:?} type {}", inner_ref_to_framework, str_type_of(&inner_ref_to_framework));
// ... but it is impossible using an Rc<Framework> to alter anything in the Framework
// BAD: inner_ref_to_framework.name = "bumblebee".to_string();
// error: "trait `DerefMut` is required to modify through a dereference,
// but it is not implemented for `Rc<Framework<'_>>`"
}
// we have also obviously lost the ability to change anything in the Framework's Rc
// BAD: rc.name = "bubbles".to_string();
}
#[derive(Debug)]
struct TextDocument {
framework: RefCell<Weak<Framework>>,
i: usize,
}
#[derive(Debug)]
struct Framework {
map: HashMap<String, TextDocument>,
name: String,
}
impl Framework {
fn new() -> Framework {
Framework {
map: HashMap::new(),
name: "my framework".to_string(),
}
}
fn populate_map(&mut self){
for i in 0..5 {
let td = TextDocument{
framework: RefCell::new(Weak::new()),
i,
};
self.map.insert(format!("document {i}"), td);
}
}
}
The thing is, I know that such a setup, whereby it would be possible to modify the framework's fields from within methods of a given TextDocument
, would be safe. I accept to take responsibility for that guarantee.
Is there any way to do this in Rust?
NB I have been wrestling with this problem for some time, and implemented for example a workaround involving a third structure, with references going from the framework and from the TextDocument
s, a struct
MapHolder
. But it's far from ideal, and I'd really like to find something simpler, presumably not involving Rc
... but something else.
Another workaround possibility which occurs is a std::sync::mpsc::channel
: TextDocument
transmits, Framework
receives, and acts accordingly. But it seems crazy that I should be forced to go to such lengths to accomplish something so straightforward.
Edit
To answer ChrisB: no, there's a single framework. But the framework does various things such as keep track of how these documents have been processed. In a later stage of development the documents (text documents, such as MS Word .docx files in fact) will be parsed in parallel, and to keep track of the total number of words processed, for the purpose of sending inter-process messages to update a progress counter, an atomic variable has to be used, I think. In my workaround I used one or two Arc
s which were then made fields of the TextDocument
struct. Maybe this is the only acceptable Rust way ... but I simply can't understand why reaching out to the framework in this setup should be such a big deal. Is it not possible to stipulate that the TextDocument
s will always have a shorter lifetime than that of the Framework
?
The TextDocument
s also have to be able to detect an in-coming inter-process message, i.e. ordering everything to cancel. This also involves Arc
s within the TextDocument
struct
. The "natural" object to detect this seems to be the framework...
Specifically what I want to improve
To answer Chayim Friedman's question: I want methods of TextDocument
to be able to use a reference to change something in the framework, either directly change a property or to execute a &mut self
method. My current understanding is that simply isn't going to be possible, but I'm a newb in Rust. Rather tortuous use of Arc
fields in TextDocument
may be the absolutely ONLY solution to what I'm trying to achieve, but I just want to be more certain of that, from you, the experts. Everything the compiler does makes sense and obeys the logic of safety. But conversely the requirement seems so simple. That's what I'm struggling with.
PS thanks for the headsup on type_name_of_val
: 1.76, just upgraded! Also, yes, the lifetimes are redundant. They were put in due to compiler demands: not sure what's changed since. Will change MRE.
Wrap the Framework
data in RefCell
, e.g.:
use std::any::type_name_of_val;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::{Rc, Weak};
fn main() {
let mut framework = Framework::new();
framework.populate_map();
println!("framework.map {:?}", framework.map);
// now set up the pointer to the framework for the TextDocuments
let rc = Rc::new(framework);
for (key, td) in rc.map.iter() {
*td.framework.borrow_mut() = Rc::downgrade(&rc);
println!("key {} td {:?}", key, td);
// at this point obtaining a reference to Framework using td.framework is possible:
let weak_ref_to_framework = &*td.framework.borrow();
let inner_ref_to_framework = weak_ref_to_framework.upgrade().unwrap();
println!(
"inner_ref_to_framework {:?} type {}",
inner_ref_to_framework,
type_name_of_val(&inner_ref_to_framework)
);
inner_ref_to_framework.data.borrow_mut().name = "bumblebee".to_string();
}
rc.data.borrow_mut().name = "bubbles".to_string();
}
#[derive(Debug)]
struct TextDocument {
framework: RefCell<Weak<Framework>>,
i: usize,
}
#[derive(Debug)]
struct Framework {
map: HashMap<String, TextDocument>,
data: RefCell<FrameworkData>,
}
#[derive(Debug)]
struct FrameworkData {
name: String,
}
impl Framework {
fn new() -> Framework {
Framework {
map: HashMap::new(),
data: RefCell::new(FrameworkData {
name: "my framework".to_string(),
}),
}
}
fn populate_map(&mut self) {
for i in 0..5 {
let td = TextDocument {
framework: RefCell::new(Weak::new()),
i,
};
self.map.insert(format!("document {i}"), td);
}
}
}
Note that this will not allow you to mutate the map from a document. This is because if we include the map in the RefCell
, in order to get a component, we need to borrow it and then access its Weak
, but we cannot borrow this Weak
's RefCell
mutably in order to mutate the map because we're already borrowing the framework.
The solution (if needed) is to detach the processed document from the framework, for instance by wrapping documents in Rc
(possibly with RefCell
) and clone it, release the framework guard, then access the framework via the Weak
, or by removing the document from the map, accessing its Weak
, then inserting it again.
Also note that if all documents are known upfront Rc::new_cyclic()
can provide much friendlier experience:
use std::any::type_name_of_val;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::{Rc, Weak};
fn main() {
let rc = Framework::new();
println!("framework.map {:?}", rc.map);
for (key, td) in rc.map.iter() {
println!("key {} td {:?}", key, td);
// at this point obtaining a reference to Framework using td.framework is possible:
let inner_ref_to_framework = td.framework.upgrade().unwrap();
println!(
"inner_ref_to_framework {:?} type {}",
inner_ref_to_framework,
type_name_of_val(&inner_ref_to_framework)
);
inner_ref_to_framework.data.borrow_mut().name = "bumblebee".to_string();
}
rc.data.borrow_mut().name = "bubbles".to_string();
}
#[derive(Debug)]
struct TextDocument {
framework: Weak<Framework>,
i: usize,
}
#[derive(Debug)]
struct Framework {
map: HashMap<String, TextDocument>,
data: RefCell<FrameworkData>,
}
#[derive(Debug)]
struct FrameworkData {
name: String,
}
impl Framework {
fn new() -> Rc<Framework> {
Rc::new_cyclic(|this| Framework {
map: (0..5)
.map(|i| {
let td = TextDocument {
framework: Weak::clone(this),
i,
};
let key = format!("document {i}");
(key, td)
})
.collect::<HashMap<_, _>>(),
data: RefCell::new(FrameworkData {
name: "my framework".to_string(),
}),
})
}
}