Functions and objects¶
Functions¶
Every exported function becomes a top-level Kotlin function in the bindings' package. Names are
converted to lowerCamelCase.
#[uniffi::export]
pub fn greet(name: String) -> String {
format!("Hello, {name}!")
}
greet("Kotlin") // "Hello, Kotlin!"
Default arguments¶
Arguments can have defaults, which become Kotlin default arguments. List them in default(...).
Either give a value (sep = ","), or just the name (max_splits) to use the type's own default,
which is None for an Option:
#[uniffi::export(default(sep = ",", max_splits))]
pub fn split(text: String, sep: String, max_splits: Option<u32>) -> Vec<String> {
match max_splits {
Some(n) => text.splitn(n as usize + 1, sep.as_str()).map(String::from).collect(),
None => text.split(sep.as_str()).map(String::from).collect(),
}
}
fun split(text: String, sep: String = ",", maxSplits: UInt? = null): List<String>
split("a,b,c") // ["a", "b", "c"]
split("a;b;c", ";", maxSplits = 1u) // ["a", "b;c"]
Constructors and methods take the same attribute: #[uniffi::constructor(default(...))] and
#[uniffi::method(default(...))].
Objects¶
An object is a Rust struct that lives on the Rust side. Kotlin holds a reference to it and calls its methods across the FFI. Unlike records, it is never copied.
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;
#[derive(uniffi::Object)]
pub struct Counter {
value: AtomicI32,
}
#[uniffi::export]
impl Counter {
#[uniffi::constructor]
pub fn new(start: i32) -> Self {
Self { value: AtomicI32::new(start) }
}
#[uniffi::constructor]
pub fn from_string(text: String) -> Arc<Self> {
Arc::new(Self::new(text.parse().unwrap_or(0)))
}
pub fn increment(&self) -> i32 {
self.value.fetch_add(1, Ordering::SeqCst) + 1
}
}
The generator produces an interface with the methods, and a class implementing it:
interface CounterInterface {
fun increment(): Int
}
open class Counter : Disposable, CounterInterface {
constructor(start: Int) // the constructor named `new`
companion object {
fun fromString(text: String): Counter // every other constructor
}
override fun increment(): Int
override fun destroy()
override fun close()
}
val counter = Counter(41)
counter.increment() // 42
- The constructor named
newbecomes the Kotlin constructor.#[uniffi::constructor(name = "new")]makes any constructor the primary one. - All other constructors become functions on the
companion object. - An
asyncprimary constructor can't be a Kotlin constructor, so no constructor is generated for it. Use a named async constructor instead, which becomes asuspend funon the companion. - Methods must take
&selforself: Arc<Self>. The object is shared, so it must beSend + Syncand use interior mutability where it changes.
Objects can be passed to and returned from functions, and stored in records, lists, maps and optionals.
Lifetime¶
Each Kotlin object holds a reference to the Rust object. The reference is released either explicitly or when the Kotlin object is garbage collected.
// Explicitly
counter.destroy()
// Scoped, like Closeable.use
Counter(0).use { c ->
c.increment()
}
Generated objects implement Disposable, which extends AutoCloseable. close() and destroy()
do the same thing.
- After
destroy(), every method call throwsIllegalStateException.destroy()itself can be called any number of times. - A call that is in progress when
destroy()is called completes normally. The Rust object is freed when the last running call finishes. - If you never call
destroy(), a cleaner frees the Rust object some time after the Kotlin object becomes unreachable. That is fine for small objects. For objects holding significant resources (file handles, large buffers), release them explicitly. - Records and enums that contain objects also implement
Disposable, and theirdestroy()destroys the objects they hold.
Interfaces and naming¶
| Rust declaration | Kotlin interface | Kotlin class |
|---|---|---|
#[derive(uniffi::Object)] struct Foo |
FooInterface |
Foo |
#[uniffi::export] trait Foo (Rust-only trait) |
FooInterface |
Foo |
#[uniffi::export(with_foreign)] trait Foo |
Foo |
FooImpl |
For the last row, users are expected to implement Foo in Kotlin, so the interface gets the
short name. See Callbacks and trait interfaces.
Fakes in tests¶
Every object has a constructor that takes NoHandle. It creates an instance without a Rust object
behind it, which is useful as a base for test fakes:
class FakeCounter : Counter(NoHandle) {
override fun increment(): Int = 7
}
Any call that reaches the Rust side through such an instance fails.