Automatically Delete Struct When Some Other code Drops last Reference?

I was about to start coding memory management, in Rust, when I stopped myself. (Just in time, too!)

Here is my problem…

I'm building an API where client code can register interest in future events. When a client registers for events I return an opaque struct that the client code can use to ask about those future events.

Inside my API I also need to keep a copy of these structs so I can refer to them when processing events.

I was going to put my copy of these structs in a vec, handy for me to access.

But how do I automatically delete my copy when the client code drops its last reference?

Some cleverness with weak references and impl Drop? (Is there some existing container that would do this for me? Some kind of vec that automatically deletes things that no one else is interested anymore??)

Thanks,

-kb

Sounds to me like you are trying to implement a custom allocator/an arena. Maybe have a look at crates like typed-arena and see if it could help you? I think this'd be easier than having to deal with a Vec<MaybeUninit<T>> or Vec<ManuallyDrop<T>> or what else yourself.

Quickly looking at typed-arena, I don't think that does it for me. "Allocated objects are destroyed all at once, when the arena itself is destroyed."

I want client code to be able create structs when it pleases, and I want Rust to deallocate these structs when they go out of scope, incrementally, back and forth. Very Rust feeling so far, right?

But I want to keep shadow copies of these structs that go away automatically when the client code drops its last reference. Feels very weak ref to me…but I've never played with weak refs before.

-kb

Hmm okay. That does sound like Weak could be used. But Weak pointer don't just magically vanish once the underlying value has been dropped, so you'd periodically have to upgrade them and throw the dropped clients out.

In addition to garbage collection of the dropped client structs, maybe an unstated problem is how to delete your copy of a struct efficiently and iterate non-dropped items efficiently?

A variation of the typed-arena that @jofas suggested is a slotmap. This allows deleting (reusing) and iterating slots safely. There are variations of the slotmap that give good iteration performance when there are holes in the map.

For garbage collection two ideas come to mind:

  1. Use the drop of the client struct to send an event or some other sort of signal that results in removing your copy. This is vague, I mention it in case you happen to have a good way to do it. I haven't tried this sort of thing using Drop to be honest.

  2. If you don't need to garbage collect more often than you iterate: Give out a Arc<ClientStruct> and keep a Weak<ClientStruct> rather than a copy of the struct itself. When iterating slots, remove the slot if Weak::upgrade returns None.

2 Likes

Yes, I was going to say that you could perhaps remove the stale references when dispatching events. Weak can be checked for liveness, so they could for example be filtered with Vec::retain.

Edit: there's even Vec::retain_mut.

2 Likes

You have to upgrade them to use them anyway, right? So one could theoretically handle this the next time you try to use one to send an event--check for liveness and throw away the weak ref if it isn't live anymore.

Maybe this is not ideal if the events are infrequent and the listeners are plentiful, leading to more memory usage than desired...but it seems like a decent first attempt.

2 Likes

(This is about the "garbage collection when processing events is good enough" approach.)

You can use Weak<T> and try to upgrade it every time you process events. If it's None, the T has been dropped (but the allocation remains). This process only requires a &Weak<T>. If you want to the allocation to be able to drop too, you have to get rid of the Weak<T> (e.g. *weak = Weak::new()), and will thus need more than just a &Weak (&mut or ownership).

Or you can use Arc::try_unwrap to try to see if you're the sole owner. If it's Ok, you now hold the T exclusively (and the allocation will go away when any Weaks are gone). This process requires at least temporarily relinquishing your sole clone of the Arc<T>.

Which is better depends on how you want to manage your collection I think.

  • Arc<T>
    • Remove from collection
    • try_unwrap on the owned Arc
      • Ok(_): No more consumers, get rid of it
      • Err(arc): Do event stuff, put Arc back into collection
  • Weak<T>
    • upgrade on the &Weak
      • Some(arc): Do event stuff, drop the Arc
      • None: No more consumers, optionally remove from container or replace with Weak::new
2 Likes

When does Drop get called for a weak ref-ed thing? Could I delete my shadow copy by doing it in an impl Drop?

-kb

When the strong count of the reference counted pointer reaches zero, i.e. when there are no Arc/Rc instances around any more, only Weak references. If you keep Weak references in your vector of shadow copies, your client would need access to that vector in its Drop::drop implementation to search for the corresponding weak reference and remove it from the vector. I think that would be possible, but then your vector would probably need to be reference counted with interior mutability as well, don't know how much synchronization overhead that would be in your case.

That seems problematic. The client struct would need to get an exclusive reference to your container. I wouldn't go down that path unless the other idea (get rid of them while iterating) has problems.

Sounds like I need to do some playing with toy code; no matter how I do it, looks like I need understand how to use std::Weak.

play.rust-lang.org here I come…

4 Likes

It could also make sense to run cleanup something like before every Nth insertion. For example if dispatch doesn't necessarily need exclusive access, but insertion does. :thinking: There are a few ways of tuning this kind of system for different needs.

The whole thing still looks very much like a solution in a search of problem.

People often argue about whether one needs four whys or maybe five, but in my experience the important things is to ask yourself about your decisions till you reach the task that's not under your control.

Just why do you need these events? Why they needs to be registered and then processed? What's the point of that array? Why do you want to free elements from it when client drops reference?

Lots of seemingly arbitrary decisions — and each one may affect the answer.

P.S. And if you couldn't answer these question and that's yet another factory factory factory, then the obvious and correct answer is: don't. Rust doesn't like such designs and they would bring you nothing but grief.

I think it will work. I think the fear I had of doing "memory management" was unfounded, because Rust.

  • Looks like I put a regular mutex in the opaque subscription struct and pass that back to the client.
  • I store a weak reference in a vec.
  • To use the weak reference I first need promote it; if that succeeds (Some), cool, if that fails (None) then I am well prevented from accessing the data. And, as my code has to account for the None case anyway, it is pretty reasonable for that to be where I delete the weak reference.

And no garbage collecting nonsense, either.

Thanks to all,

-kb

1 Like

The best design is going to depend on a lot of factors. As far as the technical parts go...

I don't know what you mean by "weak ref-ed thing", the allocation of the Arc/Weak or the T value in the Arc<T>/Weak<T>. There's one allocation for the counters and the stored value.

An Arc<T> keeps the T and the allocation alive until dropped.[1] So typically, when there are no more Arc<T>, the T drops.

A downgraded Weak<T> doesn't keep the T alive, but does keep the allocation alive until dropped. So typically, when there are no more Arc<T> or downgraded Weak<T>, the allocation drops. "Replace it with a Weak::new()" is one way to drop a downgraded Weak<T>.[2]

Weaks obtained from Weak::new() don't allocate and don't hold anything alive.


How much you need to care is a "best design" question, but if the Ts are small and/or slot reclamation/pruning is inherently high enough and/or your max collection size is small enough, you might not have to care about cleaning up Weaks at all. Heck, you might not even need them. It ultimately just depends.


  1. Also beware cycles. ↩︎

  2. So is "replace it with a freshly downgraded Weak<T>" (e.g. in the process of reusing an otherwise dead slot in your container -- maybe you didn't want to have to remove dead items all the time, and instead keep a queue of dead slot indices). ↩︎

Yes. And I assume you remove it with Vec::remove. Removing it when encountering None, during event processing, is all that was meant by garbage collection.

Yes, Vec::remove seems more orderly and deterministic than "garbage collection".

-kb

I was posing my question as clearly as I could, but not dragging in the larger why's, not turning into a larger design discussion.

You are right that the larger questions are important, usually.

But in my case my purpose is I am learning. I am writing a library for accessing a piece of Raspberry Pi hardware, but what I expect the library to be for is vague, so I building something too general purpose and too powerful.

The result is I am learning a lot.

-kb

1 Like

That's precisely what Rust fight against which, basically means that if you want something strange and vague then it's better to use some other language.

It's not impossible to write such things in Rust, mind you, but since Rust would fight you, tooth and nail, every step on the way… the whole excercise start becoming crazy very soon: you are taking very strict and rigid language which is lauded for it's strictness and rigidness and then try to eradicate it's most important advantages… the most overarching why? raises it's ugly head: if you plan to work against the language not with the laguage… maybe better to find another language? Not as strict and as rigid (but also, of course, much more error prone)?

Because, in the end, if you eradicate all these things that compiler uses to keep your program sane, obeying the rules… you would spend a lot of efforts making compiler clueless and then compiler would not be able to help you… lose-lose situation for everyone, if you'll ask me.