diff --git a/library/src/main/java/libxmtp-version.txt b/library/src/main/java/libxmtp-version.txt index 3de9cf43..a374f737 100644 --- a/library/src/main/java/libxmtp-version.txt +++ b/library/src/main/java/libxmtp-version.txt @@ -1,3 +1,3 @@ -Version: 33c06af6 +Version: 7d863bde Branch: main -Date: 2024-12-20 17:34:43 +0000 +Date: 2024-12-20 22:46:38 +0000 diff --git a/library/src/main/java/xmtpv3.kt b/library/src/main/java/xmtpv3.kt index 4d82231b..4b8a1e5d 100644 --- a/library/src/main/java/xmtpv3.kt +++ b/library/src/main/java/xmtpv3.kt @@ -1187,6 +1187,14 @@ internal interface UniffiLib : Library { `ptr`: Pointer, `error`: RustBuffer.ByValue, uniffi_out_err: UniffiRustCallStatus, ): Unit + fun uniffi_xmtpv3_fn_clone_fficonversationlistitem( + `ptr`: Pointer, uniffi_out_err: UniffiRustCallStatus, + ): Pointer + + fun uniffi_xmtpv3_fn_free_fficonversationlistitem( + `ptr`: Pointer, uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_xmtpv3_fn_clone_fficonversationmetadata( `ptr`: Pointer, uniffi_out_err: UniffiRustCallStatus, ): Pointer @@ -1231,6 +1239,10 @@ internal interface UniffiLib : Library { `ptr`: Pointer, `opts`: RustBuffer.ByValue, ): Long + fun uniffi_xmtpv3_fn_method_fficonversations_list_conversations( + `ptr`: Pointer, + ): Long + fun uniffi_xmtpv3_fn_method_fficonversations_list_dms( `ptr`: Pointer, `opts`: RustBuffer.ByValue, ): Long @@ -2112,6 +2124,9 @@ internal interface UniffiLib : Library { fun uniffi_xmtpv3_checksum_method_fficonversations_list( ): Short + fun uniffi_xmtpv3_checksum_method_fficonversations_list_conversations( + ): Short + fun uniffi_xmtpv3_checksum_method_fficonversations_list_dms( ): Short @@ -2528,6 +2543,9 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_xmtpv3_checksum_method_fficonversations_list() != 42790.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_xmtpv3_checksum_method_fficonversations_list_conversations() != 29098.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_xmtpv3_checksum_method_fficonversations_list_dms() != 41576.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -5073,6 +5091,225 @@ public object FfiConverterTypeFfiConversationCallback : // +public interface FfiConversationListItemInterface { + + companion object +} + +open class FfiConversationListItem : Disposable, AutoCloseable, FfiConversationListItemInterface { + + 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)) + } + + 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() + } + } + } + + @Synchronized + override fun close() { + this.destroy() + } + + internal inline fun 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() + } + } + } + + // 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.uniffi_xmtpv3_fn_free_fficonversationlistitem(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_xmtpv3_fn_clone_fficonversationlistitem(pointer!!, status) + } + } + + + companion object + +} + +/** + * @suppress + */ +public object FfiConverterTypeFfiConversationListItem : + FfiConverter { + + override fun lower(value: FfiConversationListItem): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): FfiConversationListItem { + return FfiConversationListItem(value) + } + + override fun read(buf: ByteBuffer): FfiConversationListItem { + // The Rust code always writes pointers as 8 bytes, and will + // fail to compile if they don't fit. + return lift(Pointer(buf.getLong())) + } + + override fun allocationSize(value: FfiConversationListItem) = 8UL + + override fun write(value: FfiConversationListItem, buf: ByteBuffer) { + // The Rust code always expects pointers written as 8 bytes, + // and will fail to compile if they don't fit. + buf.putLong(Pointer.nativeValue(lower(value))) + } +} + + +// This template implements a class for working with a Rust struct via a Pointer/Arc +// to the live Rust struct on the other side of the FFI. +// +// Each instance implements core operations for working with the Rust `Arc` 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` 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 +// + + public interface FfiConversationMetadataInterface { fun `conversationType`(): FfiConversationType @@ -5336,6 +5573,8 @@ public interface FfiConversationsInterface { suspend fun `list`(`opts`: FfiListConversationsOptions): List + suspend fun `listConversations`(): List + suspend fun `listDms`(`opts`: FfiListConversationsOptions): List suspend fun `listGroups`(`opts`: FfiListConversationsOptions): List @@ -5588,6 +5827,38 @@ open class FfiConversations : Disposable, AutoCloseable, FfiConversationsInterfa } + @Throws(GenericException::class) + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + override suspend fun `listConversations`(): List { + return uniffiRustCallAsync( + callWithPointer { thisPtr -> + UniffiLib.INSTANCE.uniffi_xmtpv3_fn_method_fficonversations_list_conversations( + thisPtr, + + ) + }, + { future, callback, continuation -> + UniffiLib.INSTANCE.ffi_xmtpv3_rust_future_poll_rust_buffer( + future, + callback, + continuation + ) + }, + { future, continuation -> + UniffiLib.INSTANCE.ffi_xmtpv3_rust_future_complete_rust_buffer( + future, + continuation + ) + }, + { future -> UniffiLib.INSTANCE.ffi_xmtpv3_rust_future_free_rust_buffer(future) }, + // lift function + { FfiConverterSequenceTypeFfiConversationListItem.lift(it) }, + // Error FFI converter + GenericException.ErrorHandler, + ) + } + + @Throws(GenericException::class) @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") override suspend fun `listDms`(`opts`: FfiListConversationsOptions): List { @@ -11773,6 +12044,34 @@ public object FfiConverterSequenceTypeFfiConversation : } +/** + * @suppress + */ +public object FfiConverterSequenceTypeFfiConversationListItem : + FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeFfiConversationListItem.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = + value.map { FfiConverterTypeFfiConversationListItem.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeFfiConversationListItem.write(it, buf) + } + } +} + + /** * @suppress */ diff --git a/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtpv3.so b/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtpv3.so index 20e93107..da10741b 100644 Binary files a/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtpv3.so and b/library/src/main/jniLibs/arm64-v8a/libuniffi_xmtpv3.so differ diff --git a/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtpv3.so b/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtpv3.so index 65f57727..69d6b87d 100644 Binary files a/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtpv3.so and b/library/src/main/jniLibs/armeabi-v7a/libuniffi_xmtpv3.so differ diff --git a/library/src/main/jniLibs/x86/libuniffi_xmtpv3.so b/library/src/main/jniLibs/x86/libuniffi_xmtpv3.so index 7f468c0c..a1221063 100644 Binary files a/library/src/main/jniLibs/x86/libuniffi_xmtpv3.so and b/library/src/main/jniLibs/x86/libuniffi_xmtpv3.so differ diff --git a/library/src/main/jniLibs/x86_64/libuniffi_xmtpv3.so b/library/src/main/jniLibs/x86_64/libuniffi_xmtpv3.so index d01aaa92..8391690c 100644 Binary files a/library/src/main/jniLibs/x86_64/libuniffi_xmtpv3.so and b/library/src/main/jniLibs/x86_64/libuniffi_xmtpv3.so differ