From 1c6983293b13807f278b217706e39dbccd01d5cf Mon Sep 17 00:00:00 2001 From: Alvinosh Date: Tue, 2 Jul 2024 12:35:10 +0200 Subject: [PATCH 1/3] Initial version --- .vscode/settings.json | 22 +- Cargo.lock | 117 +++ Cargo.toml | 6 +- README.md | 31 +- bindings/uniffi/drop_core/drop_core.kt | 1053 ++++++++++++++++++++++++ core/.cargo/config.toml | 11 - core/src/lib.rs | 42 +- core/src/metadata.rs | 4 +- package.json | 1 + src-tauri/src/main.rs | 1 + uniffi-bingen/Cargo.toml | 8 + uniffi-bingen/src/main.rs | 3 + 12 files changed, 1239 insertions(+), 60 deletions(-) create mode 100644 bindings/uniffi/drop_core/drop_core.kt delete mode 100644 core/.cargo/config.toml create mode 100644 uniffi-bingen/Cargo.toml create mode 100644 uniffi-bingen/src/main.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index c4ffd1f..cc2c2b3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,13 @@ { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" - }, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSaveMode": "file", - "editor.tabSize": 4, - "editor.insertSpaces": false, - -} \ No newline at end of file + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSaveMode": "file", + "editor.tabSize": 4, + "editor.insertSpaces": false, + "[toml]": { + "editor.defaultFormatter": "tamasfe.even-better-toml" + } +} diff --git a/Cargo.lock b/Cargo.lock index 698092e..93107f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -704,6 +753,46 @@ dependencies = [ "zeroize", ] +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + [[package]] name = "cobs" version = "0.2.3" @@ -746,6 +835,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + [[package]] name = "combine" version = "4.6.7" @@ -3099,6 +3194,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itoa" version = "0.4.8" @@ -6744,11 +6845,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31bff6daf87277a9014bcdefbc2842b0553392919d1096843c5aad899ca4588" dependencies = [ "anyhow", + "camino", + "clap", + "uniffi_bindgen", "uniffi_build", "uniffi_core", "uniffi_macros", ] +[[package]] +name = "uniffi-bingen" +version = "0.1.0" +dependencies = [ + "uniffi", +] + [[package]] name = "uniffi_bindgen" version = "0.28.0" @@ -6899,6 +7010,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.8.0" diff --git a/Cargo.toml b/Cargo.toml index ec110ef..371df79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,6 @@ [workspace] resolver = "2" -members = [ - "core", - "src-tauri" -] +members = ["core", "src-tauri", "uniffi-bingen"] [workspace.dependencies] iroh = "0.17.0" @@ -20,4 +17,3 @@ rand = "0.8.5" tokio = { version = "1.37.0", features = ["full"] } tokio-util = "0.7.11" bytes = "1.6.0" - diff --git a/README.md b/README.md index 4b4977c..a4d16c6 100644 --- a/README.md +++ b/README.md @@ -1 +1,30 @@ -# ARK Drop \ No newline at end of file +## Dependencies + +Cross compilation building is done easiest via cross library. + +- [Cross](https://github.com/cross-rs/cross) + +Alternatively you can setup the NDK and build manually + +## Build + +Make sure you have added the nessecary targets to build for android + +```sh +rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android +``` + +Build the cdylib for all the targets + +```sh +cross build -p drop_core --target aarch64-linux-android +cross build -p drop_core --target armv7-linux-androideab +cross build -p drop_core --target i686-linux-android +cross build -p drop_core --target x86_64-linux-android +``` + +Generate the bindings using uniffi for kotlin + +```sh +cargo run -p uniffi-bingen generate --library target/x86_64-linux-android/debug/libark_drop_lib.so --language=kotlin --out-dir ./bindings +``` diff --git a/bindings/uniffi/drop_core/drop_core.kt b/bindings/uniffi/drop_core/drop_core.kt new file mode 100644 index 0000000..fa6cd0a --- /dev/null +++ b/bindings/uniffi/drop_core/drop_core.kt @@ -0,0 +1,1053 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! + +@file:Suppress("NAME_SHADOWING") + +package uniffi.drop_core + +// Common helper code. +// +// Ideally this would live in a separate .kt file where it can be unittested etc +// in isolation, and perhaps even published as a re-useable package. +// +// However, it's important that the details of how this helper code works (e.g. the +// way that different builtin types are passed across the FFI) exactly match what's +// expected by the Rust code on the other side of the interface. In practice right +// now that means coming from the exact some version of `uniffi` that was used to +// compile the Rust component. The easiest way to ensure this is to bundle the Kotlin +// helpers directly inline like we're doing here. + +import com.sun.jna.Library +import com.sun.jna.IntegerType +import com.sun.jna.Native +import com.sun.jna.Pointer +import com.sun.jna.Structure +import com.sun.jna.Callback +import com.sun.jna.ptr.* +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.CharBuffer +import java.nio.charset.CodingErrorAction +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.ConcurrentHashMap + +// This is a helper for safely working with byte buffers returned from the Rust code. +// A rust-owned buffer is represented by its capacity, its current length, and a +// pointer to the underlying data. + +@Structure.FieldOrder("capacity", "len", "data") +open class RustBuffer : Structure() { + // Note: `capacity` and `len` are actually `ULong` values, but JVM only supports signed values. + // When dealing with these fields, make sure to call `toULong()`. + @JvmField var capacity: Long = 0 + @JvmField var len: Long = 0 + @JvmField var data: Pointer? = null + + class ByValue: RustBuffer(), Structure.ByValue + class ByReference: RustBuffer(), Structure.ByReference + + internal fun setValue(other: RustBuffer) { + capacity = other.capacity + len = other.len + data = other.data + } + + companion object { + internal fun alloc(size: ULong = 0UL) = uniffiRustCall() { status -> + // Note: need to convert the size to a `Long` value to make this work with JVM. + UniffiLib.INSTANCE.ffi_drop_core_rustbuffer_alloc(size.toLong(), status) + }.also { + if(it.data == null) { + throw RuntimeException("RustBuffer.alloc() returned null data pointer (size=${size})") + } + } + + internal fun create(capacity: ULong, len: ULong, data: Pointer?): RustBuffer.ByValue { + var buf = RustBuffer.ByValue() + buf.capacity = capacity.toLong() + buf.len = len.toLong() + buf.data = data + return buf + } + + internal fun free(buf: RustBuffer.ByValue) = uniffiRustCall() { status -> + UniffiLib.INSTANCE.ffi_drop_core_rustbuffer_free(buf, status) + } + } + + @Suppress("TooGenericExceptionThrown") + fun asByteBuffer() = + this.data?.getByteBuffer(0, this.len.toLong())?.also { + it.order(ByteOrder.BIG_ENDIAN) + } +} + +/** + * The equivalent of the `*mut RustBuffer` type. + * Required for callbacks taking in an out pointer. + * + * Size is the sum of all values in the struct. + */ +class RustBufferByReference : ByReference(16) { + /** + * Set the pointed-to `RustBuffer` to the given value. + */ + fun setValue(value: RustBuffer.ByValue) { + // NOTE: The offsets are as they are in the C-like struct. + val pointer = getPointer() + pointer.setLong(0, value.capacity) + pointer.setLong(8, value.len) + pointer.setPointer(16, value.data) + } + + /** + * Get a `RustBuffer.ByValue` from this reference. + */ + fun getValue(): RustBuffer.ByValue { + val pointer = getPointer() + val value = RustBuffer.ByValue() + value.writeField("capacity", pointer.getLong(0)) + value.writeField("len", pointer.getLong(8)) + value.writeField("data", pointer.getLong(16)) + + return value + } +} + +// This is a helper for safely passing byte references into the rust code. +// It's not actually used at the moment, because there aren't many things that you +// can take a direct pointer to in the JVM, and if we're going to copy something +// then we might as well copy it into a `RustBuffer`. But it's here for API +// completeness. + +@Structure.FieldOrder("len", "data") +open class ForeignBytes : Structure() { + @JvmField var len: Int = 0 + @JvmField var data: Pointer? = null + + class ByValue : ForeignBytes(), Structure.ByValue +} +// The FfiConverter interface handles converter types to and from the FFI +// +// All implementing objects should be public to support external types. When a +// type is external we need to import it's FfiConverter. +public interface FfiConverter { + // Convert an FFI type to a Kotlin type + fun lift(value: FfiType): KotlinType + + // Convert an Kotlin type to an FFI type + fun lower(value: KotlinType): FfiType + + // Read a Kotlin type from a `ByteBuffer` + fun read(buf: ByteBuffer): KotlinType + + // Calculate bytes to allocate when creating a `RustBuffer` + // + // This must return at least as many bytes as the write() function will + // write. It can return more bytes than needed, for example when writing + // Strings we can't know the exact bytes needed until we the UTF-8 + // encoding, so we pessimistically allocate the largest size possible (3 + // bytes per codepoint). Allocating extra bytes is not really a big deal + // because the `RustBuffer` is short-lived. + fun allocationSize(value: KotlinType): ULong + + // Write a Kotlin type to a `ByteBuffer` + fun write(value: KotlinType, buf: ByteBuffer) + + // Lower a value into a `RustBuffer` + // + // This method lowers a value into a `RustBuffer` rather than the normal + // FfiType. It's used by the callback interface code. Callback interface + // returns are always serialized into a `RustBuffer` regardless of their + // normal FFI type. + fun lowerIntoRustBuffer(value: KotlinType): RustBuffer.ByValue { + val rbuf = RustBuffer.alloc(allocationSize(value)) + try { + val bbuf = rbuf.data!!.getByteBuffer(0, rbuf.capacity).also { + it.order(ByteOrder.BIG_ENDIAN) + } + write(value, bbuf) + rbuf.writeField("len", bbuf.position().toLong()) + return rbuf + } catch (e: Throwable) { + RustBuffer.free(rbuf) + throw e + } + } + + // Lift a value from a `RustBuffer`. + // + // This here mostly because of the symmetry with `lowerIntoRustBuffer()`. + // It's currently only used by the `FfiConverterRustBuffer` class below. + fun liftFromRustBuffer(rbuf: RustBuffer.ByValue): KotlinType { + val byteBuf = rbuf.asByteBuffer()!! + try { + val item = read(byteBuf) + if (byteBuf.hasRemaining()) { + throw RuntimeException("junk remaining in buffer after lifting, something is very wrong!!") + } + return item + } finally { + RustBuffer.free(rbuf) + } + } +} + +// FfiConverter that uses `RustBuffer` as the FfiType +public interface FfiConverterRustBuffer: FfiConverter { + override fun lift(value: RustBuffer.ByValue) = liftFromRustBuffer(value) + override fun lower(value: KotlinType) = lowerIntoRustBuffer(value) +} +// A handful of classes and functions to support the generated data structures. +// This would be a good candidate for isolating in its own ffi-support lib. + +internal const val UNIFFI_CALL_SUCCESS = 0.toByte() +internal const val UNIFFI_CALL_ERROR = 1.toByte() +internal const val UNIFFI_CALL_UNEXPECTED_ERROR = 2.toByte() + +@Structure.FieldOrder("code", "error_buf") +internal open class UniffiRustCallStatus : Structure() { + @JvmField var code: Byte = 0 + @JvmField var error_buf: RustBuffer.ByValue = RustBuffer.ByValue() + + class ByValue: UniffiRustCallStatus(), Structure.ByValue + + fun isSuccess(): Boolean { + return code == UNIFFI_CALL_SUCCESS + } + + fun isError(): Boolean { + return code == UNIFFI_CALL_ERROR + } + + fun isPanic(): Boolean { + return code == UNIFFI_CALL_UNEXPECTED_ERROR + } + + companion object { + fun create(code: Byte, errorBuf: RustBuffer.ByValue): UniffiRustCallStatus.ByValue { + val callStatus = UniffiRustCallStatus.ByValue() + callStatus.code = code + callStatus.error_buf = errorBuf + return callStatus + } + } +} + +class InternalException(message: String) : kotlin.Exception(message) + +// Each top-level error class has a companion object that can lift the error from the call status's rust buffer +interface UniffiRustCallStatusErrorHandler { + fun lift(error_buf: RustBuffer.ByValue): E; +} + +// Helpers for calling Rust +// In practice we usually need to be synchronized to call this safely, so it doesn't +// synchronize itself + +// Call a rust function that returns a Result<>. Pass in the Error class companion that corresponds to the Err +private inline fun uniffiRustCallWithError(errorHandler: UniffiRustCallStatusErrorHandler, callback: (UniffiRustCallStatus) -> U): U { + var status = UniffiRustCallStatus() + val return_value = callback(status) + uniffiCheckCallStatus(errorHandler, status) + return return_value +} + +// Check UniffiRustCallStatus and throw an error if the call wasn't successful +private fun uniffiCheckCallStatus(errorHandler: UniffiRustCallStatusErrorHandler, status: UniffiRustCallStatus) { + if (status.isSuccess()) { + return + } else if (status.isError()) { + throw errorHandler.lift(status.error_buf) + } else if (status.isPanic()) { + // when the rust code sees a panic, it tries to construct a rustbuffer + // with the message. but if that code panics, then it just sends back + // an empty buffer. + if (status.error_buf.len > 0) { + throw InternalException(FfiConverterString.lift(status.error_buf)) + } else { + throw InternalException("Rust panic") + } + } else { + throw InternalException("Unknown rust call status: $status.code") + } +} + +// UniffiRustCallStatusErrorHandler implementation for times when we don't expect a CALL_ERROR +object UniffiNullRustCallStatusErrorHandler: UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): InternalException { + RustBuffer.free(error_buf) + return InternalException("Unexpected CALL_ERROR") + } +} + +// Call a rust function that returns a plain value +private inline fun uniffiRustCall(callback: (UniffiRustCallStatus) -> U): U { + return uniffiRustCallWithError(UniffiNullRustCallStatusErrorHandler, callback) +} + +internal inline fun uniffiTraitInterfaceCall( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, +) { + try { + writeReturn(makeCall()) + } catch(e: kotlin.Exception) { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(e.toString()) + } +} + +internal inline fun uniffiTraitInterfaceCallWithError( + callStatus: UniffiRustCallStatus, + makeCall: () -> T, + writeReturn: (T) -> Unit, + lowerError: (E) -> RustBuffer.ByValue +) { + try { + writeReturn(makeCall()) + } catch(e: kotlin.Exception) { + if (e is E) { + callStatus.code = UNIFFI_CALL_ERROR + callStatus.error_buf = lowerError(e) + } else { + callStatus.code = UNIFFI_CALL_UNEXPECTED_ERROR + callStatus.error_buf = FfiConverterString.lower(e.toString()) + } + } +} +// Map handles to objects +// +// This is used pass an opaque 64-bit handle representing a foreign object to the Rust code. +internal class UniffiHandleMap { + private val map = ConcurrentHashMap() + private val counter = java.util.concurrent.atomic.AtomicLong(0) + + val size: Int + get() = map.size + + // Insert a new object into the handle map and get a handle for it + fun insert(obj: T): Long { + val handle = counter.getAndAdd(1) + map.put(handle, obj) + return handle + } + + // Get an object from the handle map + fun get(handle: Long): T { + return map.get(handle) ?: throw InternalException("UniffiHandleMap.get: Invalid handle") + } + + // Remove an entry from the handlemap and get the Kotlin object back + fun remove(handle: Long): T { + return map.remove(handle) ?: throw InternalException("UniffiHandleMap: Invalid handle") + } +} + +// Contains loading, initialization code, +// and the FFI Function declarations in a com.sun.jna.Library. +@Synchronized +private fun findLibraryName(componentName: String): String { + val libOverride = System.getProperty("uniffi.component.$componentName.libraryOverride") + if (libOverride != null) { + return libOverride + } + return "drop_core" +} + +private inline fun loadIndirect( + componentName: String +): Lib { + return Native.load(findLibraryName(componentName), Lib::class.java) +} + +// Define FFI callback types +internal interface UniffiRustFutureContinuationCallback : com.sun.jna.Callback { + fun callback(`data`: Long,`pollResult`: Byte,) +} +internal interface UniffiForeignFutureFree : com.sun.jna.Callback { + fun callback(`handle`: Long,) +} +internal interface UniffiCallbackInterfaceFree : com.sun.jna.Callback { + fun callback(`handle`: Long,) +} +@Structure.FieldOrder("handle", "free") +internal open class UniffiForeignFuture( + @JvmField internal var `handle`: Long = 0.toLong(), + @JvmField internal var `free`: UniffiForeignFutureFree? = null, +) : Structure() { + class UniffiByValue( + `handle`: Long = 0.toLong(), + `free`: UniffiForeignFutureFree? = null, + ): UniffiForeignFuture(`handle`,`free`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFuture) { + `handle` = other.`handle` + `free` = other.`free` + } + +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU8(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU8 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU8.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI8( + @JvmField internal var `returnValue`: Byte = 0.toByte(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Byte = 0.toByte(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI8(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI8) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI8 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI8.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU16(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU16 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU16.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI16( + @JvmField internal var `returnValue`: Short = 0.toShort(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Short = 0.toShort(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI16(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI16) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI16 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI16.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI32( + @JvmField internal var `returnValue`: Int = 0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Int = 0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructU64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructU64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructU64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteU64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructU64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructI64( + @JvmField internal var `returnValue`: Long = 0.toLong(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Long = 0.toLong(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructI64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructI64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteI64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructI64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructF32( + @JvmField internal var `returnValue`: Float = 0.0f, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Float = 0.0f, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructF32(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructF32) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteF32 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructF32.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructF64( + @JvmField internal var `returnValue`: Double = 0.0, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Double = 0.0, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructF64(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructF64) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteF64 : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructF64.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructPointer( + @JvmField internal var `returnValue`: Pointer = Pointer.NULL, + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: Pointer = Pointer.NULL, + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructPointer(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructPointer) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompletePointer : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructPointer.UniffiByValue,) +} +@Structure.FieldOrder("returnValue", "callStatus") +internal open class UniffiForeignFutureStructRustBuffer( + @JvmField internal var `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `returnValue`: RustBuffer.ByValue = RustBuffer.ByValue(), + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructRustBuffer(`returnValue`,`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructRustBuffer) { + `returnValue` = other.`returnValue` + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteRustBuffer : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructRustBuffer.UniffiByValue,) +} +@Structure.FieldOrder("callStatus") +internal open class UniffiForeignFutureStructVoid( + @JvmField internal var `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), +) : Structure() { + class UniffiByValue( + `callStatus`: UniffiRustCallStatus.ByValue = UniffiRustCallStatus.ByValue(), + ): UniffiForeignFutureStructVoid(`callStatus`,), Structure.ByValue + + internal fun uniffiSetValue(other: UniffiForeignFutureStructVoid) { + `callStatus` = other.`callStatus` + } + +} +internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback { + fun callback(`callbackData`: Long,`result`: UniffiForeignFutureStructVoid.UniffiByValue,) +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +// A JNA Library to expose the extern-C FFI definitions. +// This is an implementation detail which will be called internally by the public API. + +internal interface UniffiLib : Library { + companion object { + internal val INSTANCE: UniffiLib by lazy { + loadIndirect(componentName = "drop_core") + .also { lib: UniffiLib -> + uniffiCheckContractApiVersion(lib) + uniffiCheckApiChecksums(lib) + } + } + + } + + fun ffi_drop_core_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_drop_core_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_drop_core_rustbuffer_free(`buf`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun ffi_drop_core_rustbuffer_reserve(`buf`: RustBuffer.ByValue,`additional`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_drop_core_rust_future_poll_u8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_u8(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_u8(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_u8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun ffi_drop_core_rust_future_poll_i8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_i8(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_i8(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_i8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun ffi_drop_core_rust_future_poll_u16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_u16(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_u16(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_u16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Short + fun ffi_drop_core_rust_future_poll_i16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_i16(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_i16(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_i16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Short + fun ffi_drop_core_rust_future_poll_u32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_u32(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_u32(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_u32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun ffi_drop_core_rust_future_poll_i32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_i32(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_i32(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_i32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun ffi_drop_core_rust_future_poll_u64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_u64(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_u64(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_u64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Long + fun ffi_drop_core_rust_future_poll_i64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_i64(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_i64(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_i64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Long + fun ffi_drop_core_rust_future_poll_f32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_f32(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_f32(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_f32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Float + fun ffi_drop_core_rust_future_poll_f64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_f64(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_f64(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_f64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Double + fun ffi_drop_core_rust_future_poll_pointer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_pointer(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_pointer(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_pointer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun ffi_drop_core_rust_future_poll_rust_buffer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_rust_buffer(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_rust_buffer(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_rust_buffer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_drop_core_rust_future_poll_void(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_drop_core_rust_future_cancel_void(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_free_void(`handle`: Long, + ): Unit + fun ffi_drop_core_rust_future_complete_void(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun ffi_drop_core_uniffi_contract_version( + ): Int + +} + +private fun uniffiCheckContractApiVersion(lib: UniffiLib) { + // Get the bindings contract version from our ComponentInterface + val bindings_contract_version = 26 + // Get the scaffolding contract version by calling the into the dylib + val scaffolding_contract_version = lib.ffi_drop_core_uniffi_contract_version() + if (bindings_contract_version != scaffolding_contract_version) { + throw RuntimeException("UniFFI contract version mismatch: try cleaning and rebuilding your project") + } +} + +@Suppress("UNUSED_PARAMETER") +private fun uniffiCheckApiChecksums(lib: UniffiLib) { +} + +// Async support + +// Public interface members begin here. + + +// Interface implemented by anything that can contain an object reference. +// +// Such types expose a `destroy()` method that must be called to cleanly +// dispose of the contained objects. Failure to call this method may result +// in memory leaks. +// +// The easiest way to ensure this method is called is to use the `.use` +// helper method to execute a block and destroy the object at the end. +interface Disposable { + fun destroy() + companion object { + fun destroy(vararg args: Any?) { + args.filterIsInstance() + .forEach(Disposable::destroy) + } + } +} + +inline fun T.use(block: (T) -> R) = + try { + block(this) + } finally { + try { + // N.B. our implementation is on the nullable type `Disposable?`. + this?.destroy() + } catch (e: Throwable) { + // swallow + } + } + +/** Used to instantiate an interface without an actual pointer, for fakes in tests, mostly. */ +object NoPointer + +public object FfiConverterULong: FfiConverter { + override fun lift(value: Long): ULong { + return value.toULong() + } + + override fun read(buf: ByteBuffer): ULong { + return lift(buf.getLong()) + } + + override fun lower(value: ULong): Long { + return value.toLong() + } + + override fun allocationSize(value: ULong) = 8UL + + override fun write(value: ULong, buf: ByteBuffer) { + buf.putLong(value.toLong()) + } +} + +public object FfiConverterString: FfiConverter { + // Note: we don't inherit from FfiConverterRustBuffer, because we use a + // special encoding when lowering/lifting. We can use `RustBuffer.len` to + // store our length and avoid writing it out to the buffer. + override fun lift(value: RustBuffer.ByValue): String { + try { + val byteArr = ByteArray(value.len.toInt()) + value.asByteBuffer()!!.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } finally { + RustBuffer.free(value) + } + } + + override fun read(buf: ByteBuffer): String { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr.toString(Charsets.UTF_8) + } + + fun toUtf8(value: String): ByteBuffer { + // Make sure we don't have invalid UTF-16, check for lone surrogates. + return Charsets.UTF_8.newEncoder().run { + onMalformedInput(CodingErrorAction.REPORT) + encode(CharBuffer.wrap(value)) + } + } + + override fun lower(value: String): RustBuffer.ByValue { + val byteBuf = toUtf8(value) + // Ideally we'd pass these bytes to `ffi_bytebuffer_from_bytes`, but doing so would require us + // to copy them into a JNA `Memory`. So we might as well directly copy them into a `RustBuffer`. + val rbuf = RustBuffer.alloc(byteBuf.limit().toULong()) + rbuf.asByteBuffer()!!.put(byteBuf) + return rbuf + } + + // We aren't sure exactly how many bytes our string will be once it's UTF-8 + // encoded. Allocate 3 bytes per UTF-16 code unit which will always be + // enough. + override fun allocationSize(value: String): ULong { + val sizeForLength = 4UL + val sizeForString = value.length.toULong() * 3UL + return sizeForLength + sizeForString + } + + override fun write(value: String, buf: ByteBuffer) { + val byteBuf = toUtf8(value) + buf.putInt(byteBuf.limit()) + buf.put(byteBuf) + } +} + +public object FfiConverterByteArray: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ByteArray { + val len = buf.getInt() + val byteArr = ByteArray(len) + buf.get(byteArr) + return byteArr + } + override fun allocationSize(value: ByteArray): ULong { + return 4UL + value.size.toULong() + } + override fun write(value: ByteArray, buf: ByteBuffer) { + buf.putInt(value.size) + buf.put(value) + } +} + + + +data class CollectionMetadata ( + var `header`: kotlin.ByteArray, + var `names`: List +) { + + companion object +} + +public object FfiConverterTypeCollectionMetadata: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): CollectionMetadata { + return CollectionMetadata( + FfiConverterByteArray.read(buf), + FfiConverterSequenceString.read(buf), + ) + } + + override fun allocationSize(value: CollectionMetadata) = ( + FfiConverterByteArray.allocationSize(value.`header`) + + FfiConverterSequenceString.allocationSize(value.`names`) + ) + + override fun write(value: CollectionMetadata, buf: ByteBuffer) { + FfiConverterByteArray.write(value.`header`, buf) + FfiConverterSequenceString.write(value.`names`, buf) + } +} + + + +data class FileTransfer ( + var `name`: kotlin.String, + var `transfered`: kotlin.ULong, + var `total`: kotlin.ULong +) { + + companion object +} + +public object FfiConverterTypeFileTransfer: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): FileTransfer { + return FileTransfer( + FfiConverterString.read(buf), + FfiConverterULong.read(buf), + FfiConverterULong.read(buf), + ) + } + + override fun allocationSize(value: FileTransfer) = ( + FfiConverterString.allocationSize(value.`name`) + + FfiConverterULong.allocationSize(value.`transfered`) + + FfiConverterULong.allocationSize(value.`total`) + ) + + override fun write(value: FileTransfer, buf: ByteBuffer) { + FfiConverterString.write(value.`name`, buf) + FfiConverterULong.write(value.`transfered`, buf) + FfiConverterULong.write(value.`total`, buf) + } +} + + + + +public object FfiConverterSequenceString: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterString.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterString.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterString.write(it, buf) + } + } +} + diff --git a/core/.cargo/config.toml b/core/.cargo/config.toml deleted file mode 100644 index 5eb3e64..0000000 --- a/core/.cargo/config.toml +++ /dev/null @@ -1,11 +0,0 @@ -[target.x86_64-linux-android] -linker = "C:\\Users\\alvih\\AppData\\Local\\Android\\Sdk\\ndk\\27.0.11902837\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin\\x86_64-linux-android24-clang" - -[target.i686-linux-android] -linker = "C:\\Users\\alvih\\AppData\\Local\\Android\\Sdk\\ndk\\27.0.11902837\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin\\i686-linux-android24-clang" - -[target.armv7-linux-androideabi] -linker = "C:\\Users\\alvih\\AppData\\Local\\Android\\Sdk\\ndk\\27.0.11902837\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin\\armv7a-linux-androideabi24-clang" - -[target.aarch64-linux-android] -linker = "C:\\Users\\alvih\\AppData\\Local\\Android\\Sdk\\ndk\\27.0.11902837\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin\\aarch64-linux-android24-clang" \ No newline at end of file diff --git a/core/src/lib.rs b/core/src/lib.rs index 75cd37b..40b64dd 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,7 +1,7 @@ pub mod erorr; pub mod metadata; -use anyhow::{Context, Result}; +use anyhow::Context; use erorr::{IrohError, IrohResult}; use futures_buffered::try_join_all; @@ -20,29 +20,22 @@ use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, iter::Iterator, sync::Arc, vec}; use std::{path::PathBuf, str::FromStr}; -#[derive(uniffi::Object)] -struct IrohNode(pub Node); - uniffi::setup_scaffolding!(); -#[derive(uniffi::Object)] +pub struct IrohNode(pub Node); + pub struct IrohInstance { node: Arc, } -#[derive(uniffi::Object)] -struct DropCollection(Collection); - -#[derive(Debug, Serialize, Deserialize, Clone, uniffi::Object)] +#[derive(Debug, Serialize, Deserialize, Clone, uniffi::Record)] pub struct FileTransfer { pub name: String, pub transfered: u64, pub total: u64, } -#[uniffi::export] impl IrohInstance { - #[uniffi::constructor] pub async fn new() -> IrohResult { let node = Node::memory().spawn().await?; Ok(Self { @@ -90,7 +83,7 @@ impl IrohInstance { ticket: String, // closure to handle each chunk mut handle_chunk: impl FnMut(Vec), - ) -> IrohResult { + ) -> IrohResult { let ticket = BlobTicket::from_str(&ticket)?; if ticket.format() != BlobFormat::HashSeq { @@ -114,10 +107,10 @@ impl IrohInstance { let chunk = chunk?; match chunk { DownloadProgress::FoundHashSeq { hash, .. } => { - let hs = self.node.blobs.read_to_bytes(hash).await?; + let hs = self.node.0.blobs.read_to_bytes(hash).await?; let hs = HashSeq::try_from(hs)?; let meta_hash = hs.iter().next().context("No metadata hash found")?; - let meta_bytes = self.node.blobs.read_to_bytes(meta_hash).await?; + let meta_bytes = self.node.0.blobs.read_to_bytes(meta_hash).await?; let meta: CollectionMetadata = postcard::from_bytes(&meta_bytes).context("Failed to parse metadata")?; @@ -131,10 +124,10 @@ impl IrohInstance { metadata = Some(meta); } DownloadProgress::AllDone(_) => { - let collection = self.node.blobs.get_collection(ticket.hash()).await?; + let collection = self.node.0.blobs.get_collection(ticket.hash()).await?; files = vec![]; for (name, hash) in collection.iter() { - let content = self.node.blobs.read_to_bytes(*hash).await?; + let content = self.node.0.blobs.read_to_bytes(*hash).await?; files.push({ FileTransfer { name: name.clone(), @@ -201,7 +194,7 @@ impl IrohInstance { } let collection = self.node.0.blobs.get_collection(ticket.hash()).await?; - Ok(DropCollection(collection)) + Ok(collection) } } @@ -212,6 +205,7 @@ pub async fn create_collection_from_files<'a>( try_join_all(paths.iter().map(|path| async move { let add_progress = iroh .get_node() + .0 .blobs .add_from_path(path.clone(), true, SetTagOption::Auto, WrapOption::NoWrap) .await; @@ -229,17 +223,3 @@ pub async fn create_collection_from_files<'a>( })) .await } - -uniffi::custom_type!(BlobTicket, String); - -impl UniffiCustomTypeConverter for BlobTicket { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(BlobTicket::from_str(&val).map_err(|e| anyhow::anyhow!(e))?) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} diff --git a/core/src/metadata.rs b/core/src/metadata.rs index a5c14ec..9a1ae6c 100644 --- a/core/src/metadata.rs +++ b/core/src/metadata.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Clone, uniffi::Object)] +#[derive(Serialize, Deserialize, Clone, uniffi::Record)] pub struct CollectionMetadata { - pub header: [u8; 13], // Must contain "CollectionV0." + pub header: Vec, // Must contain "CollectionV0." pub names: Vec, } diff --git a/package.json b/package.json index ebd345e..78b9e50 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.0.1", "private": true, "scripts": { + "uniffi:kotlin:debug": "cargo run -p uniffi-bingen generate --library ./target/debug/drop_core.dll --language=kotlin --out-dir ./bindings", "tauri": "tauri", "dev": "vite dev", "build": "vite build", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9eea0e0..95c4977 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -151,6 +151,7 @@ pub async fn export_collection( for (name, hash) in collection.iter() { let content = iroh .get_node() + .0 .blobs .read_to_bytes(*hash) .await diff --git a/uniffi-bingen/Cargo.toml b/uniffi-bingen/Cargo.toml new file mode 100644 index 0000000..3066587 --- /dev/null +++ b/uniffi-bingen/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "uniffi-bingen" +version = "0.1.0" +edition = "2021" + + +[dependencies] +uniffi = { version = "0.28.0", features = ["cli"] } diff --git a/uniffi-bingen/src/main.rs b/uniffi-bingen/src/main.rs new file mode 100644 index 0000000..f6cff6c --- /dev/null +++ b/uniffi-bingen/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi::uniffi_bindgen_main() +} From 1daecf5e07acf3ae68916d90f4716d0f31f8f692 Mon Sep 17 00:00:00 2001 From: Alvinosh Date: Tue, 2 Jul 2024 13:46:11 +0200 Subject: [PATCH 2/3] Added Sample Build --- README.md | 2 +- bindings/uniffi/drop_core/drop_core.kt | 38 ++++++++++++++++++++++++++ core/src/lib.rs | 5 ++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4d16c6..2a1d587 100644 --- a/README.md +++ b/README.md @@ -26,5 +26,5 @@ cross build -p drop_core --target x86_64-linux-android Generate the bindings using uniffi for kotlin ```sh -cargo run -p uniffi-bingen generate --library target/x86_64-linux-android/debug/libark_drop_lib.so --language=kotlin --out-dir ./bindings +cargo run -p uniffi-bingen generate --library target/x86_64-linux-android/debug/libdrop_core.so --language=kotlin --out-dir ./bindings ``` diff --git a/bindings/uniffi/drop_core/drop_core.kt b/bindings/uniffi/drop_core/drop_core.kt index fa6cd0a..4938abe 100644 --- a/bindings/uniffi/drop_core/drop_core.kt +++ b/bindings/uniffi/drop_core/drop_core.kt @@ -687,6 +687,8 @@ internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback { + + @@ -705,6 +707,8 @@ internal interface UniffiLib : Library { } + fun uniffi_drop_core_fn_func_add(`a`: Int,`b`: Int,uniffi_out_err: UniffiRustCallStatus, + ): Int fun ffi_drop_core_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue fun ffi_drop_core_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, @@ -817,6 +821,8 @@ internal interface UniffiLib : Library { ): Unit fun ffi_drop_core_rust_future_complete_void(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Unit + fun uniffi_drop_core_checksum_func_add( + ): Short fun ffi_drop_core_uniffi_contract_version( ): Int @@ -834,6 +840,9 @@ private fun uniffiCheckContractApiVersion(lib: UniffiLib) { @Suppress("UNUSED_PARAMETER") private fun uniffiCheckApiChecksums(lib: UniffiLib) { + if (lib.uniffi_drop_core_checksum_func_add() != 36557.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } } // Async support @@ -874,6 +883,26 @@ inline fun T.use(block: (T) -> R) = /** Used to instantiate an interface without an actual pointer, for fakes in tests, mostly. */ object NoPointer +public object FfiConverterInt: FfiConverter { + override fun lift(value: Int): Int { + return value + } + + override fun read(buf: ByteBuffer): Int { + return buf.getInt() + } + + override fun lower(value: Int): Int { + return value + } + + override fun allocationSize(value: Int) = 4UL + + override fun write(value: Int, buf: ByteBuffer) { + buf.putInt(value) + } +} + public object FfiConverterULong: FfiConverter { override fun lift(value: Long): ULong { return value.toULong() @@ -1049,5 +1078,14 @@ public object FfiConverterSequenceString: FfiConverterRustBuffer + UniffiLib.INSTANCE.uniffi_drop_core_fn_func_add( + FfiConverterInt.lower(`a`),FfiConverterInt.lower(`b`),_status) } + ) + } + + diff --git a/core/src/lib.rs b/core/src/lib.rs index 40b64dd..ca8ba26 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -35,6 +35,11 @@ pub struct FileTransfer { pub total: u64, } +#[uniffi::export] +fn add(a: i32, b: i32) -> i32 { + a + b +} + impl IrohInstance { pub async fn new() -> IrohResult { let node = Node::memory().spawn().await?; From 4a0b636d051293f9c6df8c4d3cb8302f622c4250 Mon Sep 17 00:00:00 2001 From: Alvinosh Date: Tue, 2 Jul 2024 16:15:42 +0200 Subject: [PATCH 3/3] Generated Bindings --- bindings/uniffi/drop_core/drop_core.kt | 1357 ++++++++++++++++++++++-- core/Cargo.toml | 22 +- core/src/erorr.rs | 75 +- core/src/lib.rs | 79 +- core/src/metadata.rs | 4 +- src-tauri/src/main.rs | 36 +- 6 files changed, 1448 insertions(+), 125 deletions(-) diff --git a/bindings/uniffi/drop_core/drop_core.kt b/bindings/uniffi/drop_core/drop_core.kt index 4938abe..f5c8b50 100644 --- a/bindings/uniffi/drop_core/drop_core.kt +++ b/bindings/uniffi/drop_core/drop_core.kt @@ -30,6 +30,14 @@ import java.nio.CharBuffer import java.nio.charset.CodingErrorAction import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.resume +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine // This is a helper for safely working with byte buffers returned from the Rust code. // A rust-owned buffer is represented by its capacity, its current length, and a @@ -677,6 +685,20 @@ internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback { + + + + + + + + + + + + + + @@ -705,10 +727,36 @@ internal interface UniffiLib : Library { } } + // The Cleaner for the whole library + internal val CLEANER: UniffiCleaner by lazy { + UniffiCleaner.create() + } } - fun uniffi_drop_core_fn_func_add(`a`: Int,`b`: Int,uniffi_out_err: UniffiRustCallStatus, - ): Int + fun uniffi_drop_core_fn_clone_dropcollection(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_drop_core_fn_free_dropcollection(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_drop_core_fn_clone_filetransferhandle(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_drop_core_fn_free_filetransferhandle(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_drop_core_fn_clone_irohinstance(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_drop_core_fn_free_irohinstance(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_drop_core_fn_constructor_irohinstance_new( + ): Long + fun uniffi_drop_core_fn_method_irohinstance_get_node(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_drop_core_fn_method_irohinstance_recieve_files(`ptr`: Pointer,`ticket`: RustBuffer.ByValue,`handleChunk`: Pointer, + ): Long + fun uniffi_drop_core_fn_method_irohinstance_send_files(`ptr`: Pointer,`files`: RustBuffer.ByValue, + ): Long + fun uniffi_drop_core_fn_clone_irohnode(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_drop_core_fn_free_irohnode(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit fun ffi_drop_core_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue fun ffi_drop_core_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, @@ -821,7 +869,13 @@ internal interface UniffiLib : Library { ): Unit fun ffi_drop_core_rust_future_complete_void(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Unit - fun uniffi_drop_core_checksum_func_add( + fun uniffi_drop_core_checksum_method_irohinstance_get_node( + ): Short + fun uniffi_drop_core_checksum_method_irohinstance_recieve_files( + ): Short + fun uniffi_drop_core_checksum_method_irohinstance_send_files( + ): Short + fun uniffi_drop_core_checksum_constructor_irohinstance_new( ): Short fun ffi_drop_core_uniffi_contract_version( ): Int @@ -840,12 +894,61 @@ private fun uniffiCheckContractApiVersion(lib: UniffiLib) { @Suppress("UNUSED_PARAMETER") private fun uniffiCheckApiChecksums(lib: UniffiLib) { - if (lib.uniffi_drop_core_checksum_func_add() != 36557.toShort()) { + if (lib.uniffi_drop_core_checksum_method_irohinstance_get_node() != 32150.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_drop_core_checksum_method_irohinstance_recieve_files() != 48969.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_drop_core_checksum_method_irohinstance_send_files() != 33734.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_drop_core_checksum_constructor_irohinstance_new() != 5702.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } } // Async support +// Async return type handlers + +internal const val UNIFFI_RUST_FUTURE_POLL_READY = 0.toByte() +internal const val UNIFFI_RUST_FUTURE_POLL_MAYBE_READY = 1.toByte() + +internal val uniffiContinuationHandleMap = UniffiHandleMap>() + +// FFI type for Rust future continuations +internal object uniffiRustFutureContinuationCallbackImpl: UniffiRustFutureContinuationCallback { + override fun callback(data: Long, pollResult: Byte) { + uniffiContinuationHandleMap.remove(data).resume(pollResult) + } +} + +internal suspend fun uniffiRustCallAsync( + rustFuture: Long, + pollFunc: (Long, UniffiRustFutureContinuationCallback, Long) -> Unit, + completeFunc: (Long, UniffiRustCallStatus) -> F, + freeFunc: (Long) -> Unit, + liftFunc: (F) -> T, + errorHandler: UniffiRustCallStatusErrorHandler +): T { + try { + do { + val pollResult = suspendCancellableCoroutine { continuation -> + pollFunc( + rustFuture, + uniffiRustFutureContinuationCallbackImpl, + uniffiContinuationHandleMap.insert(continuation) + ) + } + } while (pollResult != UNIFFI_RUST_FUTURE_POLL_READY); + + return liftFunc( + uniffiRustCallWithError(errorHandler, { status -> completeFunc(rustFuture, status) }) + ) + } finally { + freeFunc(rustFuture) + } +} // Public interface members begin here. @@ -883,26 +986,6 @@ inline fun T.use(block: (T) -> R) = /** Used to instantiate an interface without an actual pointer, for fakes in tests, mostly. */ object NoPointer -public object FfiConverterInt: FfiConverter { - override fun lift(value: Int): Int { - return value - } - - override fun read(buf: ByteBuffer): Int { - return buf.getInt() - } - - override fun lower(value: Int): Int { - return value - } - - override fun allocationSize(value: Int) = 4UL - - override fun write(value: Int, buf: ByteBuffer) { - buf.putInt(value) - } -} - public object FfiConverterULong: FfiConverter { override fun lift(value: Long): ULong { return value.toULong() @@ -977,48 +1060,995 @@ public object FfiConverterString: FfiConverter { } } -public object FfiConverterByteArray: FfiConverterRustBuffer { - override fun read(buf: ByteBuffer): ByteArray { - val len = buf.getInt() - val byteArr = ByteArray(len) - buf.get(byteArr) - return byteArr + +// 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 +// + + +// The cleaner interface for Object finalization code to run. +// This is the entry point to any implementation that we're using. +// +// The cleaner registers objects and returns cleanables, so now we are +// defining a `UniffiCleaner` with a `UniffiClenaer.Cleanable` to abstract the +// different implmentations available at compile time. +interface UniffiCleaner { + interface Cleanable { + fun clean() } - override fun allocationSize(value: ByteArray): ULong { - return 4UL + value.size.toULong() + + fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable + + companion object +} + +// The fallback Jna cleaner, which is available for both Android, and the JVM. +private class UniffiJnaCleaner : UniffiCleaner { + private val cleaner = com.sun.jna.internal.Cleaner.getCleaner() + + override fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable = + UniffiJnaCleanable(cleaner.register(value, cleanUpTask)) +} + +private class UniffiJnaCleanable( + private val cleanable: com.sun.jna.internal.Cleaner.Cleanable, +) : UniffiCleaner.Cleanable { + override fun clean() = cleanable.clean() +} + +// We decide at uniffi binding generation time whether we were +// using Android or not. +// There are further runtime checks to chose the correct implementation +// of the cleaner. +private fun UniffiCleaner.Companion.create(): UniffiCleaner = + try { + // For safety's sake: if the library hasn't been run in android_cleaner = true + // mode, but is being run on Android, then we still need to think about + // Android API versions. + // So we check if java.lang.ref.Cleaner is there, and use that… + java.lang.Class.forName("java.lang.ref.Cleaner") + JavaLangRefCleaner() + } catch (e: ClassNotFoundException) { + // … otherwise, fallback to the JNA cleaner. + UniffiJnaCleaner() } - override fun write(value: ByteArray, buf: ByteBuffer) { - buf.putInt(value.size) - buf.put(value) + +private class JavaLangRefCleaner : UniffiCleaner { + val cleaner = java.lang.ref.Cleaner.create() + + override fun register(value: Any, cleanUpTask: Runnable): UniffiCleaner.Cleanable = + JavaLangRefCleanable(cleaner.register(value, cleanUpTask)) +} + +private class JavaLangRefCleanable( + val cleanable: java.lang.ref.Cleaner.Cleanable +) : UniffiCleaner.Cleanable { + override fun clean() = cleanable.clean() +} +public interface DropCollectionInterface { + + companion object +} + +open class DropCollection: Disposable, AutoCloseable, DropCollectionInterface { + + 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_drop_core_fn_free_dropcollection(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_drop_core_fn_clone_dropcollection(pointer!!, status) + } + } + + + + + + companion object + } +public object FfiConverterTypeDropCollection: FfiConverter { + override fun lower(value: DropCollection): Pointer { + return value.uniffiClonePointer() + } -data class CollectionMetadata ( - var `header`: kotlin.ByteArray, - var `names`: List -) { + override fun lift(value: Pointer): DropCollection { + return DropCollection(value) + } + + override fun read(buf: ByteBuffer): DropCollection { + // 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: DropCollection) = 8UL + + override fun write(value: DropCollection, 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 FileTransferHandleInterface { companion object } -public object FfiConverterTypeCollectionMetadata: FfiConverterRustBuffer { - override fun read(buf: ByteBuffer): CollectionMetadata { - return CollectionMetadata( - FfiConverterByteArray.read(buf), - FfiConverterSequenceString.read(buf), - ) +open class FileTransferHandle: Disposable, AutoCloseable, FileTransferHandleInterface { + + 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_drop_core_fn_free_filetransferhandle(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_drop_core_fn_clone_filetransferhandle(pointer!!, status) + } + } + + + + + + companion object + +} + +public object FfiConverterTypeFileTransferHandle: FfiConverter { + + override fun lower(value: FileTransferHandle): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): FileTransferHandle { + return FileTransferHandle(value) + } + + override fun read(buf: ByteBuffer): FileTransferHandle { + // 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: FileTransferHandle) = 8UL + + override fun write(value: FileTransferHandle, 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 IrohInstanceInterface { + + fun `getNode`(): IrohNode + + suspend fun `recieveFiles`(`ticket`: kotlin.String, `handleChunk`: FileTransferHandle): DropCollection + + suspend fun `sendFiles`(`files`: List): BlobTicket + + companion object +} + +open class IrohInstance: Disposable, AutoCloseable, IrohInstanceInterface { + + 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)) + } + // Note no constructor generated for this object as it is async. + + 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_drop_core_fn_free_irohinstance(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_drop_core_fn_clone_irohinstance(pointer!!, status) + } + } + + override fun `getNode`(): IrohNode { + return FfiConverterTypeIrohNode.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_drop_core_fn_method_irohinstance_get_node( + it, _status) +} + } + ) + } + + + + @Throws(IrohException::class) + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + override suspend fun `recieveFiles`(`ticket`: kotlin.String, `handleChunk`: FileTransferHandle) : DropCollection { + return uniffiRustCallAsync( + callWithPointer { thisPtr -> + UniffiLib.INSTANCE.uniffi_drop_core_fn_method_irohinstance_recieve_files( + thisPtr, + FfiConverterString.lower(`ticket`),FfiConverterTypeFileTransferHandle.lower(`handleChunk`), + ) + }, + { future, callback, continuation -> UniffiLib.INSTANCE.ffi_drop_core_rust_future_poll_pointer(future, callback, continuation) }, + { future, continuation -> UniffiLib.INSTANCE.ffi_drop_core_rust_future_complete_pointer(future, continuation) }, + { future -> UniffiLib.INSTANCE.ffi_drop_core_rust_future_free_pointer(future) }, + // lift function + { FfiConverterTypeDropCollection.lift(it) }, + // Error FFI converter + IrohException.ErrorHandler, + ) } - override fun allocationSize(value: CollectionMetadata) = ( - FfiConverterByteArray.allocationSize(value.`header`) + - FfiConverterSequenceString.allocationSize(value.`names`) + + @Throws(IrohException::class) + @Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE") + override suspend fun `sendFiles`(`files`: List) : BlobTicket { + return uniffiRustCallAsync( + callWithPointer { thisPtr -> + UniffiLib.INSTANCE.uniffi_drop_core_fn_method_irohinstance_send_files( + thisPtr, + FfiConverterSequenceTypePathBuf.lower(`files`), + ) + }, + { future, callback, continuation -> UniffiLib.INSTANCE.ffi_drop_core_rust_future_poll_rust_buffer(future, callback, continuation) }, + { future, continuation -> UniffiLib.INSTANCE.ffi_drop_core_rust_future_complete_rust_buffer(future, continuation) }, + { future -> UniffiLib.INSTANCE.ffi_drop_core_rust_future_free_rust_buffer(future) }, + // lift function + { FfiConverterTypeBlobTicket.lift(it) }, + // Error FFI converter + IrohException.ErrorHandler, ) + } + + + + + + companion object + +} + +public object FfiConverterTypeIrohInstance: FfiConverter { + + override fun lower(value: IrohInstance): Pointer { + return value.uniffiClonePointer() + } - override fun write(value: CollectionMetadata, buf: ByteBuffer) { - FfiConverterByteArray.write(value.`header`, buf) - FfiConverterSequenceString.write(value.`names`, buf) + override fun lift(value: Pointer): IrohInstance { + return IrohInstance(value) + } + + override fun read(buf: ByteBuffer): IrohInstance { + // 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: IrohInstance) = 8UL + + override fun write(value: IrohInstance, 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 IrohNodeInterface { + + companion object +} + +open class IrohNode: Disposable, AutoCloseable, IrohNodeInterface { + + 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_drop_core_fn_free_irohnode(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_drop_core_fn_clone_irohnode(pointer!!, status) + } + } + + + + + + companion object + +} + +public object FfiConverterTypeIrohNode: FfiConverter { + + override fun lower(value: IrohNode): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): IrohNode { + return IrohNode(value) + } + + override fun read(buf: ByteBuffer): IrohNode { + // 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: IrohNode) = 8UL + + override fun write(value: IrohNode, 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))) } } @@ -1058,34 +2088,227 @@ public object FfiConverterTypeFileTransfer: FfiConverterRustBuffer -public object FfiConverterSequenceString: FfiConverterRustBuffer> { - override fun read(buf: ByteBuffer): List { + +sealed class IrohException: kotlin.Exception() { + + class Anyhow( + + val v1: AnyhowError + ) : IrohException() { + override val message + get() = "v1=${ v1 }" + } + + class InvalidTicket( + + val v1: IrohBaseError + ) : IrohException() { + override val message + get() = "v1=${ v1 }" + } + + class InvalidMetadata( + ) : IrohException() { + override val message + get() = "" + } + + class UnsupportedFormat( + + val v1: BlobFormat + ) : IrohException() { + override val message + get() = "v1=${ v1 }" + } + + class SendException( + + val v1: SendFileError + ) : IrohException() { + override val message + get() = "v1=${ v1 }" + } + + + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { + override fun lift(error_buf: RustBuffer.ByValue): IrohException = FfiConverterTypeIrohError.lift(error_buf) + } + + +} + +public object FfiConverterTypeIrohError : FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): IrohException { + + + return when(buf.getInt()) { + 1 -> IrohException.Anyhow( + FfiConverterTypeAnyhowError.read(buf), + ) + 2 -> IrohException.InvalidTicket( + FfiConverterTypeIrohBaseError.read(buf), + ) + 3 -> IrohException.InvalidMetadata() + 4 -> IrohException.UnsupportedFormat( + FfiConverterTypeBlobFormat.read(buf), + ) + 5 -> IrohException.SendException( + FfiConverterTypeSendFileError.read(buf), + ) + else -> throw RuntimeException("invalid error enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: IrohException): ULong { + return when(value) { + is IrohException.Anyhow -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterTypeAnyhowError.allocationSize(value.v1) + ) + is IrohException.InvalidTicket -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterTypeIrohBaseError.allocationSize(value.v1) + ) + is IrohException.InvalidMetadata -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + ) + is IrohException.UnsupportedFormat -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterTypeBlobFormat.allocationSize(value.v1) + ) + is IrohException.SendException -> ( + // Add the size for the Int that specifies the variant plus the size needed for all fields + 4UL + + FfiConverterTypeSendFileError.allocationSize(value.v1) + ) + } + } + + override fun write(value: IrohException, buf: ByteBuffer) { + when(value) { + is IrohException.Anyhow -> { + buf.putInt(1) + FfiConverterTypeAnyhowError.write(value.v1, buf) + Unit + } + is IrohException.InvalidTicket -> { + buf.putInt(2) + FfiConverterTypeIrohBaseError.write(value.v1, buf) + Unit + } + is IrohException.InvalidMetadata -> { + buf.putInt(3) + Unit + } + is IrohException.UnsupportedFormat -> { + buf.putInt(4) + FfiConverterTypeBlobFormat.write(value.v1, buf) + Unit + } + is IrohException.SendException -> { + buf.putInt(5) + FfiConverterTypeSendFileError.write(value.v1, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } + +} + + + + +public object FfiConverterSequenceTypePathBuf: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { val len = buf.getInt() - return List(len) { - FfiConverterString.read(buf) + return List(len) { + FfiConverterTypePathBuf.read(buf) } } - override fun allocationSize(value: List): ULong { + override fun allocationSize(value: List): ULong { val sizeForLength = 4UL - val sizeForItems = value.map { FfiConverterString.allocationSize(it) }.sum() + val sizeForItems = value.map { FfiConverterTypePathBuf.allocationSize(it) }.sum() return sizeForLength + sizeForItems } - override fun write(value: List, buf: ByteBuffer) { + override fun write(value: List, buf: ByteBuffer) { buf.putInt(value.size) value.iterator().forEach { - FfiConverterString.write(it, buf) + FfiConverterTypePathBuf.write(it, buf) } } -} fun `add`(`a`: kotlin.Int, `b`: kotlin.Int): kotlin.Int { - return FfiConverterInt.lift( - uniffiRustCall() { _status -> - UniffiLib.INSTANCE.uniffi_drop_core_fn_func_add( - FfiConverterInt.lower(`a`),FfiConverterInt.lower(`b`),_status) } - ) - } - + + + +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias AnyhowError = kotlin.String +public typealias FfiConverterTypeAnyhowError = FfiConverterString + + + +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias BlobFormat = kotlin.String +public typealias FfiConverterTypeBlobFormat = FfiConverterString + + + +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias BlobTicket = kotlin.String +public typealias FfiConverterTypeBlobTicket = FfiConverterString + + + +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias IrohBaseError = kotlin.String +public typealias FfiConverterTypeIrohBaseError = FfiConverterString + + + +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias PathBuf = kotlin.String +public typealias FfiConverterTypePathBuf = FfiConverterString + + + +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + * It's also what we have an external type that references a custom type. + */ +public typealias SendFileError = kotlin.String +public typealias FfiConverterTypeSendFileError = FfiConverterString + + + + + + + diff --git a/core/Cargo.toml b/core/Cargo.toml index 08291cf..24f0280 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -8,17 +8,17 @@ name = "drop_core" crate-type = ["cdylib", "lib"] [dependencies] -iroh = {workspace = true} -iroh-blobs = {workspace = true} -iroh-base = {workspace = true} -iroh-net = {workspace = true} -anyhow = {workspace = true} -thiserror = {workspace = true} -futures-buffered = {workspace = true} -futures-lite = {workspace = true} -serde = {workspace = true} +iroh = { workspace = true } +iroh-blobs = { workspace = true } +iroh-base = { workspace = true } +iroh-net = { workspace = true } +anyhow = { workspace = true } +thiserror = { workspace = true } +futures-buffered = { workspace = true } +futures-lite = { workspace = true } +serde = { workspace = true } postcard = "1.0.8" -uniffi = { version = "0.28.0"} +uniffi = { version = "0.28.0" } [build-dependencies] -uniffi = { version = "0.28.0", features = [ "build" ] } \ No newline at end of file +uniffi = { version = "0.28.0", features = ["build"] } diff --git a/core/src/erorr.rs b/core/src/erorr.rs index 2abfa1e..22b5b60 100644 --- a/core/src/erorr.rs +++ b/core/src/erorr.rs @@ -1,14 +1,20 @@ +use std::{error::Error, sync::mpsc::SendError}; + use iroh_blobs::BlobFormat; use thiserror::Error; -use crate::UniffiCustomTypeConverter; +use crate::{FileTransfer, UniffiCustomTypeConverter}; pub type IrohResult = Result; -#[derive(Error, Debug)] +pub type AnyhowError = anyhow::Error; +pub type IrohBaseError = iroh_base::ticket::Error; +pub type SendFileError = SendError>; + +#[derive(Error, Debug, uniffi::Error)] pub enum IrohError { #[error(transparent)] - Anyhow(#[from] anyhow::Error), + Anyhow(#[from] AnyhowError), #[error("invalid ticket")] InvalidTicket(#[from] iroh_base::ticket::Error), @@ -18,25 +24,70 @@ pub enum IrohError { #[error("unsupported format: {0}")] UnsupportedFormat(BlobFormat), + + #[error("send error")] + SendError(#[from] SendFileError), } -uniffi::custom_type!(IrohError, String); +uniffi::custom_type!(BlobFormat, String); -impl UniffiCustomTypeConverter for IrohError { +impl UniffiCustomTypeConverter for BlobFormat { type Builtin = String; fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(match val.as_str() { - _ => IrohError::Anyhow(anyhow::Error::msg(val)), - }) + match val.as_str() { + "hashseq" => Ok(BlobFormat::HashSeq), + "raw" => Ok(BlobFormat::Raw), + _ => Err(anyhow::anyhow!("unsupported format: {}", val).into()), + } } fn from_custom(obj: Self) -> Self::Builtin { match obj { - IrohError::Anyhow(err) => err.to_string(), - IrohError::InvalidTicket(err) => err.to_string(), - IrohError::InvalidMetadata => "invalid metadata".to_string(), - IrohError::UnsupportedFormat(format) => format!("unsupported format: {}", format), + BlobFormat::HashSeq => "hashseq".to_string(), + BlobFormat::Raw => "raw".to_string(), } } } + +uniffi::custom_type!(AnyhowError, String); + +impl UniffiCustomTypeConverter for AnyhowError { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(anyhow::Error::msg(val)) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +uniffi::custom_type!(IrohBaseError, String); + +impl UniffiCustomTypeConverter for IrohBaseError { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(iroh_base::ticket::Error::Kind { expected: "None" }) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +uniffi::custom_type!(SendFileError, String); + +impl UniffiCustomTypeConverter for SendFileError { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Err(anyhow::anyhow!("send error").into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index ca8ba26..6eee591 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -17,13 +17,20 @@ use iroh_blobs::{ }; use metadata::CollectionMetadata; use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, iter::Iterator, sync::Arc, vec}; +use std::{ + collections::BTreeMap, + iter::Iterator, + sync::{mpsc::Sender, Arc}, + vec, +}; use std::{path::PathBuf, str::FromStr}; uniffi::setup_scaffolding!(); +#[derive(uniffi::Object)] pub struct IrohNode(pub Node); +#[derive(uniffi::Object)] pub struct IrohInstance { node: Arc, } @@ -35,12 +42,49 @@ pub struct FileTransfer { pub total: u64, } -#[uniffi::export] -fn add(a: i32, b: i32) -> i32 { - a + b +uniffi::custom_type!(PathBuf, String); + +impl UniffiCustomTypeConverter for PathBuf { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(PathBuf::from(val)) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string_lossy().to_string() + } +} + +uniffi::custom_type!(BlobTicket, String); + +impl UniffiCustomTypeConverter for BlobTicket { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(BlobTicket::from_str(&val)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +#[derive(uniffi::Object)] +pub struct DropCollection(pub Collection); + +impl From for DropCollection { + fn from(collection: Collection) -> Self { + Self(collection) + } } +#[derive(uniffi::Object)] +pub struct FileTransferHandle(pub Sender>); + +#[uniffi::export] impl IrohInstance { + #[uniffi::constructor] pub async fn new() -> IrohResult { let node = Node::memory().spawn().await?; Ok(Self { @@ -52,7 +96,7 @@ impl IrohInstance { self.node.clone() } - pub async fn send_files(&self, files: &[PathBuf]) -> IrohResult { + pub async fn send_files(&self, files: Vec) -> IrohResult { let outcome = create_collection_from_files(self, files).await?; let collection = outcome @@ -86,9 +130,8 @@ impl IrohInstance { pub async fn recieve_files( &self, ticket: String, - // closure to handle each chunk - mut handle_chunk: impl FnMut(Vec), - ) -> IrohResult { + handle_chunk: Arc, + ) -> IrohResult { let ticket = BlobTicket::from_str(&ticket)?; if ticket.format() != BlobFormat::HashSeq { @@ -141,8 +184,8 @@ impl IrohInstance { } }) } - handle_chunk(files.clone()); - return Ok(collection); + handle_chunk.0.send(files.clone())?; + return Ok(collection.into()); } DownloadProgress::Done { id } => { if let Some(name) = map.get(&id) { @@ -150,7 +193,7 @@ impl IrohInstance { file.transfered = file.total; } } - handle_chunk(files.clone()); + handle_chunk.0.send(files.clone())?; } DownloadProgress::Found { id, hash, size, .. } => { if let (Some(hashseq), Some(metadata)) = (&hashseq, &metadata) { @@ -162,7 +205,7 @@ impl IrohInstance { transfered: 0, total: size, }); - handle_chunk(files.clone()); + handle_chunk.0.send(files.clone())?; map.insert(id, name.clone()); } } @@ -175,7 +218,7 @@ impl IrohInstance { file.transfered = offset; } } - handle_chunk(files.clone()); + handle_chunk.0.send(files.clone())?; } DownloadProgress::FoundLocal { hash, size, .. } => { if let (Some(hashseq), Some(metadata)) = (&hashseq, &metadata) { @@ -187,7 +230,7 @@ impl IrohInstance { { file.transfered = size.value(); file.total = size.value(); - handle_chunk(files.clone()); + handle_chunk.0.send(files.clone())?; } } } @@ -199,14 +242,14 @@ impl IrohInstance { } let collection = self.node.0.blobs.get_collection(ticket.hash()).await?; - Ok(collection) + Ok(collection.into()) } } pub async fn create_collection_from_files<'a>( iroh: &IrohInstance, - paths: &'a [PathBuf], -) -> IrohResult> { + paths: Vec, +) -> IrohResult> { try_join_all(paths.iter().map(|path| async move { let add_progress = iroh .get_node() @@ -218,7 +261,7 @@ pub async fn create_collection_from_files<'a>( Ok(add_progress) => { let progress = add_progress.finish().await; if let Ok(progress) = progress { - Ok((path, progress)) + Ok((path.clone(), progress)) } else { Err(progress.err().unwrap().into()) } diff --git a/core/src/metadata.rs b/core/src/metadata.rs index 9a1ae6c..d31cb64 100644 --- a/core/src/metadata.rs +++ b/core/src/metadata.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Clone, uniffi::Record)] +#[derive(Serialize, Deserialize, Clone)] pub struct CollectionMetadata { - pub header: Vec, // Must contain "CollectionV0." + pub header: [u8; 13], // Must contain "CollectionV0." pub names: Vec, } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 95c4977..926d286 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,14 +1,14 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use std::{path::PathBuf, str::FromStr, vec}; - use anyhow::{anyhow, Result}; -use drop_core::FileTransfer; use drop_core::IrohInstance; +use drop_core::{FileTransfer, FileTransferHandle}; use iroh_base::ticket::BlobTicket; use iroh_blobs::format::collection::Collection; use iroh_blobs::BlobFormat; +use std::sync::Arc; +use std::{path::PathBuf, str::FromStr, vec}; use tauri::{generate_context, generate_handler, InvokeError, Manager}; use tokio::sync::mpsc; use tokio::sync::Mutex; @@ -105,7 +105,7 @@ async fn generate_ticket( ) -> Result { state .iroh - .send_files(&paths) + .send_files(paths) .await .map_err(|e| InvokeError::from_anyhow(anyhow!(e))) } @@ -115,30 +115,36 @@ async fn recieve_files( state: tauri::State<'_, AppState>, ticket: String, ) -> Result { - let async_proc_input_tx = state.inner.lock().await; + let async_proc_input_tx = state.inner.lock().await.clone(); let mut handles = Vec::new(); - let files = state - .iroh - .recieve_files(ticket, |progress| { - let async_proc_input_tx = async_proc_input_tx.clone(); + let (tx, rx) = std::sync::mpsc::channel::>(); - let handle = - tokio::spawn(async move { async_proc_input_tx.send(Event::Files(progress)).await }); + handles.push(tokio::spawn(async move { + loop { + let files = rx.recv(); + if let Ok(files) = files { + let _ = async_proc_input_tx.send(Event::Files(files)).await; + } else { + break; + } + } + })); - handles.push(handle); - }) + let files = state + .iroh + .recieve_files(ticket, Arc::new(FileTransferHandle(tx))) .await .map_err(|e| InvokeError::from_anyhow(anyhow!(e)))?; for handle in handles { - let _ = handle.await; + handle.abort(); } let outpath = dirs::download_dir().unwrap(); - export_collection(&state.iroh, files, &outpath).await?; + export_collection(&state.iroh, files.0, &outpath).await?; Ok(outpath) }