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 new becomes the Kotlin constructor. #[uniffi::constructor(name = "new")] makes any constructor the primary one.
  • All other constructors become functions on the companion object.
  • An async primary constructor can't be a Kotlin constructor, so no constructor is generated for it. Use a named async constructor instead, which becomes a suspend fun on the companion.
  • Methods must take &self or self: Arc<Self>. The object is shared, so it must be Send + Sync and 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 throws IllegalStateException. 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 their destroy() 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.

Fixtures