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.