Skip to content

Commit

Permalink
ADR-0008: Freeing objects and resources
Browse files Browse the repository at this point in the history
  • Loading branch information
bendk committed Oct 12, 2023
1 parent b2d4714 commit d17b6b7
Showing 1 changed file with 132 additions and 0 deletions.
132 changes: 132 additions & 0 deletions docs/adr/0008-freeing-objects-and-resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Freeing objects and resources

* Status: Propsed
* Deciders: ?
* Date: 2023-10-10

Prevous discussion: [PR 1394](https://github.com/mozilla/uniffi-rs/issues/1394), [Issue 8](https://github.com/mozilla/uniffi-rs/issues/8)

## Context and Problem Statement

Some objects hold resources that should be cleaned up promptly like database connections, file handles, network sockets, large memory allocations, etc.
This present a challenge for UniFFI, because these types of objects are handled differently by different languages:

* On Kotlin and other GC-based languages, the general advice is to explicitly close the resources and not rely on garbage collection to deal with them.
* On Rust and other RAII-based languages, the general advice is to close the resources in a destructor.

Currently, our Kotlin bindings try to address this problem for Objects by adding a `destroy()` method that manually drops the Rust value.
They implement the `AutoCloseable` interface which allows users to ensure this is called via the `use` extension function.
However, this is not sufficient for several reasons:

* It is specific to Kotlin, but many languages face similar issues.
Python might want to implement a context manager to free the resources.
C# and Go might also want similar solutions.
However, it's not clear how to generalize our current solution.
* All objects get a resource closing functionality, when only some need it.
* This system is tied to decision to *not* support freeing UniFFI objects via GC-based systems like `Cleaner` (#8).
However, in some cases GC-based cleanup is what users want (#1394).
* This system has prevented us from allowing Objects to be stored in Enums and Records.
It's especially unclear how this could work with an Enum, since changing the variant can cause the object to be dropped.

We should come up with a general approach for resource-holding objects and apply it consistently across all languages.

### Callback interfaces

UniFFI has historically only tried to solve this issue in one direction: Rust objects used by the foreign code.
The same issue also arises with callback interfaces: Foreign objects used by Rust code.
In fact, callback interfaces seem like a harder issue to solve.

The current status is to make releasing the `Box` on Rust cause the foreign reference to be released.
This requires Kotlin consumers to release their resources in their finalizer method, which is highly discouraged.
This is less discouraged on Ref-counted languages like Python, but even in that case it's possible that reference cycles delay the release of the Python object.
This will cause releasing the underlying resource to be delayed.

One solution would be to allow users to define a `close` method -- dropping the `Box` in Rust causes `close()` to be called in the foreign language.
This will often be the correct behavior, but sometimes not:

```python

with open_file() as file:
# Peform some operation on the file in Rust, via a UniFFI call
use_the_file_in_rust(file)
# Peform some operation on the file in native Python
use_the_file_in_python(file)
```

Because Rust will drop the `Box` after the `use_the_file_in_rust` call, the file will be closed in the `use_the_file_in_python` call.

One last solution is to allow users to define a `close` method and explicitly call it in Rust.
However, this calls into question the strategy of auto-magically closing resources.
Maybe users should define their `close` methods in the API directly and UniFFI should get out of their way.

## Decision Drivers

* If an object does not hold a resource, then we should be able to use the GC to free it.
* The solution should work for both Rust objects passed to the foreign language and foreign objects passed to Rust.

## Considered Options

### [Option 1] Stop trying to solve this at the library level and have users define their own APIs

* Don't automatically implement any methods to close a resource.
* Allow users to store Objects in Enums and Records.
* Offer some general advice in the documents, like "define a close() method" and ensure it's called.
But otherwise leave this problem for users to solve.
* Use the `Cleaner` API on Kotlin to automatically free UniFFI pointers for Kotlin objects.
Note that this combines with the previous bullet. `close()` releases the resource, the Cleaner
action frees the heap allocation.
* Initially, don't implement `Autoclosable` anymore. If users want this feature, we could
consider supporting `Closable` as a standard trait, like we do with `Debug`, `Display`, etc.
In this case, `Closable` wouldn't change the Rust code, instead it would implement
`Autoclosable` on Kotlin and maybe implement interfaces for other languages as well.

### [Option 2] Resource and Object as separate types

* Require UDL users to label their interfaces as either a `Resource` or `Object` and proc-macro
users to derive either `Resource` or `Object`.
* Each type would have slightly different handling. For example on Kotlin `Resource` would work
like an `Object` does today and `Object` would work like the `Object` from [1].
* For other languages, we could either introduce similar distinctions or make both types behave
the same.

### [Option 3] Make Object do both

* Kotlin objects keep their current behavior. They still have a `destroy` method and also
implement `AutoCloseable`.
* In addition, Kotlin objects also use the `Cleaner` API to ensure they are destroyed if the
user doesn't manually call it.

### Notes on Cleaner

For all options, UniFFI will start using the `Cleaner` API on Kotlin.
We should consider adding a Kotlin configuration option to generate a `destroy()` method to free the Rust pointer instead of relying on the Cleaner.
This option would be for users who need to support older Java versions where Cleaner is not available and users who don't trust that Cleaner is implemented correctly.

Many Java sources recommend logging a warning if the `Cleaner` action runs on a resource-holding object without `close()` being called.
However, this doesn't seem like a realistic option for UniFFI since we don't have hook into the user's logging library.

## Pros and Cons of the Options

### [Option 1] User-defined resource close method

* Good, because gives the user control over their object namespace. UniFFI doesn't define any methods for them.
* Bad, because it's not idiomatic to call `close()` on Rust or other RAII-based languages.
* Bad, because it requires Rust and other RAII-based language implementations to manually check if the resource object has been closed or not.

### [Option 2] Resource and Object as separate types

* Good, because users explicitly decide on their interface behavior.
* Good, because we can easily continue to implement special traits like `AutoCloseable`.
* Bad, because it introduces more complexity in the type system.
* Bad, because it's not clear how to handle callback interfaces

### [Option 3] Make Object do both

* Good, because it's the easiest to implement.
* Good, because we can easily continue to implement special traits like `AutoCloseable`.
* Bad, because it implicitly defines both a `close()` and `destroy()` method for all objects, preventing users from defining methods with those names that have different semantics.
* Bad, because it's not clear how to handle callback interfaces

## Decision Outcome

Chosen option:

0 comments on commit d17b6b7

Please sign in to comment.