|
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. |