diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13635f7a..f69ca4a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,6 +152,52 @@ jobs: - name: Build and Test Bindings run: ./scripts/build_python_bindings.sh --test + kotlin-bindings: + name: Kotlin Bindings + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Cache Gradle dependencies + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Cache Cargo dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-kotlin-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-kotlin- + + - name: Build and Verify Kotlin Bindings + run: ./scripts/build_kotlin_bindings.sh + + - name: Verify Examples Build + run: | + cd kotlin + ./gradlew :examples:simple_client:build + ./gradlew :examples:quic_client:build + coverage: name: Code Coverage runs-on: ubuntu-latest @@ -161,7 +207,7 @@ jobs: uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with: components: llvm-tools-preview @@ -210,7 +256,7 @@ jobs: uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 - name: Install protoc uses: arduino/setup-protoc@v3 @@ -248,7 +294,7 @@ jobs: uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 with: targets: ${{ matrix.target }} diff --git a/.gitignore b/.gitignore index b63c262c..07eff542 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,13 @@ python/examples/*.so python/examples/*.dll python/**/*.pyc + +# Kotlin Examples (generated files) +kotlin/package/src/main/resources/libflowsdk_ffi.dylib +kotlin/.gradle/ +kotlin/build/ +kotlin/**/build/ + +# TLS keylog files (for debugging) +sslkeylog.txt +**/sslkeylog.txt diff --git a/examples/c_ffi_example/quic_main.c b/examples/c_ffi_example/quic_main.c index 071b03b4..b048e9a8 100644 --- a/examples/c_ffi_example/quic_main.c +++ b/examples/c_ffi_example/quic_main.c @@ -22,6 +22,7 @@ typedef struct { const char *client_key_file; const char *alpn; uint8_t insecure_skip_verify; + uint8_t enable_key_log; } MqttTlsOptionsC; typedef struct { @@ -88,6 +89,7 @@ int main(int argc, char **argv) { */ const char *broker_host = "broker.emqx.io"; const char *broker_port = "14567"; + const char *sslkeylogfile = getenv("SSLKEYLOGFILE"); if (argc > 1) broker_host = argv[1]; @@ -143,6 +145,11 @@ int main(int argc, char **argv) { MqttTlsOptionsC q_opts = {0}; q_opts.insecure_skip_verify = 1; + q_opts.enable_key_log = sslkeylogfile != NULL; + + if (q_opts.enable_key_log) { + printf("TLS key logging enabled -> %s\n", sslkeylogfile); + } if (mqtt_quic_engine_connect(engine, server_addr_str, broker_host, &q_opts) != 0) { diff --git a/examples/c_ffi_example/tls_main.c b/examples/c_ffi_example/tls_main.c index e7bf9831..8c2003d2 100644 --- a/examples/c_ffi_example/tls_main.c +++ b/examples/c_ffi_example/tls_main.c @@ -23,6 +23,7 @@ typedef struct { const char *client_key_file; const char *alpn; uint8_t insecure_skip_verify; + uint8_t enable_key_log; } MqttTlsOptionsC; TlsMqttEngineFFI *mqtt_tls_engine_new(const char *client_id, @@ -77,6 +78,7 @@ int main(int argc, char **argv) { */ const char *broker_host = "broker.emqx.io"; const char *broker_port = "8883"; + const char *sslkeylogfile = getenv("SSLKEYLOGFILE"); if (argc > 1) broker_host = argv[1]; @@ -114,6 +116,11 @@ int main(int argc, char **argv) { // Initialize TLS Engine MqttTlsOptionsC q_opts = {0}; q_opts.insecure_skip_verify = 1; + q_opts.enable_key_log = sslkeylogfile != NULL; + + if (q_opts.enable_key_log) { + printf("TLS key logging enabled -> %s\n", sslkeylogfile); + } char client_id[32]; snprintf(client_id, sizeof(client_id), "c_ffi_tls_%u", diff --git a/flowsdk_ffi/src/engine.rs b/flowsdk_ffi/src/engine.rs index 57a6cdde..b8142fa1 100644 --- a/flowsdk_ffi/src/engine.rs +++ b/flowsdk_ffi/src/engine.rs @@ -310,6 +310,10 @@ impl TlsMqttEngineFFI { config.alpn_protocols = vec![b"mqtt".to_vec()]; } + if tls_opts.enable_key_log { + config.key_log = Arc::new(rustls::KeyLogFile::new()); + } + let engine = TlsMqttEngine::new(options, &server_name, Arc::new(config)).unwrap(); TlsMqttEngineFFI { engine: Mutex::new(engine), @@ -506,6 +510,10 @@ impl QuicMqttEngineFFI { config.alpn_protocols = vec![b"mqtt".to_vec()]; } + if tls_opts.enable_key_log { + config.key_log = Arc::new(rustls::KeyLogFile::new()); + } + self.engine .lock() .unwrap() @@ -907,6 +915,7 @@ pub unsafe extern "C" fn mqtt_tls_engine_new( client_key_file: None, insecure_skip_verify: false, alpn_protocols: vec!["mqtt".to_string()], + enable_key_log: false, } } else { let r = &*tls_opts; @@ -948,6 +957,7 @@ pub unsafe extern "C" fn mqtt_tls_engine_new( client_key_file, insecure_skip_verify: r.insecure_skip_verify != 0, alpn_protocols, + enable_key_log: r.enable_key_log != 0, } }; @@ -1203,6 +1213,7 @@ pub unsafe extern "C" fn mqtt_quic_engine_connect( client_key_file: None, insecure_skip_verify: false, alpn_protocols: vec!["mqtt".to_string()], + enable_key_log: false, } } else { let r = &*tls_opts; @@ -1239,6 +1250,7 @@ pub unsafe extern "C" fn mqtt_quic_engine_connect( client_key_file, insecure_skip_verify: r.insecure_skip_verify != 0, alpn_protocols: vec!["mqtt".to_string()], + enable_key_log: r.enable_key_log != 0, } }; @@ -1455,6 +1467,7 @@ pub struct MqttTlsOptionsC { pub client_key_file: *const c_char, pub alpn: *const c_char, pub insecure_skip_verify: u8, + pub enable_key_log: u8, } #[repr(C)] diff --git a/flowsdk_ffi/src/engine/ffi_types.rs b/flowsdk_ffi/src/engine/ffi_types.rs index 88a16bf1..d40c7ccb 100644 --- a/flowsdk_ffi/src/engine/ffi_types.rs +++ b/flowsdk_ffi/src/engine/ffi_types.rs @@ -67,6 +67,7 @@ pub struct MqttTlsOptionsFFI { pub client_key_file: Option, pub insecure_skip_verify: bool, pub alpn_protocols: Vec, + pub enable_key_log: bool, } #[derive(uniffi::Record)] diff --git a/kotlin/build.gradle.kts b/kotlin/build.gradle.kts new file mode 100644 index 00000000..c0ac19da --- /dev/null +++ b/kotlin/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + kotlin("jvm") version "1.9.22" apply false +} + +allprojects { + repositories { + mavenCentral() + } +} diff --git a/kotlin/examples/quic_client/README.md b/kotlin/examples/quic_client/README.md new file mode 100644 index 00000000..ae5af582 --- /dev/null +++ b/kotlin/examples/quic_client/README.md @@ -0,0 +1,106 @@ +# FlowSDK QUIC MQTT Client Example + +A Kotlin example demonstrating QUIC-based MQTT connectivity using FlowSDK's FFI bindings. + +## Prerequisites + +- **JDK 17** or later (Temurin recommended) +- **Rust toolchain** (stable) +- **QUIC-enabled MQTT broker** (e.g., `broker.emqx.io:14567`) + +## Build & Run + +### 1. Build the FFI Bindings + +From the repository root: + +```bash +./scripts/build_kotlin_bindings.sh +``` + +This compiles the Rust FFI library with QUIC support and generates Kotlin bindings. + +### 2. Run the Example + +```bash +cd kotlin +./gradlew :examples:quic_client:run +``` + +### 3. (Optional) Enable TLS Key Logging for Wireshark + +To capture and decrypt QUIC traffic with Wireshark, enable TLS key logging: + +```bash +cd kotlin +SSLKEYLOGFILE=$PWD/sslkeylog.txt ./gradlew :examples:quic_client:run +``` + +The TLS session keys will be written to `sslkeylog.txt` in the kotlin directory. In Wireshark: +1. Go to **Edit** → **Preferences** → **Protocols** → **TLS** +2. Set **(Pre)-Master-Secret log filename** to the absolute path of `sslkeylog.txt` +3. Start capturing on your network interface +4. Filter for `udp.port == 14567` to see the QUIC traffic +5. Wireshark will automatically decrypt the QUIC packets using the logged keys + +**Note:** Make sure to use an absolute path for `SSLKEYLOGFILE` as Gradle may change the working directory. + +Or from the example directory: + +```bash +cd kotlin/examples/quic_client +../../gradlew run +``` + +## What It Does + +1. **Connects** to `broker.emqx.io:14567` via QUIC (MQTT over UDP) +2. **Subscribes** to topic `test/kotlin/quic` (QoS 1) +3. **Publishes** message `"Hello from Kotlin QUIC!"` to the same topic +4. **Receives** the echoed message back from the broker +5. **Runs** for 10 seconds demonstrating the QUIC event loop +6. **Disconnects** gracefully sending QUIC close frames + +## Implementation Highlights + +- **Java NIO**: Uses `DatagramChannel` + `Selector` for non-blocking UDP I/O +- **Event-driven**: Polls `engine.handleTick()` every 10ms to drive QUIC timers and process MQTT events +- **Relative Timing**: Uses milliseconds elapsed since engine creation (not absolute UNIX time) for proper timeout detection +- **TLS Key Logging**: Supports SSLKEYLOGFILE environment variable for Wireshark debugging +- **Datagram flow**: + - Outgoing: `engine.takeOutgoingDatagrams()` → UDP send + - Incoming: UDP receive → `engine.handleDatagram()` + +## Troubleshooting + +**Build fails with "QuicMqttEngineFfi not found"** +→ Run `./scripts/build_kotlin_bindings.sh` from repository root first + +**Connection timeout** +→ Ensure the broker supports QUIC and is reachable on UDP port 14567 + +**Connection instability / Messages not received** +→ Public QUIC brokers may experience connection stability issues. The example successfully connects, subscribes, and publishes, but message delivery may be affected by connection drops. For production use, consider: + - Using a dedicated QUIC broker with stable configuration + - Implementing connection state monitoring and reconnection logic + - Testing with a local QUIC-enabled MQTT broker + +**Native library not found** +→ Check `kotlin/package/src/main/resources/` contains `libflowsdk_ffi.dylib` (macOS) or `.so` (Linux) +→ If missing, run `./scripts/build_kotlin_bindings.sh` from repository root to build and copy the library + +## Configuration + +Edit [Main.kt](src/main/kotlin/Main.kt) constants to customize: + +```kotlin +private const val BROKER_HOST = "broker.emqx.io" +private const val BROKER_PORT = 14567 +private const val RUN_DURATION_MS = 10_000L // Run for 10 seconds +``` + +## See Also + +- [Simple TCP Client Example](../simple_client/) — TCP-based MQTT with coroutines +- [C FFI Example](../../../examples/c_ffi_example/) — C client using the same FFI +- [FlowSDK Documentation](../../../README.md) diff --git a/kotlin/examples/quic_client/build.gradle.kts b/kotlin/examples/quic_client/build.gradle.kts new file mode 100644 index 00000000..f213f73b --- /dev/null +++ b/kotlin/examples/quic_client/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + kotlin("jvm") + id("application") +} + +group = "io.emqx.examples" +version = "0.1.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":package")) + implementation(kotlin("stdlib")) +} + +application { + mainClass.set("example.quic.MainKt") +} + +// Ensure native library is available +tasks.run.configure { + // The 'package' project copies native library to src/main/resources + // JNA usually finds it if it's in resource root +} diff --git a/kotlin/examples/quic_client/src/main/kotlin/Main.kt b/kotlin/examples/quic_client/src/main/kotlin/Main.kt new file mode 100644 index 00000000..929042b5 --- /dev/null +++ b/kotlin/examples/quic_client/src/main/kotlin/Main.kt @@ -0,0 +1,150 @@ +package example.quic + +import uniffi.flowsdk_ffi.* +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.nio.channels.DatagramChannel +import java.nio.channels.SelectionKey +import java.nio.channels.Selector +import java.nio.charset.StandardCharsets + +private const val BROKER_HOST = "broker.emqx.io" +private const val BROKER_PORT = 14567 +private const val RUN_DURATION_MS = 10_000L +private const val TICK_INTERVAL_MS = 10L +private const val RECV_BUFFER_SIZE = 65536 + +fun main() { + println("Initializing FlowSDK QUIC Kotlin Example...") + + // 1. Configure MQTT Options + val opts = MqttOptionsFfi( + clientId = "kotlin_quic_${System.currentTimeMillis() % 100000}", + mqttVersion = 5.toUByte(), + cleanStart = true, + keepAlive = 30.toUShort(), // Match Python default: 30 seconds (was 60) + username = null, + password = null, + reconnectBaseDelayMs = 1000.toULong(), + reconnectMaxDelayMs = 10000.toULong(), + maxReconnectAttempts = 3.toUInt() + ) + + // 2. Create QUIC Engine + val engine = QuicMqttEngineFfi(opts) + println("QUIC Engine created.") + + // 3. TLS options — insecureSkipVerify=true for demo broker only + val enableTlsKeyLog = !System.getenv("SSLKEYLOGFILE").isNullOrBlank() + val tlsOpts = MqttTlsOptionsFfi( + caCertFile = null, + clientCertFile = null, + clientKeyFile = null, + insecureSkipVerify = true, + alpnProtocols = listOf(), + enableKeyLog = enableTlsKeyLog // Enable TLS keylog for debugging QUIC traffic + ) + + // 4. Resolve broker and open non-blocking UDP socket + val brokerAddr = InetSocketAddress(BROKER_HOST, BROKER_PORT) + val channel = DatagramChannel.open().apply { + configureBlocking(false) + connect(brokerAddr) + } + val selector = Selector.open() + channel.register(selector, SelectionKey.OP_READ) + + val serverAddrStr = "${brokerAddr.address.hostAddress}:$BROKER_PORT" + println("Connecting to QUIC broker at $serverAddrStr (host: $BROKER_HOST)...") + + // 5. Initiate QUIC handshake — tick to generate initial packets + val startTime = System.currentTimeMillis() // Track start time for relative timestamps + engine.connect(serverAddrStr, BROKER_HOST, tlsOpts, nowMs(startTime)) + engine.handleTick(nowMs(startTime)) + sendOutgoing(engine, channel) + + // 6. Event loop: send/receive datagrams + tick engine + val recvBuf = ByteBuffer.allocateDirect(RECV_BUFFER_SIZE) + var subscribed = false + var published = false + + while (System.currentTimeMillis() - startTime < RUN_DURATION_MS) { + // Wait up to one tick interval for incoming data + selector.select(TICK_INTERVAL_MS) + + // Drain all received datagrams + recvBuf.clear() + while (channel.receive(recvBuf) != null) { + recvBuf.flip() + val data = ByteArray(recvBuf.limit()) + recvBuf.get(data) + engine.handleDatagram(data, serverAddrStr, nowMs(startTime)) + recvBuf.clear() + } + + // Tick the engine (drives QUIC timers and MQTT keepalive) + val events = engine.handleTick(nowMs(startTime)) + + // Process events + for (event in events) { + when (event) { + is MqttEventFfi.Connected -> { + println("Connected! sessionPresent=${event.v1.sessionPresent}") + + if (!subscribed) { + val pid = engine.subscribe("test/kotlin/quic", 1.toUByte()) + println("Subscribed to 'test/kotlin/quic' (PID $pid)") + subscribed = true + // Send the SUBSCRIBE packet immediately + sendOutgoing(engine, channel) + } + } + is MqttEventFfi.Subscribed -> { + println("Subscribe ack PID ${event.v1.packetId}") + + // Publish after subscription is confirmed + if (!published) { + val payload = "Hello from Kotlin QUIC!".toByteArray(StandardCharsets.UTF_8) + val pid = engine.publish("test/kotlin/quic", payload, 1.toUByte()) + println("Published to 'test/kotlin/quic' (PID $pid)") + published = true + // Tick immediately to generate QUIC frames for the PUBLISH packet + engine.handleTick(nowMs(startTime)) + // Send the PUBLISH packet immediately + sendOutgoing(engine, channel) + } + } + is MqttEventFfi.MessageReceived -> { + val msg = String(event.v1.payload, StandardCharsets.UTF_8) + println("✅ Message on '${event.v1.topic}': $msg") + } + is MqttEventFfi.Published -> println("✅ Publish ack PID ${event.v1.packetId}") + is MqttEventFfi.Disconnected -> println("⚠️ Disconnected. reasonCode=${event.reasonCode}") + is MqttEventFfi.Error -> println("❌ Error: ${event.message}") + else -> Unit // Ignore other events + } + } + + // Forward any engine-generated outgoing datagrams + sendOutgoing(engine, channel) + } + + // 7. Graceful shutdown + println("Run time elapsed, disconnecting...") + engine.disconnect() + sendOutgoing(engine, channel) // flush final QUIC close frames + + selector.close() + channel.close() + println("Done.") +} + +/** Calculate milliseconds elapsed since startTime (relative time for engine) */ +private fun nowMs(startTime: Long): ULong = (System.currentTimeMillis() - startTime).toULong() + +/** Drain all pending outgoing datagrams from the engine and send them over UDP. */ +private fun sendOutgoing(engine: QuicMqttEngineFfi, channel: DatagramChannel) { + for (dgram in engine.takeOutgoingDatagrams()) { + channel.write(ByteBuffer.wrap(dgram.data)) + } +} diff --git a/kotlin/examples/simple_client/build.gradle.kts b/kotlin/examples/simple_client/build.gradle.kts new file mode 100644 index 00000000..50ac323f --- /dev/null +++ b/kotlin/examples/simple_client/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + kotlin("jvm") + id("application") +} + +group = "io.emqx.examples" +version = "0.1.0" + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":package")) + implementation(kotlin("stdlib")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") +} + +application { + mainClass.set("example.MainKt") +} + +// Ensure native library is available +tasks.run.configure { + // We need to make sure the native library (dylib/so/dll) is in java.library.path or accessible + // The 'package' project copies it to src/main/resources, so it should be on classpath. + // JNA usually finds it if it's in resource root. +} diff --git a/kotlin/examples/simple_client/src/main/kotlin/Main.kt b/kotlin/examples/simple_client/src/main/kotlin/Main.kt new file mode 100644 index 00000000..b78d1e07 --- /dev/null +++ b/kotlin/examples/simple_client/src/main/kotlin/Main.kt @@ -0,0 +1,191 @@ +package example + +import uniffi.flowsdk_ffi.* +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.nio.channels.SelectionKey +import java.nio.channels.Selector +import java.nio.channels.SocketChannel +import java.nio.charset.StandardCharsets +import kotlinx.coroutines.* + +private const val BROKER_HOST = "broker.emqx.io" +private const val BROKER_PORT = 1883 +private const val RUN_DURATION_MS = 15_000L +private const val TICK_INTERVAL_MS = 10L + +fun main() = runBlocking { + println("Initializing FlowSDK TCP MQTT Kotlin Example...") + + // 1. Configure Options + val opts = MqttOptionsFfi( + clientId = "kotlin_tcp_${System.currentTimeMillis() % 100000}", + mqttVersion = 5.toUByte(), + cleanStart = true, + keepAlive = 60.toUShort(), + username = null, + password = null, + reconnectBaseDelayMs = 1000.toULong(), + reconnectMaxDelayMs = 10000.toULong(), + maxReconnectAttempts = 3.toUInt() + ) + + // 2. Create Engine + val engine = MqttEngineFfi.newWithOpts(opts) + println("Engine created.") + + // 3. Connect TCP socket + val brokerAddr = InetSocketAddress(BROKER_HOST, BROKER_PORT) + val channel = SocketChannel.open().apply { + configureBlocking(false) + connect(brokerAddr) + } + val selector = Selector.open() + channel.register(selector, SelectionKey.OP_CONNECT or SelectionKey.OP_READ) + + println("Connecting to TCP broker at $BROKER_HOST:$BROKER_PORT...") + + // 4. Trigger MQTT connect (produces outgoing CONNECT packet) + engine.connect() + + // 5. Event loop using coroutines + val recvBuf = ByteBuffer.allocateDirect(65536) + val startTime = System.currentTimeMillis() + var subscribed = false + var published = false + var tcpConnected = false + + launch { + while (System.currentTimeMillis() - startTime < RUN_DURATION_MS) { + // Wait for I/O or tick timeout + selector.select(TICK_INTERVAL_MS) + + val keys = selector.selectedKeys() + for (key in keys) { + if (key.isConnectable) { + if (channel.finishConnect()) { + println("TCP connected.") + key.interestOps(SelectionKey.OP_READ) + tcpConnected = true + + // Send initial MQTT CONNECT packet + val outgoing = engine.takeOutgoing() + if (outgoing.isNotEmpty()) { + channel.write(ByteBuffer.wrap(outgoing)) + } + } + } + if (key.isReadable) { + // Read from TCP socket + recvBuf.clear() + val bytesRead = channel.read(recvBuf) + if (bytesRead > 0) { + recvBuf.flip() + val data = ByteArray(recvBuf.limit()) + recvBuf.get(data) + val incomingEvents = engine.handleIncoming(data) + + // Process events from handleIncoming immediately + for (event in incomingEvents) { + when (event) { + is MqttEventFfi.Connected -> { + println("✓ Connected! Session Present: ${event.v1.sessionPresent}") + + if (!subscribed) { + val subPid = engine.subscribe("test/kotlin/tcp", 1.toUByte()) + println("✓ Subscribed to 'test/kotlin/tcp' with PID: $subPid") + subscribed = true + } + } + is MqttEventFfi.Subscribed -> { + println("✓ Subscribe Ack: PID ${event.v1.packetId}") + + if (!published) { + val payload = "Hello from Kotlin TCP!".toByteArray(StandardCharsets.UTF_8) + val pubPid = engine.publish("test/kotlin/tcp", payload, 1.toUByte(), null) + println("✓ Published to 'test/kotlin/tcp' with PID: $pubPid") + published = true + } + } + is MqttEventFfi.MessageReceived -> { + val msg = String(event.v1.payload, StandardCharsets.UTF_8) + println("📨 Received Message on '${event.v1.topic}': $msg") + } + is MqttEventFfi.Published -> println("✓ Publish Ack: PID ${event.v1.packetId}") + is MqttEventFfi.Disconnected -> println("✗ Disconnected. Reason: ${event.reasonCode}") + is MqttEventFfi.Error -> println("✗ Error: ${event.message}") + else -> Unit + } + } + } else if (bytesRead < 0) { + println("TCP connection closed by broker") + break + } + } + } + keys.clear() + + // Handle tick + val nowMs = (System.currentTimeMillis() - startTime).toULong() + val events = engine.handleTick(nowMs) + + for (event in events) { + when (event) { + is MqttEventFfi.Connected -> { + println("✓ Connected! Session Present: ${event.v1.sessionPresent}") + + if (!subscribed) { + val subPid = engine.subscribe("test/kotlin/tcp", 1.toUByte()) + println("✓ Subscribed to 'test/kotlin/tcp' with PID: $subPid") + subscribed = true + } + } + is MqttEventFfi.Subscribed -> { + println("✓ Subscribe Ack: PID ${event.v1.packetId}") + + // Publish after subscription is confirmed + if (!published) { + val payload = "Hello from Kotlin TCP!".toByteArray(StandardCharsets.UTF_8) + val pubPid = engine.publish("test/kotlin/tcp", payload, 1.toUByte(), null) + println("✓ Published to 'test/kotlin/tcp' with PID: $pubPid") + published = true + } + } + is MqttEventFfi.MessageReceived -> { + val msg = String(event.v1.payload, StandardCharsets.UTF_8) + println("📨 Received Message on '${event.v1.topic}': $msg") + } + is MqttEventFfi.Published -> println("✓ Publish Ack: PID ${event.v1.packetId}") + is MqttEventFfi.Disconnected -> { + println("✗ Disconnected. Reason: ${event.reasonCode}") + } + is MqttEventFfi.Error -> println("✗ Error: ${event.message}") + else -> Unit + } + } + + // Send any outgoing data (only after TCP connection is established) + if (tcpConnected) { + val outgoing = engine.takeOutgoing() + if (outgoing.isNotEmpty()) { + channel.write(ByteBuffer.wrap(outgoing)) + } + } + + // Use coroutine delay + delay(10) + } + }.join() + + // 6. Cleanup + println("Run time elapsed, disconnecting...") + engine.disconnect() + val finalData = engine.takeOutgoing() + if (finalData.isNotEmpty()) { + channel.write(ByteBuffer.wrap(finalData)) + } + + selector.close() + channel.close() + println("Done.") +} diff --git a/kotlin/gradle/wrapper/gradle-wrapper.jar b/kotlin/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..d997cfc6 Binary files /dev/null and b/kotlin/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kotlin/gradle/wrapper/gradle-wrapper.properties b/kotlin/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..b82aa23a --- /dev/null +++ b/kotlin/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kotlin/gradlew b/kotlin/gradlew new file mode 100755 index 00000000..739907df --- /dev/null +++ b/kotlin/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kotlin/gradlew.bat b/kotlin/gradlew.bat new file mode 100644 index 00000000..e509b2dd --- /dev/null +++ b/kotlin/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin/package/build.gradle.kts b/kotlin/package/build.gradle.kts new file mode 100644 index 00000000..87dd5a38 --- /dev/null +++ b/kotlin/package/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + kotlin("jvm") + id("java-library") +} + +group = "io.emqx" +version = "0.4.2" + +repositories { + mavenCentral() +} + +dependencies { + implementation(kotlin("stdlib")) + implementation("net.java.dev.jna:jna:5.14.0") +} + +kotlin { + jvmToolchain(17) +} diff --git a/kotlin/package/src/main/kotlin/uniffi/flowsdk_ffi/flowsdk_ffi.kt b/kotlin/package/src/main/kotlin/uniffi/flowsdk_ffi/flowsdk_ffi.kt new file mode 100644 index 00000000..24344645 --- /dev/null +++ b/kotlin/package/src/main/kotlin/uniffi/flowsdk_ffi/flowsdk_ffi.kt @@ -0,0 +1,3800 @@ +// 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.flowsdk_ffi + +// 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 +import java.util.concurrent.atomic.AtomicBoolean + +// 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. + +/** + * @suppress + */ +@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_flowsdk_ffi_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_flowsdk_ffi_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. + * + * @suppress + */ +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") +internal 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. + * + * @suppress + */ +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 + * + * @suppress + */ +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 + * + * @suppress + */ +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 + * + * @suppress + */ +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 "flowsdk_ffi" +} + +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 = "flowsdk_ffi") + .also { lib: UniffiLib -> + uniffiCheckContractApiVersion(lib) + uniffiCheckApiChecksums(lib) + } + } + + // The Cleaner for the whole library + internal val CLEANER: UniffiCleaner by lazy { + UniffiCleaner.create() + } + } + + fun uniffi_flowsdk_ffi_fn_clone_mqttengineffi(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_flowsdk_ffi_fn_free_mqttengineffi(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_constructor_mqttengineffi_new(`clientId`: RustBuffer.ByValue,`mqttVersion`: Byte,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_flowsdk_ffi_fn_constructor_mqttengineffi_new_with_opts(`opts`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_auth(`ptr`: Pointer,`reasonCode`: Byte,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_connect(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_disconnect(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_get_version(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_handle_connection_lost(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_handle_incoming(`ptr`: Pointer,`data`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_handle_tick(`ptr`: Pointer,`nowMs`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_is_connected(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_next_tick_ms(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Long + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_publish(`ptr`: Pointer,`topic`: RustBuffer.ByValue,`payload`: RustBuffer.ByValue,`qos`: Byte,`priority`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_push_event_ffi(`ptr`: Pointer,`event`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_subscribe(`ptr`: Pointer,`topicFilter`: RustBuffer.ByValue,`qos`: Byte,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_take_events(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_take_outgoing(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_flowsdk_ffi_fn_method_mqttengineffi_unsubscribe(`ptr`: Pointer,`topicFilter`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun uniffi_flowsdk_ffi_fn_clone_mqtteventlistffi(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_flowsdk_ffi_fn_free_mqtteventlistffi(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_mqtteventlistffi_get(`ptr`: Pointer,`index`: Int,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_flowsdk_ffi_fn_method_mqtteventlistffi_is_empty(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun uniffi_flowsdk_ffi_fn_method_mqtteventlistffi_len(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun uniffi_flowsdk_ffi_fn_clone_quicmqttengineffi(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_flowsdk_ffi_fn_free_quicmqttengineffi(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_constructor_quicmqttengineffi_new(`opts`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_connect(`ptr`: Pointer,`serverAddr`: RustBuffer.ByValue,`serverName`: RustBuffer.ByValue,`tlsOpts`: RustBuffer.ByValue,`nowMs`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_disconnect(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_handle_datagram(`ptr`: Pointer,`data`: RustBuffer.ByValue,`remoteAddr`: RustBuffer.ByValue,`nowMs`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_handle_tick(`ptr`: Pointer,`nowMs`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_is_connected(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_publish(`ptr`: Pointer,`topic`: RustBuffer.ByValue,`payload`: RustBuffer.ByValue,`qos`: Byte,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_subscribe(`ptr`: Pointer,`topicFilter`: RustBuffer.ByValue,`qos`: Byte,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_take_events(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_take_outgoing_datagrams(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_unsubscribe(`ptr`: Pointer,`topicFilter`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun uniffi_flowsdk_ffi_fn_clone_tlsmqttengineffi(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_flowsdk_ffi_fn_free_tlsmqttengineffi(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_constructor_tlsmqttengineffi_new(`opts`: RustBuffer.ByValue,`tlsOpts`: RustBuffer.ByValue,`serverName`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_connect(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_disconnect(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_handle_socket_data(`ptr`: Pointer,`data`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_handle_tick(`ptr`: Pointer,`nowMs`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_is_connected(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_publish(`ptr`: Pointer,`topic`: RustBuffer.ByValue,`payload`: RustBuffer.ByValue,`qos`: Byte,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_subscribe(`ptr`: Pointer,`topicFilter`: RustBuffer.ByValue,`qos`: Byte,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_take_events(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_take_socket_data(`ptr`: Pointer,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_unsubscribe(`ptr`: Pointer,`topicFilter`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun ffi_flowsdk_ffi_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_flowsdk_ffi_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_flowsdk_ffi_rustbuffer_free(`buf`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun ffi_flowsdk_ffi_rustbuffer_reserve(`buf`: RustBuffer.ByValue,`additional`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_flowsdk_ffi_rust_future_poll_u8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_u8(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_u8(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_u8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun ffi_flowsdk_ffi_rust_future_poll_i8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_i8(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_i8(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_i8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Byte + fun ffi_flowsdk_ffi_rust_future_poll_u16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_u16(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_u16(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_u16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Short + fun ffi_flowsdk_ffi_rust_future_poll_i16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_i16(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_i16(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_i16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Short + fun ffi_flowsdk_ffi_rust_future_poll_u32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_u32(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_u32(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_u32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun ffi_flowsdk_ffi_rust_future_poll_i32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_i32(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_i32(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_i32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Int + fun ffi_flowsdk_ffi_rust_future_poll_u64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_u64(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_u64(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_u64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Long + fun ffi_flowsdk_ffi_rust_future_poll_i64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_i64(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_i64(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_i64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Long + fun ffi_flowsdk_ffi_rust_future_poll_f32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_f32(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_f32(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_f32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Float + fun ffi_flowsdk_ffi_rust_future_poll_f64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_f64(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_f64(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_f64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Double + fun ffi_flowsdk_ffi_rust_future_poll_pointer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_pointer(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_pointer(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_pointer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Pointer + fun ffi_flowsdk_ffi_rust_future_poll_rust_buffer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_rust_buffer(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_rust_buffer(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_rust_buffer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): RustBuffer.ByValue + fun ffi_flowsdk_ffi_rust_future_poll_void(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_cancel_void(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_free_void(`handle`: Long, + ): Unit + fun ffi_flowsdk_ffi_rust_future_complete_void(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + ): Unit + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_auth( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_connect( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_disconnect( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_get_version( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_handle_connection_lost( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_handle_incoming( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_handle_tick( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_is_connected( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_next_tick_ms( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_publish( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_push_event_ffi( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_subscribe( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_take_events( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_take_outgoing( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqttengineffi_unsubscribe( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqtteventlistffi_get( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqtteventlistffi_is_empty( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_mqtteventlistffi_len( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_connect( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_disconnect( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_handle_datagram( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_handle_tick( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_is_connected( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_publish( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_subscribe( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_take_events( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_take_outgoing_datagrams( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_unsubscribe( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_connect( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_disconnect( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_handle_socket_data( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_handle_tick( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_is_connected( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_publish( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_subscribe( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_take_events( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_take_socket_data( + ): Short + fun uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_unsubscribe( + ): Short + fun uniffi_flowsdk_ffi_checksum_constructor_mqttengineffi_new( + ): Short + fun uniffi_flowsdk_ffi_checksum_constructor_mqttengineffi_new_with_opts( + ): Short + fun uniffi_flowsdk_ffi_checksum_constructor_quicmqttengineffi_new( + ): Short + fun uniffi_flowsdk_ffi_checksum_constructor_tlsmqttengineffi_new( + ): Short + fun ffi_flowsdk_ffi_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_flowsdk_ffi_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) { + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_auth() != 13274.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_connect() != 11843.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_disconnect() != 35275.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_get_version() != 19826.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_handle_connection_lost() != 12358.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_handle_incoming() != 55488.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_handle_tick() != 40864.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_is_connected() != 33756.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_next_tick_ms() != 63992.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_publish() != 44572.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_push_event_ffi() != 16806.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_subscribe() != 31682.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_take_events() != 17921.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_take_outgoing() != 12337.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqttengineffi_unsubscribe() != 51097.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqtteventlistffi_get() != 5088.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqtteventlistffi_is_empty() != 43194.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_mqtteventlistffi_len() != 36035.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_connect() != 39570.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_disconnect() != 54076.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_handle_datagram() != 34322.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_handle_tick() != 39076.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_is_connected() != 53863.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_publish() != 17969.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_subscribe() != 16787.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_take_events() != 34914.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_take_outgoing_datagrams() != 32224.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_quicmqttengineffi_unsubscribe() != 48.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_connect() != 53946.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_disconnect() != 58642.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_handle_socket_data() != 27993.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_handle_tick() != 57570.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_is_connected() != 11670.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_publish() != 29858.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_subscribe() != 35959.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_take_events() != 41728.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_take_socket_data() != 6663.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_method_tlsmqttengineffi_unsubscribe() != 21790.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_constructor_mqttengineffi_new() != 63280.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_constructor_mqttengineffi_new_with_opts() != 49926.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_constructor_quicmqttengineffi_new() != 4296.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } + if (lib.uniffi_flowsdk_ffi_checksum_constructor_tlsmqttengineffi_new() != 55234.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } +} + +// 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) + } + } +} + +/** + * @suppress + */ +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. + * + * @suppress + * */ +object NoPointer + +/** + * @suppress + */ +public object FfiConverterUByte: FfiConverter { + override fun lift(value: Byte): UByte { + return value.toUByte() + } + + override fun read(buf: ByteBuffer): UByte { + return lift(buf.get()) + } + + override fun lower(value: UByte): Byte { + return value.toByte() + } + + override fun allocationSize(value: UByte) = 1UL + + override fun write(value: UByte, buf: ByteBuffer) { + buf.put(value.toByte()) + } +} + +/** + * @suppress + */ +public object FfiConverterUShort: FfiConverter { + override fun lift(value: Short): UShort { + return value.toUShort() + } + + override fun read(buf: ByteBuffer): UShort { + return lift(buf.getShort()) + } + + override fun lower(value: UShort): Short { + return value.toShort() + } + + override fun allocationSize(value: UShort) = 2UL + + override fun write(value: UShort, buf: ByteBuffer) { + buf.putShort(value.toShort()) + } +} + +/** + * @suppress + */ +public object FfiConverterUInt: FfiConverter { + override fun lift(value: Int): UInt { + return value.toUInt() + } + + override fun read(buf: ByteBuffer): UInt { + return lift(buf.getInt()) + } + + override fun lower(value: UInt): Int { + return value.toInt() + } + + override fun allocationSize(value: UInt) = 4UL + + override fun write(value: UInt, buf: ByteBuffer) { + buf.putInt(value.toInt()) + } +} + +/** + * @suppress + */ +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) + } +} + +/** + * @suppress + */ +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()) + } +} + +/** + * @suppress + */ +public object FfiConverterLong: FfiConverter { + override fun lift(value: Long): Long { + return value + } + + override fun read(buf: ByteBuffer): Long { + return buf.getLong() + } + + override fun lower(value: Long): Long { + return value + } + + override fun allocationSize(value: Long) = 8UL + + override fun write(value: Long, buf: ByteBuffer) { + buf.putLong(value) + } +} + +/** + * @suppress + */ +public object FfiConverterBoolean: FfiConverter { + override fun lift(value: Byte): Boolean { + return value.toInt() != 0 + } + + override fun read(buf: ByteBuffer): Boolean { + return lift(buf.get()) + } + + override fun lower(value: Boolean): Byte { + return if (value) 1.toByte() else 0.toByte() + } + + override fun allocationSize(value: Boolean) = 1UL + + override fun write(value: Boolean, buf: ByteBuffer) { + buf.put(lower(value)) + } +} + +/** + * @suppress + */ +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) + } +} + +/** + * @suppress + */ +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) + } +} + + +// 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. + * + * @suppress + */ +interface UniffiCleaner { + interface Cleanable { + fun clean() + } + + 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() + } + +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 MqttEngineFfiInterface { + + fun `auth`(`reasonCode`: kotlin.UByte) + + fun `connect`() + + fun `disconnect`() + + fun `getVersion`(): kotlin.UByte + + fun `handleConnectionLost`() + + fun `handleIncoming`(`data`: kotlin.ByteArray): List + + fun `handleTick`(`nowMs`: kotlin.ULong): List + + fun `isConnected`(): kotlin.Boolean + + fun `nextTickMs`(): kotlin.Long + + fun `publish`(`topic`: kotlin.String, `payload`: kotlin.ByteArray, `qos`: kotlin.UByte, `priority`: kotlin.UByte?): kotlin.Int + + fun `pushEventFfi`(`event`: MqttEventFfi) + + fun `subscribe`(`topicFilter`: kotlin.String, `qos`: kotlin.UByte): kotlin.Int + + fun `takeEvents`(): List + + fun `takeOutgoing`(): kotlin.ByteArray + + fun `unsubscribe`(`topicFilter`: kotlin.String): kotlin.Int + + companion object +} + +open class MqttEngineFfi: Disposable, AutoCloseable, MqttEngineFfiInterface { + + 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)) + } + constructor(`clientId`: kotlin.String?, `mqttVersion`: kotlin.UByte) : + this( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_constructor_mqttengineffi_new( + FfiConverterOptionalString.lower(`clientId`),FfiConverterUByte.lower(`mqttVersion`),_status) +} + ) + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @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_flowsdk_ffi_fn_free_mqttengineffi(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_clone_mqttengineffi(pointer!!, status) + } + } + + override fun `auth`(`reasonCode`: kotlin.UByte) + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_auth( + it, FfiConverterUByte.lower(`reasonCode`),_status) +} + } + + + + override fun `connect`() + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_connect( + it, _status) +} + } + + + + override fun `disconnect`() + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_disconnect( + it, _status) +} + } + + + + override fun `getVersion`(): kotlin.UByte { + return FfiConverterUByte.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_get_version( + it, _status) +} + } + ) + } + + + override fun `handleConnectionLost`() + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_handle_connection_lost( + it, _status) +} + } + + + + override fun `handleIncoming`(`data`: kotlin.ByteArray): List { + return FfiConverterSequenceTypeMqttEventFFI.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_handle_incoming( + it, FfiConverterByteArray.lower(`data`),_status) +} + } + ) + } + + + override fun `handleTick`(`nowMs`: kotlin.ULong): List { + return FfiConverterSequenceTypeMqttEventFFI.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_handle_tick( + it, FfiConverterULong.lower(`nowMs`),_status) +} + } + ) + } + + + override fun `isConnected`(): kotlin.Boolean { + return FfiConverterBoolean.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_is_connected( + it, _status) +} + } + ) + } + + + override fun `nextTickMs`(): kotlin.Long { + return FfiConverterLong.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_next_tick_ms( + it, _status) +} + } + ) + } + + + override fun `publish`(`topic`: kotlin.String, `payload`: kotlin.ByteArray, `qos`: kotlin.UByte, `priority`: kotlin.UByte?): kotlin.Int { + return FfiConverterInt.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_publish( + it, FfiConverterString.lower(`topic`),FfiConverterByteArray.lower(`payload`),FfiConverterUByte.lower(`qos`),FfiConverterOptionalUByte.lower(`priority`),_status) +} + } + ) + } + + + override fun `pushEventFfi`(`event`: MqttEventFfi) + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_push_event_ffi( + it, FfiConverterTypeMqttEventFFI.lower(`event`),_status) +} + } + + + + override fun `subscribe`(`topicFilter`: kotlin.String, `qos`: kotlin.UByte): kotlin.Int { + return FfiConverterInt.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_subscribe( + it, FfiConverterString.lower(`topicFilter`),FfiConverterUByte.lower(`qos`),_status) +} + } + ) + } + + + override fun `takeEvents`(): List { + return FfiConverterSequenceTypeMqttEventFFI.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_take_events( + it, _status) +} + } + ) + } + + + override fun `takeOutgoing`(): kotlin.ByteArray { + return FfiConverterByteArray.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_take_outgoing( + it, _status) +} + } + ) + } + + + override fun `unsubscribe`(`topicFilter`: kotlin.String): kotlin.Int { + return FfiConverterInt.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqttengineffi_unsubscribe( + it, FfiConverterString.lower(`topicFilter`),_status) +} + } + ) + } + + + + + + companion object { + fun `newWithOpts`(`opts`: MqttOptionsFfi): MqttEngineFfi { + return FfiConverterTypeMqttEngineFFI.lift( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_constructor_mqttengineffi_new_with_opts( + FfiConverterTypeMqttOptionsFFI.lower(`opts`),_status) +} + ) + } + + + + } + +} + +/** + * @suppress + */ +public object FfiConverterTypeMqttEngineFFI: FfiConverter { + + override fun lower(value: MqttEngineFfi): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): MqttEngineFfi { + return MqttEngineFfi(value) + } + + override fun read(buf: ByteBuffer): MqttEngineFfi { + // 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: MqttEngineFfi) = 8UL + + override fun write(value: MqttEngineFfi, 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 MqttEventListFfiInterface { + + fun `get`(`index`: kotlin.UInt): MqttEventFfi? + + fun `isEmpty`(): kotlin.Boolean + + fun `len`(): kotlin.UInt + + companion object +} + +open class MqttEventListFfi: Disposable, AutoCloseable, MqttEventListFfiInterface { + + 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_flowsdk_ffi_fn_free_mqtteventlistffi(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_clone_mqtteventlistffi(pointer!!, status) + } + } + + override fun `get`(`index`: kotlin.UInt): MqttEventFfi? { + return FfiConverterOptionalTypeMqttEventFFI.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqtteventlistffi_get( + it, FfiConverterUInt.lower(`index`),_status) +} + } + ) + } + + + override fun `isEmpty`(): kotlin.Boolean { + return FfiConverterBoolean.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqtteventlistffi_is_empty( + it, _status) +} + } + ) + } + + + override fun `len`(): kotlin.UInt { + return FfiConverterUInt.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_mqtteventlistffi_len( + it, _status) +} + } + ) + } + + + + + + + companion object + +} + +/** + * @suppress + */ +public object FfiConverterTypeMqttEventListFFI: FfiConverter { + + override fun lower(value: MqttEventListFfi): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): MqttEventListFfi { + return MqttEventListFfi(value) + } + + override fun read(buf: ByteBuffer): MqttEventListFfi { + // 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: MqttEventListFfi) = 8UL + + override fun write(value: MqttEventListFfi, 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 QuicMqttEngineFfiInterface { + + fun `connect`(`serverAddr`: kotlin.String, `serverName`: kotlin.String, `tlsOpts`: MqttTlsOptionsFfi, `nowMs`: kotlin.ULong) + + fun `disconnect`() + + fun `handleDatagram`(`data`: kotlin.ByteArray, `remoteAddr`: kotlin.String, `nowMs`: kotlin.ULong) + + fun `handleTick`(`nowMs`: kotlin.ULong): List + + fun `isConnected`(): kotlin.Boolean + + fun `publish`(`topic`: kotlin.String, `payload`: kotlin.ByteArray, `qos`: kotlin.UByte): kotlin.Int + + fun `subscribe`(`topicFilter`: kotlin.String, `qos`: kotlin.UByte): kotlin.Int + + fun `takeEvents`(): List + + fun `takeOutgoingDatagrams`(): List + + fun `unsubscribe`(`topicFilter`: kotlin.String): kotlin.Int + + companion object +} + +open class QuicMqttEngineFfi: Disposable, AutoCloseable, QuicMqttEngineFfiInterface { + + 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)) + } + constructor(`opts`: MqttOptionsFfi) : + this( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_constructor_quicmqttengineffi_new( + FfiConverterTypeMqttOptionsFFI.lower(`opts`),_status) +} + ) + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @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_flowsdk_ffi_fn_free_quicmqttengineffi(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_clone_quicmqttengineffi(pointer!!, status) + } + } + + override fun `connect`(`serverAddr`: kotlin.String, `serverName`: kotlin.String, `tlsOpts`: MqttTlsOptionsFfi, `nowMs`: kotlin.ULong) + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_connect( + it, FfiConverterString.lower(`serverAddr`),FfiConverterString.lower(`serverName`),FfiConverterTypeMqttTlsOptionsFFI.lower(`tlsOpts`),FfiConverterULong.lower(`nowMs`),_status) +} + } + + + + override fun `disconnect`() + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_disconnect( + it, _status) +} + } + + + + override fun `handleDatagram`(`data`: kotlin.ByteArray, `remoteAddr`: kotlin.String, `nowMs`: kotlin.ULong) + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_handle_datagram( + it, FfiConverterByteArray.lower(`data`),FfiConverterString.lower(`remoteAddr`),FfiConverterULong.lower(`nowMs`),_status) +} + } + + + + override fun `handleTick`(`nowMs`: kotlin.ULong): List { + return FfiConverterSequenceTypeMqttEventFFI.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_handle_tick( + it, FfiConverterULong.lower(`nowMs`),_status) +} + } + ) + } + + + override fun `isConnected`(): kotlin.Boolean { + return FfiConverterBoolean.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_is_connected( + it, _status) +} + } + ) + } + + + override fun `publish`(`topic`: kotlin.String, `payload`: kotlin.ByteArray, `qos`: kotlin.UByte): kotlin.Int { + return FfiConverterInt.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_publish( + it, FfiConverterString.lower(`topic`),FfiConverterByteArray.lower(`payload`),FfiConverterUByte.lower(`qos`),_status) +} + } + ) + } + + + override fun `subscribe`(`topicFilter`: kotlin.String, `qos`: kotlin.UByte): kotlin.Int { + return FfiConverterInt.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_subscribe( + it, FfiConverterString.lower(`topicFilter`),FfiConverterUByte.lower(`qos`),_status) +} + } + ) + } + + + override fun `takeEvents`(): List { + return FfiConverterSequenceTypeMqttEventFFI.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_take_events( + it, _status) +} + } + ) + } + + + override fun `takeOutgoingDatagrams`(): List { + return FfiConverterSequenceTypeMqttDatagramFFI.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_take_outgoing_datagrams( + it, _status) +} + } + ) + } + + + override fun `unsubscribe`(`topicFilter`: kotlin.String): kotlin.Int { + return FfiConverterInt.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_quicmqttengineffi_unsubscribe( + it, FfiConverterString.lower(`topicFilter`),_status) +} + } + ) + } + + + + + + + companion object + +} + +/** + * @suppress + */ +public object FfiConverterTypeQuicMqttEngineFFI: FfiConverter { + + override fun lower(value: QuicMqttEngineFfi): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): QuicMqttEngineFfi { + return QuicMqttEngineFfi(value) + } + + override fun read(buf: ByteBuffer): QuicMqttEngineFfi { + // 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: QuicMqttEngineFfi) = 8UL + + override fun write(value: QuicMqttEngineFfi, 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 TlsMqttEngineFfiInterface { + + fun `connect`() + + fun `disconnect`() + + fun `handleSocketData`(`data`: kotlin.ByteArray) + + fun `handleTick`(`nowMs`: kotlin.ULong): List + + fun `isConnected`(): kotlin.Boolean + + fun `publish`(`topic`: kotlin.String, `payload`: kotlin.ByteArray, `qos`: kotlin.UByte): kotlin.Int + + fun `subscribe`(`topicFilter`: kotlin.String, `qos`: kotlin.UByte): kotlin.Int + + fun `takeEvents`(): List + + fun `takeSocketData`(): kotlin.ByteArray + + fun `unsubscribe`(`topicFilter`: kotlin.String): kotlin.Int + + companion object +} + +open class TlsMqttEngineFfi: Disposable, AutoCloseable, TlsMqttEngineFfiInterface { + + 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)) + } + constructor(`opts`: MqttOptionsFfi, `tlsOpts`: MqttTlsOptionsFfi, `serverName`: kotlin.String) : + this( + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_constructor_tlsmqttengineffi_new( + FfiConverterTypeMqttOptionsFFI.lower(`opts`),FfiConverterTypeMqttTlsOptionsFFI.lower(`tlsOpts`),FfiConverterString.lower(`serverName`),_status) +} + ) + + protected val pointer: Pointer? + protected val cleanable: UniffiCleaner.Cleanable + + private val wasDestroyed = AtomicBoolean(false) + private val callCounter = AtomicLong(1) + + override fun destroy() { + // Only allow a single call to this method. + // TODO: maybe we should log a warning if called more than once? + if (this.wasDestroyed.compareAndSet(false, true)) { + // This decrement always matches the initial count of 1 given at creation time. + if (this.callCounter.decrementAndGet() == 0L) { + cleanable.clean() + } + } + } + + @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_flowsdk_ffi_fn_free_tlsmqttengineffi(ptr, status) + } + } + } + } + + fun uniffiClonePointer(): Pointer { + return uniffiRustCall() { status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_clone_tlsmqttengineffi(pointer!!, status) + } + } + + override fun `connect`() + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_connect( + it, _status) +} + } + + + + override fun `disconnect`() + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_disconnect( + it, _status) +} + } + + + + override fun `handleSocketData`(`data`: kotlin.ByteArray) + = + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_handle_socket_data( + it, FfiConverterByteArray.lower(`data`),_status) +} + } + + + + override fun `handleTick`(`nowMs`: kotlin.ULong): List { + return FfiConverterSequenceTypeMqttEventFFI.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_handle_tick( + it, FfiConverterULong.lower(`nowMs`),_status) +} + } + ) + } + + + override fun `isConnected`(): kotlin.Boolean { + return FfiConverterBoolean.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_is_connected( + it, _status) +} + } + ) + } + + + override fun `publish`(`topic`: kotlin.String, `payload`: kotlin.ByteArray, `qos`: kotlin.UByte): kotlin.Int { + return FfiConverterInt.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_publish( + it, FfiConverterString.lower(`topic`),FfiConverterByteArray.lower(`payload`),FfiConverterUByte.lower(`qos`),_status) +} + } + ) + } + + + override fun `subscribe`(`topicFilter`: kotlin.String, `qos`: kotlin.UByte): kotlin.Int { + return FfiConverterInt.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_subscribe( + it, FfiConverterString.lower(`topicFilter`),FfiConverterUByte.lower(`qos`),_status) +} + } + ) + } + + + override fun `takeEvents`(): List { + return FfiConverterSequenceTypeMqttEventFFI.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_take_events( + it, _status) +} + } + ) + } + + + override fun `takeSocketData`(): kotlin.ByteArray { + return FfiConverterByteArray.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_take_socket_data( + it, _status) +} + } + ) + } + + + override fun `unsubscribe`(`topicFilter`: kotlin.String): kotlin.Int { + return FfiConverterInt.lift( + callWithPointer { + uniffiRustCall() { _status -> + UniffiLib.INSTANCE.uniffi_flowsdk_ffi_fn_method_tlsmqttengineffi_unsubscribe( + it, FfiConverterString.lower(`topicFilter`),_status) +} + } + ) + } + + + + + + + companion object + +} + +/** + * @suppress + */ +public object FfiConverterTypeTlsMqttEngineFFI: FfiConverter { + + override fun lower(value: TlsMqttEngineFfi): Pointer { + return value.uniffiClonePointer() + } + + override fun lift(value: Pointer): TlsMqttEngineFfi { + return TlsMqttEngineFfi(value) + } + + override fun read(buf: ByteBuffer): TlsMqttEngineFfi { + // 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: TlsMqttEngineFfi) = 8UL + + override fun write(value: TlsMqttEngineFfi, 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))) + } +} + + + +data class ConnectionResultFfi ( + var `reasonCode`: kotlin.UByte, + var `sessionPresent`: kotlin.Boolean +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeConnectionResultFFI: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): ConnectionResultFfi { + return ConnectionResultFfi( + FfiConverterUByte.read(buf), + FfiConverterBoolean.read(buf), + ) + } + + override fun allocationSize(value: ConnectionResultFfi) = ( + FfiConverterUByte.allocationSize(value.`reasonCode`) + + FfiConverterBoolean.allocationSize(value.`sessionPresent`) + ) + + override fun write(value: ConnectionResultFfi, buf: ByteBuffer) { + FfiConverterUByte.write(value.`reasonCode`, buf) + FfiConverterBoolean.write(value.`sessionPresent`, buf) + } +} + + + +data class MqttDatagramFfi ( + var `addr`: kotlin.String, + var `data`: kotlin.ByteArray +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeMqttDatagramFFI: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): MqttDatagramFfi { + return MqttDatagramFfi( + FfiConverterString.read(buf), + FfiConverterByteArray.read(buf), + ) + } + + override fun allocationSize(value: MqttDatagramFfi) = ( + FfiConverterString.allocationSize(value.`addr`) + + FfiConverterByteArray.allocationSize(value.`data`) + ) + + override fun write(value: MqttDatagramFfi, buf: ByteBuffer) { + FfiConverterString.write(value.`addr`, buf) + FfiConverterByteArray.write(value.`data`, buf) + } +} + + + +data class MqttMessageFfi ( + var `topic`: kotlin.String, + var `payload`: kotlin.ByteArray, + var `qos`: kotlin.UByte, + var `retain`: kotlin.Boolean +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeMqttMessageFFI: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): MqttMessageFfi { + return MqttMessageFfi( + FfiConverterString.read(buf), + FfiConverterByteArray.read(buf), + FfiConverterUByte.read(buf), + FfiConverterBoolean.read(buf), + ) + } + + override fun allocationSize(value: MqttMessageFfi) = ( + FfiConverterString.allocationSize(value.`topic`) + + FfiConverterByteArray.allocationSize(value.`payload`) + + FfiConverterUByte.allocationSize(value.`qos`) + + FfiConverterBoolean.allocationSize(value.`retain`) + ) + + override fun write(value: MqttMessageFfi, buf: ByteBuffer) { + FfiConverterString.write(value.`topic`, buf) + FfiConverterByteArray.write(value.`payload`, buf) + FfiConverterUByte.write(value.`qos`, buf) + FfiConverterBoolean.write(value.`retain`, buf) + } +} + + + +data class MqttOptionsFfi ( + var `clientId`: kotlin.String, + var `mqttVersion`: kotlin.UByte, + var `cleanStart`: kotlin.Boolean, + var `keepAlive`: kotlin.UShort, + var `username`: kotlin.String?, + var `password`: kotlin.String?, + var `reconnectBaseDelayMs`: kotlin.ULong, + var `reconnectMaxDelayMs`: kotlin.ULong, + var `maxReconnectAttempts`: kotlin.UInt +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeMqttOptionsFFI: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): MqttOptionsFfi { + return MqttOptionsFfi( + FfiConverterString.read(buf), + FfiConverterUByte.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterUShort.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterULong.read(buf), + FfiConverterULong.read(buf), + FfiConverterUInt.read(buf), + ) + } + + override fun allocationSize(value: MqttOptionsFfi) = ( + FfiConverterString.allocationSize(value.`clientId`) + + FfiConverterUByte.allocationSize(value.`mqttVersion`) + + FfiConverterBoolean.allocationSize(value.`cleanStart`) + + FfiConverterUShort.allocationSize(value.`keepAlive`) + + FfiConverterOptionalString.allocationSize(value.`username`) + + FfiConverterOptionalString.allocationSize(value.`password`) + + FfiConverterULong.allocationSize(value.`reconnectBaseDelayMs`) + + FfiConverterULong.allocationSize(value.`reconnectMaxDelayMs`) + + FfiConverterUInt.allocationSize(value.`maxReconnectAttempts`) + ) + + override fun write(value: MqttOptionsFfi, buf: ByteBuffer) { + FfiConverterString.write(value.`clientId`, buf) + FfiConverterUByte.write(value.`mqttVersion`, buf) + FfiConverterBoolean.write(value.`cleanStart`, buf) + FfiConverterUShort.write(value.`keepAlive`, buf) + FfiConverterOptionalString.write(value.`username`, buf) + FfiConverterOptionalString.write(value.`password`, buf) + FfiConverterULong.write(value.`reconnectBaseDelayMs`, buf) + FfiConverterULong.write(value.`reconnectMaxDelayMs`, buf) + FfiConverterUInt.write(value.`maxReconnectAttempts`, buf) + } +} + + + +data class MqttTlsOptionsFfi ( + var `caCertFile`: kotlin.String?, + var `clientCertFile`: kotlin.String?, + var `clientKeyFile`: kotlin.String?, + var `insecureSkipVerify`: kotlin.Boolean, + var `alpnProtocols`: List, + var `enableKeyLog`: kotlin.Boolean +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeMqttTlsOptionsFFI: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): MqttTlsOptionsFfi { + return MqttTlsOptionsFfi( + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterOptionalString.read(buf), + FfiConverterBoolean.read(buf), + FfiConverterSequenceString.read(buf), + FfiConverterBoolean.read(buf), + ) + } + + override fun allocationSize(value: MqttTlsOptionsFfi) = ( + FfiConverterOptionalString.allocationSize(value.`caCertFile`) + + FfiConverterOptionalString.allocationSize(value.`clientCertFile`) + + FfiConverterOptionalString.allocationSize(value.`clientKeyFile`) + + FfiConverterBoolean.allocationSize(value.`insecureSkipVerify`) + + FfiConverterSequenceString.allocationSize(value.`alpnProtocols`) + + FfiConverterBoolean.allocationSize(value.`enableKeyLog`) + ) + + override fun write(value: MqttTlsOptionsFfi, buf: ByteBuffer) { + FfiConverterOptionalString.write(value.`caCertFile`, buf) + FfiConverterOptionalString.write(value.`clientCertFile`, buf) + FfiConverterOptionalString.write(value.`clientKeyFile`, buf) + FfiConverterBoolean.write(value.`insecureSkipVerify`, buf) + FfiConverterSequenceString.write(value.`alpnProtocols`, buf) + FfiConverterBoolean.write(value.`enableKeyLog`, buf) + } +} + + + +data class PublishResultFfi ( + var `packetId`: kotlin.UShort?, + var `reasonCode`: kotlin.UByte?, + var `qos`: kotlin.UByte +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypePublishResultFFI: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): PublishResultFfi { + return PublishResultFfi( + FfiConverterOptionalUShort.read(buf), + FfiConverterOptionalUByte.read(buf), + FfiConverterUByte.read(buf), + ) + } + + override fun allocationSize(value: PublishResultFfi) = ( + FfiConverterOptionalUShort.allocationSize(value.`packetId`) + + FfiConverterOptionalUByte.allocationSize(value.`reasonCode`) + + FfiConverterUByte.allocationSize(value.`qos`) + ) + + override fun write(value: PublishResultFfi, buf: ByteBuffer) { + FfiConverterOptionalUShort.write(value.`packetId`, buf) + FfiConverterOptionalUByte.write(value.`reasonCode`, buf) + FfiConverterUByte.write(value.`qos`, buf) + } +} + + + +data class SubscribeResultFfi ( + var `packetId`: kotlin.UShort, + var `reasonCodes`: kotlin.ByteArray +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeSubscribeResultFFI: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): SubscribeResultFfi { + return SubscribeResultFfi( + FfiConverterUShort.read(buf), + FfiConverterByteArray.read(buf), + ) + } + + override fun allocationSize(value: SubscribeResultFfi) = ( + FfiConverterUShort.allocationSize(value.`packetId`) + + FfiConverterByteArray.allocationSize(value.`reasonCodes`) + ) + + override fun write(value: SubscribeResultFfi, buf: ByteBuffer) { + FfiConverterUShort.write(value.`packetId`, buf) + FfiConverterByteArray.write(value.`reasonCodes`, buf) + } +} + + + +data class UnsubscribeResultFfi ( + var `packetId`: kotlin.UShort, + var `reasonCodes`: kotlin.ByteArray +) { + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeUnsubscribeResultFFI: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): UnsubscribeResultFfi { + return UnsubscribeResultFfi( + FfiConverterUShort.read(buf), + FfiConverterByteArray.read(buf), + ) + } + + override fun allocationSize(value: UnsubscribeResultFfi) = ( + FfiConverterUShort.allocationSize(value.`packetId`) + + FfiConverterByteArray.allocationSize(value.`reasonCodes`) + ) + + override fun write(value: UnsubscribeResultFfi, buf: ByteBuffer) { + FfiConverterUShort.write(value.`packetId`, buf) + FfiConverterByteArray.write(value.`reasonCodes`, buf) + } +} + + + +sealed class MqttEventFfi { + + data class Connected( + val v1: ConnectionResultFfi) : MqttEventFfi() { + companion object + } + + data class Disconnected( + val `reasonCode`: kotlin.UByte?) : MqttEventFfi() { + companion object + } + + data class MessageReceived( + val v1: MqttMessageFfi) : MqttEventFfi() { + companion object + } + + data class Published( + val v1: PublishResultFfi) : MqttEventFfi() { + companion object + } + + data class Subscribed( + val v1: SubscribeResultFfi) : MqttEventFfi() { + companion object + } + + data class Unsubscribed( + val v1: UnsubscribeResultFfi) : MqttEventFfi() { + companion object + } + + data class PingResponse( + val `success`: kotlin.Boolean) : MqttEventFfi() { + companion object + } + + data class Error( + val `message`: kotlin.String) : MqttEventFfi() { + companion object + } + + object ReconnectNeeded : MqttEventFfi() + + + data class ReconnectScheduled( + val `attempt`: kotlin.UInt, + val `delayMs`: kotlin.ULong) : MqttEventFfi() { + companion object + } + + + + companion object +} + +/** + * @suppress + */ +public object FfiConverterTypeMqttEventFFI : FfiConverterRustBuffer{ + override fun read(buf: ByteBuffer): MqttEventFfi { + return when(buf.getInt()) { + 1 -> MqttEventFfi.Connected( + FfiConverterTypeConnectionResultFFI.read(buf), + ) + 2 -> MqttEventFfi.Disconnected( + FfiConverterOptionalUByte.read(buf), + ) + 3 -> MqttEventFfi.MessageReceived( + FfiConverterTypeMqttMessageFFI.read(buf), + ) + 4 -> MqttEventFfi.Published( + FfiConverterTypePublishResultFFI.read(buf), + ) + 5 -> MqttEventFfi.Subscribed( + FfiConverterTypeSubscribeResultFFI.read(buf), + ) + 6 -> MqttEventFfi.Unsubscribed( + FfiConverterTypeUnsubscribeResultFFI.read(buf), + ) + 7 -> MqttEventFfi.PingResponse( + FfiConverterBoolean.read(buf), + ) + 8 -> MqttEventFfi.Error( + FfiConverterString.read(buf), + ) + 9 -> MqttEventFfi.ReconnectNeeded + 10 -> MqttEventFfi.ReconnectScheduled( + FfiConverterUInt.read(buf), + FfiConverterULong.read(buf), + ) + else -> throw RuntimeException("invalid enum value, something is very wrong!!") + } + } + + override fun allocationSize(value: MqttEventFfi) = when(value) { + is MqttEventFfi.Connected -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeConnectionResultFFI.allocationSize(value.v1) + ) + } + is MqttEventFfi.Disconnected -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterOptionalUByte.allocationSize(value.`reasonCode`) + ) + } + is MqttEventFfi.MessageReceived -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeMqttMessageFFI.allocationSize(value.v1) + ) + } + is MqttEventFfi.Published -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypePublishResultFFI.allocationSize(value.v1) + ) + } + is MqttEventFfi.Subscribed -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeSubscribeResultFFI.allocationSize(value.v1) + ) + } + is MqttEventFfi.Unsubscribed -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterTypeUnsubscribeResultFFI.allocationSize(value.v1) + ) + } + is MqttEventFfi.PingResponse -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterBoolean.allocationSize(value.`success`) + ) + } + is MqttEventFfi.Error -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterString.allocationSize(value.`message`) + ) + } + is MqttEventFfi.ReconnectNeeded -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + ) + } + is MqttEventFfi.ReconnectScheduled -> { + // Add the size for the Int that specifies the variant plus the size needed for all fields + ( + 4UL + + FfiConverterUInt.allocationSize(value.`attempt`) + + FfiConverterULong.allocationSize(value.`delayMs`) + ) + } + } + + override fun write(value: MqttEventFfi, buf: ByteBuffer) { + when(value) { + is MqttEventFfi.Connected -> { + buf.putInt(1) + FfiConverterTypeConnectionResultFFI.write(value.v1, buf) + Unit + } + is MqttEventFfi.Disconnected -> { + buf.putInt(2) + FfiConverterOptionalUByte.write(value.`reasonCode`, buf) + Unit + } + is MqttEventFfi.MessageReceived -> { + buf.putInt(3) + FfiConverterTypeMqttMessageFFI.write(value.v1, buf) + Unit + } + is MqttEventFfi.Published -> { + buf.putInt(4) + FfiConverterTypePublishResultFFI.write(value.v1, buf) + Unit + } + is MqttEventFfi.Subscribed -> { + buf.putInt(5) + FfiConverterTypeSubscribeResultFFI.write(value.v1, buf) + Unit + } + is MqttEventFfi.Unsubscribed -> { + buf.putInt(6) + FfiConverterTypeUnsubscribeResultFFI.write(value.v1, buf) + Unit + } + is MqttEventFfi.PingResponse -> { + buf.putInt(7) + FfiConverterBoolean.write(value.`success`, buf) + Unit + } + is MqttEventFfi.Error -> { + buf.putInt(8) + FfiConverterString.write(value.`message`, buf) + Unit + } + is MqttEventFfi.ReconnectNeeded -> { + buf.putInt(9) + Unit + } + is MqttEventFfi.ReconnectScheduled -> { + buf.putInt(10) + FfiConverterUInt.write(value.`attempt`, buf) + FfiConverterULong.write(value.`delayMs`, buf) + Unit + } + }.let { /* this makes the `when` an expression, which ensures it is exhaustive */ } + } +} + + + + + + +/** + * @suppress + */ +public object FfiConverterOptionalUByte: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.UByte? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterUByte.read(buf) + } + + override fun allocationSize(value: kotlin.UByte?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterUByte.allocationSize(value) + } + } + + override fun write(value: kotlin.UByte?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterUByte.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalUShort: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.UShort? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterUShort.read(buf) + } + + override fun allocationSize(value: kotlin.UShort?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterUShort.allocationSize(value) + } + } + + override fun write(value: kotlin.UShort?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterUShort.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalString: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): kotlin.String? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterString.read(buf) + } + + override fun allocationSize(value: kotlin.String?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterString.allocationSize(value) + } + } + + override fun write(value: kotlin.String?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterString.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterOptionalTypeMqttEventFFI: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): MqttEventFfi? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterTypeMqttEventFFI.read(buf) + } + + override fun allocationSize(value: MqttEventFfi?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterTypeMqttEventFFI.allocationSize(value) + } + } + + override fun write(value: MqttEventFfi?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterTypeMqttEventFFI.write(value, buf) + } + } +} + + + + +/** + * @suppress + */ +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) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceTypeMqttDatagramFFI: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeMqttDatagramFFI.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeMqttDatagramFFI.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeMqttDatagramFFI.write(it, buf) + } + } +} + + + + +/** + * @suppress + */ +public object FfiConverterSequenceTypeMqttEventFFI: FfiConverterRustBuffer> { + override fun read(buf: ByteBuffer): List { + val len = buf.getInt() + return List(len) { + FfiConverterTypeMqttEventFFI.read(buf) + } + } + + override fun allocationSize(value: List): ULong { + val sizeForLength = 4UL + val sizeForItems = value.map { FfiConverterTypeMqttEventFFI.allocationSize(it) }.sum() + return sizeForLength + sizeForItems + } + + override fun write(value: List, buf: ByteBuffer) { + buf.putInt(value.size) + value.iterator().forEach { + FfiConverterTypeMqttEventFFI.write(it, buf) + } + } +} + diff --git a/kotlin/settings.gradle.kts b/kotlin/settings.gradle.kts new file mode 100644 index 00000000..e4c376b2 --- /dev/null +++ b/kotlin/settings.gradle.kts @@ -0,0 +1,5 @@ +rootProject.name = "flowsdk-kotlin" + +include("package") +include("examples:simple_client") +include("examples:quic_client") diff --git a/python/examples/asyncio_quic_client_example.py b/python/examples/asyncio_quic_client_example.py index 760383ad..b6141882 100644 --- a/python/examples/asyncio_quic_client_example.py +++ b/python/examples/asyncio_quic_client_example.py @@ -5,8 +5,15 @@ This example shows how to use the FlowMqttClient with QUIC transport. Note: Requires a QUIC-enabled MQTT broker (e.g., EMQX with QUIC support). + +TLS Key Logging (for Wireshark): + Set the SSLKEYLOGFILE environment variable to capture TLS secrets: + SSLKEYLOGFILE=$PWD/sslkeylog.txt python asyncio_quic_client_example.py + Then open the capture in Wireshark and configure the keylog file under: + Edit > Preferences > Protocols > TLS > (Pre)-Master-Secret log filename """ import asyncio +import os from flowsdk import FlowMqttClient, TransportType @@ -19,12 +26,19 @@ async def main(): """Main example demonstrating QUIC MQTT client.""" print("🚀 Starting QUIC MQTT Client Example...") + # Check if TLS key logging is requested via environment variable + keylog_file = os.environ.get("SSLKEYLOGFILE") + enable_key_log = keylog_file is not None + if enable_key_log: + print(f"🔑 TLS key logging enabled → {keylog_file}") + # Create QUIC client client = FlowMqttClient( client_id="quic_test_client", transport=TransportType.QUIC, mqtt_version=5, insecure_skip_verify=True, # Skip TLS verification for demo + enable_key_log=enable_key_log, on_message=on_message ) diff --git a/python/examples/asyncio_tls_client_example.py b/python/examples/asyncio_tls_client_example.py index 90518f66..9865d5ee 100644 --- a/python/examples/asyncio_tls_client_example.py +++ b/python/examples/asyncio_tls_client_example.py @@ -25,12 +25,19 @@ def on_message(topic: str, payload: bytes, qos: int): if topic == "test/python/tls" and payload == b"Hello TLS!": msg_received.set() + # Check if TLS key logging is requested via environment variable + keylog_file = os.environ.get("SSLKEYLOGFILE") + enable_key_log = keylog_file is not None + if enable_key_log: + print(f"🔑 TLS key logging enabled → {keylog_file}") + # We'll use broker.emqx.io:8883 which supports TLS client = FlowMqttClient( client_id=f"python_tls_test_{int(time.time() % 10000)}", transport=TransportType.TLS, insecure_skip_verify=False, # Skip verification for simplicity in test server_name="broker.emqx.io", + enable_key_log=enable_key_log, on_message=on_message ) diff --git a/python/package/flowsdk/async_client.py b/python/package/flowsdk/async_client.py index 738f6b0f..4ede807b 100644 --- a/python/package/flowsdk/async_client.py +++ b/python/package/flowsdk/async_client.py @@ -333,7 +333,8 @@ def __init__( client_key_file: Optional[str] = None, insecure_skip_verify: bool = False, alpn_protocols: Optional[List[str]] = None, - server_name: Optional[str] = None + server_name: Optional[str] = None, + enable_key_log: bool = False ): self.transport_type = transport self.server_name = server_name @@ -355,7 +356,8 @@ def __init__( client_cert_file=client_cert_file, client_key_file=client_key_file, insecure_skip_verify=insecure_skip_verify, - alpn_protocols=alpn_protocols or ["mqtt"] + alpn_protocols=alpn_protocols or ["mqtt"], + enable_key_log=enable_key_log ) # Create appropriate engine based on transport type diff --git a/scripts/build_kotlin_bindings.sh b/scripts/build_kotlin_bindings.sh new file mode 100755 index 00000000..a3f0f0f2 --- /dev/null +++ b/scripts/build_kotlin_bindings.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -e + +# Default to debug build +PROFILE="debug" +CARGO_PROFILE="dev" +TARGET_DIR="target/debug" + +if [[ "$1" == "--release" ]]; then + PROFILE="release" + CARGO_PROFILE="release" + TARGET_DIR="target/release" + shift +fi + +# Platforms +OS="$(uname -s)" +case "${OS}" in + Linux*) EXT="so";; + Darwin*) EXT="dylib";; + CYGWIN*|MINGW*|MSYS*) EXT="dll";; # Windows-ish + *) EXT="so";; +esac + +echo "Building flowsdk_ffi ($PROFILE)..." +cargo build -p flowsdk_ffi --profile $CARGO_PROFILE --features quic + +echo "Generating Kotlin bindings..." +# Create package directory if it doesn't exist +mkdir -p kotlin/package/src/main/kotlin + +# Output direct to kotlin/package/src/main/kotlin +cargo run -p flowsdk_ffi --features=uniffi/cli,quic --bin uniffi-bindgen generate \ + --library "$TARGET_DIR/libflowsdk_ffi.$EXT" \ + --language kotlin \ + --out-dir kotlin/package/src/main/kotlin + +echo "Copying library for Kotlin package..." +# JNA looks for libraries in the resource directory or system paths +mkdir -p kotlin/package/src/main/resources +cp "$TARGET_DIR/libflowsdk_ffi.$EXT" kotlin/package/src/main/resources/ + +echo "Building Kotlin package..." +cd kotlin +# We now have a multi-module project (package + examples) +./gradlew build + +echo "Done!" diff --git a/src/mqtt_client/engine.rs b/src/mqtt_client/engine.rs index 2f89b083..9843534d 100644 --- a/src/mqtt_client/engine.rs +++ b/src/mqtt_client/engine.rs @@ -914,6 +914,14 @@ impl QuicMqttEngine { // Disable unreliable datagrams (buffer size 0 / None) let mut transport = quinn_proto::TransportConfig::default(); transport.datagram_receive_buffer_size(None); + // Set max_idle_timeout to prevent QUIC from timing out before MQTT keepalive mechanism + // Use 120 seconds to accommodate MQTT keepalive (typically 30-60s) with 2x multiplier for safety + let idle_timeout = std::time::Duration::from_secs(120) + .try_into() + .map_err(|e| MqttClientError::InternalError { + message: format!("Failed to convert QUIC idle timeout: {}", e), + })?; + transport.max_idle_timeout(Some(idle_timeout)); client_config.transport_config(Arc::new(transport)); let (ch, conn) = self