summaryrefslogtreecommitdiffstats
path: root/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt343
1 files changed, 250 insertions, 93 deletions
diff --git a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt
index 8ce27a5d04..62cac7a4d0 100644
--- a/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt
+++ b/third_party/rust/uniffi_bindgen/src/bindings/kotlin/templates/ObjectTemplate.kt
@@ -1,125 +1,282 @@
+// This template implements a class for working with a Rust struct via a Pointer/Arc<T>
+// to the live Rust struct on the other side of the FFI.
+//
+// Each instance implements core operations for working with the Rust `Arc<T>` and the
+// Kotlin Pointer to work with the live Rust struct on the other side of the FFI.
+//
+// There's some subtlety here, because we have to be careful not to operate on a Rust
+// struct after it has been dropped, and because we must expose a public API for freeing
+// theq Kotlin wrapper object in lieu of reliable finalizers. The core requirements are:
+//
+// * Each instance holds an opaque pointer to the underlying Rust struct.
+// Method calls need to read this pointer from the object's state and pass it in to
+// the Rust FFI.
+//
+// * When an instance is no longer needed, its pointer should be passed to a
+// special destructor function provided by the Rust FFI, which will drop the
+// underlying Rust struct.
+//
+// * Given an instance, calling code is expected to call the special
+// `destroy` method in order to free it after use, either by calling it explicitly
+// or by using a higher-level helper like the `use` method. Failing to do so risks
+// leaking the underlying Rust struct.
+//
+// * We can't assume that calling code will do the right thing, and must be prepared
+// to handle Kotlin method calls executing concurrently with or even after a call to
+// `destroy`, and to handle multiple (possibly concurrent!) calls to `destroy`.
+//
+// * We must never allow Rust code to operate on the underlying Rust struct after
+// the destructor has been called, and must never call the destructor more than once.
+// Doing so may trigger memory unsafety.
+//
+// * To mitigate many of the risks of leaking memory and use-after-free unsafety, a `Cleaner`
+// is implemented to call the destructor when the Kotlin object becomes unreachable.
+// This is done in a background thread. This is not a panacea, and client code should be aware that
+// 1. the thread may starve if some there are objects that have poorly performing
+// `drop` methods or do significant work in their `drop` methods.
+// 2. the thread is shared across the whole library. This can be tuned by using `android_cleaner = true`,
+// or `android = true` in the [`kotlin` section of the `uniffi.toml` file](https://mozilla.github.io/uniffi-rs/kotlin/configuration.html).
+//
+// If we try to implement this with mutual exclusion on access to the pointer, there is the
+// possibility of a race between a method call and a concurrent call to `destroy`:
+//
+// * Thread A starts a method call, reads the value of the pointer, but is interrupted
+// before it can pass the pointer over the FFI to Rust.
+// * Thread B calls `destroy` and frees the underlying Rust struct.
+// * Thread A resumes, passing the already-read pointer value to Rust and triggering
+// a use-after-free.
+//
+// One possible solution would be to use a `ReadWriteLock`, with each method call taking
+// a read lock (and thus allowed to run concurrently) and the special `destroy` method
+// taking a write lock (and thus blocking on live method calls). However, we aim not to
+// generate methods with any hidden blocking semantics, and a `destroy` method that might
+// block if called incorrectly seems to meet that bar.
+//
+// So, we achieve our goals by giving each instance an associated `AtomicLong` counter to track
+// the number of in-flight method calls, and an `AtomicBoolean` flag to indicate whether `destroy`
+// has been called. These are updated according to the following rules:
+//
+// * The initial value of the counter is 1, indicating a live object with no in-flight calls.
+// The initial value for the flag is false.
+//
+// * At the start of each method call, we atomically check the counter.
+// If it is 0 then the underlying Rust struct has already been destroyed and the call is aborted.
+// If it is nonzero them we atomically increment it by 1 and proceed with the method call.
+//
+// * At the end of each method call, we atomically decrement and check the counter.
+// If it has reached zero then we destroy the underlying Rust struct.
+//
+// * When `destroy` is called, we atomically flip the flag from false to true.
+// If the flag was already true we silently fail.
+// Otherwise we atomically decrement and check the counter.
+// If it has reached zero then we destroy the underlying Rust struct.
+//
+// Astute readers may observe that this all sounds very similar to the way that Rust's `Arc<T>` works,
+// and indeed it is, with the addition of a flag to guard against multiple calls to `destroy`.
+//
+// The overall effect is that the underlying Rust struct is destroyed only when `destroy` has been
+// called *and* all in-flight method calls have completed, avoiding violating any of the expectations
+// of the underlying Rust code.
+//
+// This makes a cleaner a better alternative to _not_ calling `destroy()` as
+// and when the object is finished with, but the abstraction is not perfect: if the Rust object's `drop`
+// method is slow, and/or there are many objects to cleanup, and it's on a low end Android device, then the cleaner
+// thread may be starved, and the app will leak memory.
+//
+// In this case, `destroy`ing manually may be a better solution.
+//
+// The cleaner can live side by side with the manual calling of `destroy`. In the order of responsiveness, uniffi objects
+// with Rust peers are reclaimed:
+//
+// 1. By calling the `destroy` method of the object, which calls `rustObject.free()`. If that doesn't happen:
+// 2. When the object becomes unreachable, AND the Cleaner thread gets to call `rustObject.free()`. If the thread is starved then:
+// 3. The memory is reclaimed when the process terminates.
+//
+// [1] https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope/24380219
+//
+
+{{ self.add_import("java.util.concurrent.atomic.AtomicBoolean") }}
+{%- if self.include_once_check("interface-support") %}
+ {%- include "ObjectCleanerHelper.kt" %}
+{%- endif %}
+
{%- let obj = ci|get_object_definition(name) %}
-{%- if self.include_once_check("ObjectRuntime.kt") %}{% include "ObjectRuntime.kt" %}{% endif %}
-{{- self.add_import("java.util.concurrent.atomic.AtomicLong") }}
-{{- self.add_import("java.util.concurrent.atomic.AtomicBoolean") }}
+{%- let (interface_name, impl_class_name) = obj|object_names(ci) %}
+{%- let methods = obj.methods() %}
+{%- let interface_docstring = obj.docstring() %}
+{%- let is_error = ci.is_name_used_as_error(name) %}
+{%- let ffi_converter_name = obj|ffi_converter_name %}
-public interface {{ type_name }}Interface {
- {% for meth in obj.methods() -%}
- {%- match meth.throws_type() -%}
- {%- when Some with (throwable) -%}
- @Throws({{ throwable|type_name(ci) }}::class)
- {%- when None -%}
- {%- endmatch %}
- {% if meth.is_async() -%}
- suspend fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %})
- {%- else -%}
- fun {{ meth.name()|fn_name }}({% call kt::arg_list_decl(meth) %})
- {%- endif %}
- {%- match meth.return_type() -%}
- {%- when Some with (return_type) %}: {{ return_type|type_name(ci) -}}
- {%- when None -%}
- {%- endmatch -%}
+{%- include "Interface.kt" %}
- {% endfor %}
- companion object
-}
+{%- call kt::docstring(obj, 0) %}
+{% if (is_error) %}
+open class {{ impl_class_name }} : Exception, Disposable, AutoCloseable, {{ interface_name }} {
+{% else -%}
+open class {{ impl_class_name }}: Disposable, AutoCloseable, {{ interface_name }} {
+{%- endif %}
-class {{ type_name }}(
- pointer: Pointer
-) : FFIObject(pointer), {{ type_name }}Interface {
+ constructor(pointer: Pointer) {
+ this.pointer = pointer
+ this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer))
+ }
+
+ /**
+ * This constructor can be used to instantiate a fake object. Only used for tests. Any
+ * attempt to actually use an object constructed this way will fail as there is no
+ * connected Rust object.
+ */
+ @Suppress("UNUSED_PARAMETER")
+ constructor(noPointer: NoPointer) {
+ this.pointer = null
+ this.cleanable = UniffiLib.CLEANER.register(this, UniffiCleanAction(pointer))
+ }
{%- match obj.primary_constructor() %}
- {%- when Some with (cons) %}
- constructor({% call kt::arg_list_decl(cons) -%}) :
+ {%- when Some(cons) %}
+ {%- if cons.is_async() %}
+ // Note no constructor generated for this object as it is async.
+ {%- else %}
+ {%- call kt::docstring(cons, 4) %}
+ constructor({% call kt::arg_list(cons, true) -%}) :
this({% call kt::to_ffi_call(cons) %})
+ {%- endif %}
{%- when None %}
{%- endmatch %}
- /**
- * Disconnect the object from the underlying Rust object.
- *
- * It can be called more than once, but once called, interacting with the object
- * causes an `IllegalStateException`.
- *
- * Clients **must** call this method once done with the object, or cause a memory leak.
- */
- override protected fun freeRustArcPtr() {
- rustCall() { status ->
- _UniFFILib.INSTANCE.{{ obj.ffi_object_free().name() }}(this.pointer, status)
+ protected val pointer: Pointer?
+ protected val cleanable: UniffiCleaner.Cleanable
+
+ private val wasDestroyed = AtomicBoolean(false)
+ private val callCounter = AtomicLong(1)
+
+ override fun destroy() {
+ // Only allow a single call to this method.
+ // TODO: maybe we should log a warning if called more than once?
+ if (this.wasDestroyed.compareAndSet(false, true)) {
+ // This decrement always matches the initial count of 1 given at creation time.
+ if (this.callCounter.decrementAndGet() == 0L) {
+ cleanable.clean()
+ }
}
}
- {% for meth in obj.methods() -%}
- {%- match meth.throws_type() -%}
- {%- when Some with (throwable) %}
- @Throws({{ throwable|type_name(ci) }}::class)
- {%- else -%}
- {%- endmatch -%}
- {%- if meth.is_async() %}
- @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
- override suspend fun {{ meth.name()|fn_name }}({%- call kt::arg_list_decl(meth) -%}){% match meth.return_type() %}{% when Some with (return_type) %} : {{ return_type|type_name(ci) }}{% when None %}{%- endmatch %} {
- return uniffiRustCallAsync(
- callWithPointer { thisPtr ->
- _UniFFILib.INSTANCE.{{ meth.ffi_func().name() }}(
- thisPtr,
- {% call kt::arg_list_lowered(meth) %}
- )
- },
- {{ meth|async_poll(ci) }},
- {{ meth|async_complete(ci) }},
- {{ meth|async_free(ci) }},
- // lift function
- {%- match meth.return_type() %}
- {%- when Some(return_type) %}
- { {{ return_type|lift_fn }}(it) },
- {%- when None %}
- { Unit },
- {% endmatch %}
- // Error FFI converter
- {%- match meth.throws_type() %}
- {%- when Some(e) %}
- {{ e|type_name(ci) }}.ErrorHandler,
- {%- when None %}
- NullCallStatusErrorHandler,
- {%- endmatch %}
- )
- }
- {%- else -%}
- {%- match meth.return_type() -%}
- {%- when Some with (return_type) -%}
- override fun {{ meth.name()|fn_name }}({% call kt::arg_list_protocol(meth) %}): {{ return_type|type_name(ci) }} =
- callWithPointer {
- {%- call kt::to_ffi_call_with_prefix("it", meth) %}
- }.let {
- {{ return_type|lift_fn }}(it)
+ @Synchronized
+ override fun close() {
+ this.destroy()
+ }
+
+ internal inline fun <R> callWithPointer(block: (ptr: Pointer) -> R): R {
+ // Check and increment the call counter, to keep the object alive.
+ // This needs a compare-and-set retry loop in case of concurrent updates.
+ do {
+ val c = this.callCounter.get()
+ if (c == 0L) {
+ throw IllegalStateException("${this.javaClass.simpleName} object has already been destroyed")
+ }
+ if (c == Long.MAX_VALUE) {
+ throw IllegalStateException("${this.javaClass.simpleName} call counter would overflow")
+ }
+ } while (! this.callCounter.compareAndSet(c, c + 1L))
+ // Now we can safely do the method call without the pointer being freed concurrently.
+ try {
+ return block(this.uniffiClonePointer())
+ } finally {
+ // This decrement always matches the increment we performed above.
+ if (this.callCounter.decrementAndGet() == 0L) {
+ cleanable.clean()
+ }
}
+ }
- {%- when None -%}
- override fun {{ meth.name()|fn_name }}({% call kt::arg_list_protocol(meth) %}) =
- callWithPointer {
- {%- call kt::to_ffi_call_with_prefix("it", meth) %}
+ // Use a static inner class instead of a closure so as not to accidentally
+ // capture `this` as part of the cleanable's action.
+ private class UniffiCleanAction(private val pointer: Pointer?) : Runnable {
+ override fun run() {
+ pointer?.let { ptr ->
+ uniffiRustCall { status ->
+ UniffiLib.INSTANCE.{{ obj.ffi_object_free().name() }}(ptr, status)
+ }
+ }
}
- {% endmatch %}
- {% endif %}
+ }
+
+ fun uniffiClonePointer(): Pointer {
+ return uniffiRustCall() { status ->
+ UniffiLib.INSTANCE.{{ obj.ffi_object_clone().name() }}(pointer!!, status)
+ }
+ }
+
+ {% for meth in obj.methods() -%}
+ {%- call kt::func_decl("override", meth, 4) %}
{% endfor %}
+ {%- for tm in obj.uniffi_traits() %}
+ {%- match tm %}
+ {% when UniffiTrait::Display { fmt } %}
+ override fun toString(): String {
+ return {{ fmt.return_type().unwrap()|lift_fn }}({% call kt::to_ffi_call(fmt) %})
+ }
+ {% when UniffiTrait::Eq { eq, ne } %}
+ {# only equals used #}
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is {{ impl_class_name}}) return false
+ return {{ eq.return_type().unwrap()|lift_fn }}({% call kt::to_ffi_call(eq) %})
+ }
+ {% when UniffiTrait::Hash { hash } %}
+ override fun hashCode(): Int {
+ return {{ hash.return_type().unwrap()|lift_fn }}({%- call kt::to_ffi_call(hash) %}).toInt()
+ }
+ {%- else %}
+ {%- endmatch %}
+ {%- endfor %}
+
+ {# XXX - "companion object" confusion? How to have alternate constructors *and* be an error? #}
{% if !obj.alternate_constructors().is_empty() -%}
companion object {
{% for cons in obj.alternate_constructors() -%}
- fun {{ cons.name()|fn_name }}({% call kt::arg_list_decl(cons) %}): {{ type_name }} =
- {{ type_name }}({% call kt::to_ffi_call(cons) %})
+ {% call kt::func_decl("", cons, 4) %}
{% endfor %}
}
+ {% else if is_error %}
+ companion object ErrorHandler : UniffiRustCallStatusErrorHandler<{{ impl_class_name }}> {
+ override fun lift(error_buf: RustBuffer.ByValue): {{ impl_class_name }} {
+ // Due to some mismatches in the ffi converter mechanisms, errors are a RustBuffer.
+ val bb = error_buf.asByteBuffer()
+ if (bb == null) {
+ throw InternalException("?")
+ }
+ return {{ ffi_converter_name }}.read(bb)
+ }
+ }
{% else %}
companion object
{% endif %}
}
-public object {{ obj|ffi_converter_name }}: FfiConverter<{{ type_name }}, Pointer> {
- override fun lower(value: {{ type_name }}): Pointer = value.callWithPointer { it }
+{%- if obj.has_callback_interface() %}
+{%- let vtable = obj.vtable().expect("trait interface should have a vtable") %}
+{%- let vtable_methods = obj.vtable_methods() %}
+{%- let ffi_init_callback = obj.ffi_init_callback() %}
+{% include "CallbackInterfaceImpl.kt" %}
+{%- endif %}
+
+public object {{ ffi_converter_name }}: FfiConverter<{{ type_name }}, Pointer> {
+ {%- if obj.has_callback_interface() %}
+ internal val handleMap = UniffiHandleMap<{{ type_name }}>()
+ {%- endif %}
+
+ override fun lower(value: {{ type_name }}): Pointer {
+ {%- if obj.has_callback_interface() %}
+ return Pointer(handleMap.insert(value))
+ {%- else %}
+ return value.uniffiClonePointer()
+ {%- endif %}
+ }
override fun lift(value: Pointer): {{ type_name }} {
- return {{ type_name }}(value)
+ return {{ impl_class_name }}(value)
}
override fun read(buf: ByteBuffer): {{ type_name }} {
@@ -128,7 +285,7 @@ public object {{ obj|ffi_converter_name }}: FfiConverter<{{ type_name }}, Pointe
return lift(Pointer(buf.getLong()))
}
- override fun allocationSize(value: {{ type_name }}) = 8
+ override fun allocationSize(value: {{ type_name }}) = 8UL
override fun write(value: {{ type_name }}, buf: ByteBuffer) {
// The Rust code always expects pointers written as 8 bytes,