Contexted drop in Rust

 In game development, it is often the case that, for performance reasons we allocate objects from a pool. When we want to clean up an allocated object, we need to release it back to the pool it was allocated from.

Of course C++ is an unsafe language. It is very easy to cause one of various errors with such a pattern:

  • Forget to free an allocated object (memory leak)
  • The pool is freed before the allocated objects (dangling pointers)
  • The pool is accessed unsafely from multiple threads (data races)
  • We free the object to another pool than the one it was allocated from

 Pool allocations have another quirk. They feel non-idiomatic. The idiomatic way of freeing an object in C++ is relying on its destructor. However, passing a pool as an argument to destructors in C++ is not possible as destructors in C++ take no arguments. The only workaround seems to be storing a pointer or reference to the pool in each allocated object. While this will allow easy cleanup of allocated objects through their destructor, it needlessly increases the size of each allocated object, plus it creates opportunities for data races or dangling references, just like outlined above.

It feels like too much cost for little convenience.

Now, let's say we want to implement a similar scheme in Rust. How would that work?

Well, things can be slightly better in that case. We can embed the lifetime of the pool to each allocated object through a PhantomData member. This will help prevent freeing the pool before any allocated objects. It won't however prevent forgetting to free an allocated object, as Rust suffers from the same issue that C++ suffers from: The drop trait does not take any other inputs apart from self. We could panic on drop, but we need to store extra metadata about an object's liveness, and this simply moves the error from compile time to run time. Not so nice...

What's even worse, storing a reference to an allocator is arguably worse in Rust: Releasing an object is, arguably a mutable operation so we can't store that reference to more than one object at a time! We can maybe work around this with interior mutability, but it's awkward.

The situation feels hopeless.

This problem feels very similar to the Context problem. We want, somehow to automate passing an extra variable to the drop function. In this case, such a variable will be a mutable reference to our pool.

Doing it this way would help to automatically clean up pool-allocated objects ergonomically and idiomatically. The pool is never stored in our allocated objects, so there is no shared mutability anymore: The pool is borrowed mutably only at the point where the allocated object is dropped.

I feel that the context pattern is very powerful, but the way it is formulated in that blog post, while very useful, may not fit this use case very well. Users will need to bind a pool to a context before any object allocated from it can be dropped.

The Rust compiler could, by the same mechanism it determines lifetimes, make sure to automatically pass the pool object to any allocated objects in their drop function. In general, passing the pool object to allocated objects on any of its methods is a potentially very powerful pattern.

In computer graphics, we often need to use a graphics device reference while calling any functions on resources allocated by the device itself, such as textures or buffers. Having the compiler pass that reference for us automatically would be a huge convenience. Of course contexts as described in the blog post would be a solution as devices are singletons, and wrapping our whole application in the device context means we could get the device passed for free on any function that needs it. Again though, one could envision cases where it might be possible to pass the wrong device, and in that case, being able to pass the exact device an object was allocated from would be more beneficial.


Comments

Popular posts from this blog

Improving the performance of UI list updates

Logarithmic scales for UI