| 1 | --- |
| 2 | name: uniffi-rs |
| 3 | description: Generate Swift, Kotlin, and Python bindings from Rust using UniFFI. Use when adding, modifying, or debugging FFI-exported types and functions in the onde crate. |
| 4 | allowed-tools: Read, Write, Edit, Glob, Grep |
| 5 | user-invocable: false |
| 6 | --- |
| 7 | |
| 8 | # uniffi-rs |
| 9 | |
| 10 | ## What This Skill Covers |
| 11 | |
| 12 | How UniFFI is used in the `onde` crate to generate Swift, Kotlin, and Python bindings from a single Rust implementation. |
| 13 | |
| 14 | ## Version Pin |
| 15 | |
| 16 | UniFFI is pinned to exactly `=0.31.0` everywhere. This version must stay in sync across: |
| 17 | - `onde/Cargo.toml` → `uniffi = { version = "=0.31.0", features = ["tokio"] }` |
| 18 | - `onde/Cargo.toml` build-deps → `uniffi = { version = "=0.31.0", features = ["build"] }` |
| 19 | - `onde/uniffi-bindgen/Cargo.toml` → `uniffi = { version = "=0.31.0", features = ["cli"] }` |
| 20 | |
| 21 | Never bump one without bumping all three. Mismatched versions produce silently broken bindings. |
| 22 | |
| 23 | ## Project Structure |
| 24 | |
| 25 | ``` |
| 26 | onde/ |
| 27 | ├── src/lib.rs # uniffi::setup_scaffolding!() |
| 28 | ├── src/inference/ |
| 29 | │ ├── types.rs # #[derive(uniffi::Record/Enum/Error)] |
| 30 | │ └── ffi.rs # #[derive(uniffi::Object)], #[uniffi::export] |
| 31 | ├── uniffi-bindgen/ |
| 32 | │ ├── Cargo.toml # Standalone bin crate for the bindgen CLI |
| 33 | │ └── uniffi-bindgen.rs # fn main() { uniffi::uniffi_bindgen_main() } |
| 34 | ├── uniffi.toml # Binding config (package names, cdylib name) |
| 35 | └── build.rs # uniffi build support (build-dep) |
| 36 | ``` |
| 37 | |
| 38 | ## Scaffolding Setup |
| 39 | |
| 40 | `src/lib.rs` uses the proc-macro approach: |
| 41 | |
| 42 | ```rust |
| 43 | uniffi::setup_scaffolding!(); |
| 44 | ``` |
| 45 | |
| 46 | This is equivalent to the UDL approach but requires no `.udl` file. The `build.rs` still declares the `uniffi` build-dep to keep compatibility if the project ever switches to UDL. |
| 47 | |
| 48 | ## Type Annotations |
| 49 | |
| 50 | | Rust construct | UniFFI derive | Crosses FFI as | |
| 51 | |---|---|---| |
| 52 | | Plain data struct | `#[derive(uniffi::Record)]` | Swift `struct` / Kotlin `data class` | |
| 53 | | C-like / rich enum | `#[derive(uniffi::Enum)]` | Swift `enum` / Kotlin `sealed class` | |
| 54 | | `thiserror::Error` enum | `#[derive(uniffi::Error)]` | Swift `Error enum` / Kotlin exception | |
| 55 | | `Arc<T>` wrapper struct | `#[derive(uniffi::Object)]` | Swift `class` / Kotlin `object` | |
| 56 | | Trait for callbacks | `#[uniffi::export(callback_interface)]` | Swift `protocol` / Kotlin `interface` | |
| 57 | |
| 58 | All exported functions and impl blocks are annotated with `#[uniffi::export]`. |
| 59 | |
| 60 | ## Type Compatibility Rules |
| 61 | |
| 62 | These constraints come directly from UniFFI 0.31 limitations: |
| 63 | |
| 64 | - **`usize` / `isize` are not supported** — use `u64` / `i64` instead. |
| 65 | - **`impl Into<String>` is not FFI-safe** — use concrete `String` in all exported method signatures. |
| 66 | - **`tokio::sync::mpsc::Receiver` is not FFI-safe** — streaming is exposed via a `callback_interface` trait delivered through a free function, not an Object method. |
| 67 | - **`callback_interface` parameters must be on free functions** — UniFFI 0.31 does not support `callback_interface` arguments on `#[uniffi::Object]` methods. |
| 68 | - **Generics are not supported** — all exported types must be fully concrete. |
| 69 | |
| 70 | ## The uniffi-bindgen CLI |
| 71 | |
| 72 | `onde/uniffi-bindgen/` is a minimal standalone binary crate: |
| 73 | |
| 74 | ```rust |
| 75 | // uniffi-bindgen.rs |
| 76 | fn main() { |
| 77 | uniffi::uniffi_bindgen_main() |
| 78 | } |
| 79 | ``` |
| 80 | |
| 81 | Build it once and run it to generate bindings: |
| 82 | |
| 83 | ```bash |
| 84 | cargo build --manifest-path uniffi-bindgen/Cargo.toml --release |
| 85 | |
| 86 | # Generate Swift |
| 87 | ./target/release/uniffi-bindgen generate \ |
| 88 | --library target/aarch64-apple-tvos/release/libonde.a \ |
| 89 | --language swift \ |
| 90 | --out-dir sdk/Onde/Sources/Onde \ |
| 91 | --config uniffi.toml |
| 92 | |
| 93 | # Generate Kotlin |
| 94 | ./target/release/uniffi-bindgen generate \ |
| 95 | --library target/aarch64-linux-android/release/libonde.so \ |
| 96 | --language kotlin \ |
| 97 | --out-dir sdk/kotlin/onde/src/main/kotlin \ |
| 98 | --config uniffi.toml |
| 99 | |
| 100 | # Generate Python |
| 101 | ./target/release/uniffi-bindgen generate \ |
| 102 | --library target/release/libonde.dylib \ |
| 103 | --language python \ |
| 104 | --out-dir sdk/python \ |
| 105 | --config uniffi.toml |
| 106 | ``` |
| 107 | |
| 108 | The `--library` argument points to a compiled artifact (`.a`, `.so`, or `.dylib`). UniFFI reads the embedded metadata from the binary — any compiled artifact from any platform works for generating bindings for any language. |
| 109 | |
| 110 | ## uniffi.toml |
| 111 | |
| 112 | ```toml |
| 113 | [bindings.kotlin] |
| 114 | package_name = "com.ondeinference.onde" |
| 115 | cdylib_name = "onde" |
| 116 | |
| 117 | [bindings.python] |
| 118 | cdylib_name = "onde" |
| 119 | ``` |
| 120 | |
| 121 | No Swift-specific config is needed here. `cargo-swift` (when used) and the raw `uniffi-bindgen` both derive Swift names from the Rust struct/function names. |
| 122 | |
| 123 | ## Async Support |
| 124 | |
| 125 | UniFFI 0.31 supports `async fn` exports when the crate enables the `tokio` feature: |
| 126 | |
| 127 | ```toml |
| 128 | uniffi = { version = "=0.31.0", features = ["tokio"] } |
| 129 | ``` |
| 130 | |
| 131 | All `async fn` methods on `#[uniffi::Object]` and all `async` free functions annotated with `#[uniffi::export]` are automatically bridged to Swift `async throws` and Kotlin `suspend fun`. |
| 132 | |
| 133 | ## Streaming Pattern |
| 134 | |
| 135 | Because `callback_interface` is not allowed on Object methods in UniFFI 0.31, streaming is exposed as a top-level free function that accepts the Object by `Arc<T>`: |
| 136 | |
| 137 | ```rust |
| 138 | // CORRECT — free function with Arc<Object> |
| 139 | #[uniffi::export] |
| 140 | pub async fn stream_chat_message( |
| 141 | engine: Arc<OndeChatEngine>, |
| 142 | message: String, |
| 143 | listener: Box<dyn StreamChunkListener>, // callback_interface |
| 144 | ) -> Result<(), InferenceError> { ... } |
| 145 | |
| 146 | // WRONG — callback_interface on Object method (not supported in 0.31) |
| 147 | // impl OndeChatEngine { |
| 148 | // pub async fn stream(&self, listener: Box<dyn StreamChunkListener>) { ... } |
| 149 | // } |
| 150 | ``` |
| 151 | |
| 152 | ## Adding a New Exported Type |
| 153 | |
| 154 | 1. Define the type in `src/inference/types.rs` (or whichever module owns it). |
| 155 | 2. Add the appropriate `#[derive(uniffi::Record)]` / `#[derive(uniffi::Enum)]` / `#[derive(uniffi::Error)]`. |
| 156 | 3. Replace any `usize` fields with `u64`, and any `isize` with `i64`. |
| 157 | 4. If the type needs to be exported from the FFI Object, add it to the method signatures in `src/inference/ffi.rs`. |
| 158 | 5. Recompile for any Apple target and re-run `uniffi-bindgen generate` for Swift. |
| 159 | 6. Re-run `build-kotlin.sh` for Kotlin. |
| 160 | |
| 161 | ## Common Pitfalls |
| 162 | |
| 163 | - **Version mismatch**: If the `uniffi` version in `onde/Cargo.toml` differs from the one in `onde/uniffi-bindgen/Cargo.toml`, the generated bindings will be incompatible with the compiled library at runtime. Always keep them identical with the `=` exact pin. |
| 164 | - **Forgetting `setup_scaffolding!()`**: Without `uniffi::setup_scaffolding!()` in `lib.rs`, no scaffolding code is generated and all `#[uniffi::export]` annotations are silently ignored. |
| 165 | - **`usize` in struct fields**: UniFFI will produce a hard compile error or a confusing runtime panic if `usize` appears in any exported type. Always use `u64`. |
| 166 | - **Callback interface in Object method**: This compiles fine but panics at runtime in UniFFI 0.31. Move the method to a free function. |
| 167 | - **Not rebuilding the bindgen binary**: If you update `uniffi` in the main `Cargo.toml` without rebuilding `uniffi-bindgen`, the CLI will use the old schema and generate stale/broken bindings. |