UniFFI object destruction on Kotlin

UniFFI supports interface objects, which are implemented by Boxing a Rust object and sending the raw pointer to the foreign code. Once the objects are no longer in use, the foreign code needs to destroy the object and free the underlying resources.

This is slightly tricky on Kotlin. The prevailing Java wisdom is to use explicit destructors and avoid using finalizers for destruction, which means we can't simply rely on the garbage collector to free the pointer. The wisdom seems simple to follow, but in practice it can be difficult to know how to apply it to specific situations. This document examines provides guidelines for handling UniFFI objects.

You can create objects in a function if you also destroy them there

The simplest way to get destruction right is to create an object and destroy it in the same function. The use function makes this really easy:

SomeUniFFIObject()
  .use { obj ->
      obj.doSomething()
      obj.doSomethingElse()
  }

You can create and store objects in singletons

If we are okay with UniFFI objects living for the entire application lifetime, then they can be stored in singletons. This is how we handle our database connections, for example SyncableLoginsStorage and PlacesReaderConnection.

You can create and store objects in an class, then destroy them in a corresponding lifecycle method

UniFFI objects can stored in classes like the Android Fragment class that have a defined lifecycle, with methods called at different stages. Classes can construct UniFFI objects in one of the lifecycle methods, then destroy it in the corresponding one. For example, creating an object in Fragment.onCreate and destroying it in Fragment.onDestroy().

You can share objects

Several classes can hold references to an object, as long as (exactly) one class is responsible for managing it and destroying it when it's not used. A good example is the GeckoLoginStorageDelegate. The LoginStorage is initialized and managed by another object, and GeckoLoginStorageDelegate is passed a (lazy) reference to it.

Care should be taken to ensure that once the managing class destroys the object, no other class attempts to use it. If they do, then the generate code will raise an IllegalStateException. This clearly should be avoided, although it won't result in memory corruption.

Destruction may not always happen

Destructors may not run when a process is killed, which can easily happen on Android. This is especially true of lifecycle methods. This is normally fine, since the OS will close resources like file handles and network connections on its own. However, be aware that custom code in the destructor may not run.