Rust cycle weak reference problems

Hello I have the following problem I basically want a simple api and this is my design:

(Playground Design V1)
(Playground Design V2)

basically I have a struct Manager and a struct Document and I want to have a Weak reference to documents inside the manager and one weak reference from the document to the manager.
and the document should be created inside the implementation of the manager, is this even possible?

I try to mimic an api in another language which uses the weakreference to basically cleanup the code or in the case of the document to get another object from the manager (operations which are "global")

Weak is used to break parent/child relationship where one is a Weak ref and other is not. In your case, both are Weak!!

Besides, Manager should own the Documents. It being the manger. And then you can have a Weak ref in your Document. This way you wouldn't have a cycle.

1 Like

well the problem here is that I want a drop method of the documents so it would be impossible to let the manager own the documents or am I wrong?

under normal operations my manager would be alive the whole application runtime and my documents would come and go.
a manager can hold operations and operations can hold documents, documents are only held inside operations but if documents are dropped, they need to remove itself from the operations and from the manager.

well the problem here is that I want a drop method of the documents so it would be impossible to let the manager own the documents or am I wrong?

I dont think that's true. I made a minor modification to your first design where I just removed what I said and it works.

Would this work ?

struct DocumentId(u64);

struct Document {
  manager: Rc<Manager>,
  id: DocumentId,
  data: ...
}

struct Manager {
  next_free_id: DocumentId,
  docs: HashMap<DocumentId4, Weak<Document>>
}
1 Like

@AlwaysLearning I found that hard part is actually getting a weak reference to the Manager inside the open_document method since I can't have a Weak that contains a reference?

@zeroexcuses that's the design I'm actually looking for, but I could not get it to work...

Where do you get stuck?

Yeah I added a todo!() to make a point.
But after going through your design...im still not sure why you need Weak ref to document from Manager.

I see two issues:

  • In open_document(&self) returns a Document but it isnt clear how Manager achieves this. How can an empty Vec in manager return a Document? Something cannot be produced from nothing.
  • You are using Rc just so you can downcast to a Weak?

In my opinion sample code from @zeroexcuses should be a start but without Weak.

I was under the (possibly incorrect) impression that if the only ptr to a Document is from the Manager, then it should be dropped (because nothing else is using it).

actually it is a wrapper to a c-api and when the manager gets released all documents need to be released aswell (of course), maybe in rust I need to think differently and the manager should not have a weak ref (when thinking about it...).

basically my open_document also takes a string/pathbuf and returns a result and when the document will be opened I create a struct with the "raw" pointer and return it, else I will return Err.
the document can than be attached to a operation a operation will be created with the manager. it's a bit of a hazzle to use the api correctly, it's probably hard as a rust beginner to think how I can solve it.

Here is a small diagramm that shows how it works:

Unbenanntes Diagramm

i.e. if manager goes out of scope (via drop) it should cleanup all operations and documents, if document goes out of scope it should remove itself from the operation (if it is attached to one)
if a operation goes out of scope basically nothing happens.

Edit: small info:
I'm coming from C# so I'm still have trouble with Rc/Weak and Ownership/Lifetime, basically in C# I have a class Manager which has a HashMap<IntPtr, WeakRef> to Operations and Documents and cleans them up inside Dispose (like Drop), maybe I try to hard to mimic that and my idea was that the Drop function will also free the memory of the document ptr (the document struct only has methods that return Result, so if the ptr is null i can return an err)

Edit2:
when thinking about the answer's I think I made a big mistake to mimic the C# api too much since, rusts lifetime probably will do it for me if the document contains a ref to the manager but the manager does not need to know about documents since they need to go out of scope before the manager, silly me.

prob as easy as:

pub struct DocumentManager {
    lock: RwLock<()>,
    native_ptr
}

impl DocumentManager {
    pub fn new() -> Result<DocumentManager> {
        let err = unsafe {  };
        if err < native_err {
            return Err(Error { error_code: err });
        }

        Ok(DocumentManager {
            lock: RwLock::new(()),
        })
    }

    pub fn open_document<'a, T: Into<PathBuf>>(
        &'a self,
        file_path: T,
        password: Option<&str>,
    ) -> Result<Document<'a>> {
        let _locked = self.lock.read().unwrap();

        let document = Document::new(
            self,
            file_path
                .into()
                .to_str()
                .ok_or(Error { error_code: -500 })?,
            password,
        )?;

        Ok(document)
    }
}

impl Drop for DocumentManager {
    fn drop(&mut self) {
        let _locked = self.lock.write().unwrap();
        unsafe { ... };
    }
}

pub struct Document<'a> {
    manager: &'a DocumentManager,
    lock: RwLock<()>,
    handle: native_ptr,
}
impl<'a> Document<'a> {
 fn new(
        manager: &'a DocumentManager,
        file_name: &str,
        password: Option<&str>,
    ) -> Result<Document<'a>> {
        let c_file_name = CString::new(file_name)?;
        let mut pdf_document = MaybeUninit::<native_ptr>::uninit();
        let err: native_err;
        unsafe {
            ...
        };
        if err < native_err {
            return Err(Error { error_code: err });
        }

        Ok(Document {
            manager,
            lock: RwLock::new(()),
            handle: unsafe { pdf_document.assume_init() },
        })
    }
}

impl<'a> Drop for Document<'a> {
    fn drop(&mut self) {
        let _locked = self.lock.write().unwrap();
        unsafe { ... };
    }
}

I'm also unsure about the RwLock if it should contain the native_ptr or if it should stay ()...

it's probably impossible from multiple threads to deref the manager before the document

I think I gave the incorrect impression that this approach might or might not work. Previously, using similar structure, I built a VersionedObj where a Manager stored multiple versions of an Object (identified by a Tag).

If I understand your problem correctly, and you post the error you run into, I might be able to guide you through.

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.