Records and enums

Records and enums are passed by value: every time one crosses the FFI it is serialised into a buffer and rebuilt on the other side. Changing a record in Kotlin does not change anything in Rust.

Records

#[derive(uniffi::Record)]
pub struct Person {
    pub name: String,
    pub age: u32,
    pub email: Option<String>,
}
data class Person(
    var name: String,
    var age: UInt,
    var email: String?,
)

Records become data classes, so you get copy, equals, hashCode and destructuring.

Default values

Field defaults become constructor defaults:

#[derive(uniffi::Record)]
pub struct Settings {
    pub name: String,
    #[uniffi(default = 42)]
    pub retries: i32,
    #[uniffi(default = [])]
    pub tags: Vec<String>,
    #[uniffi(default = None)]
    pub proxy: Option<String>,
    #[uniffi(default)]          // the type's own default: 0, "", false, empty list, ...
    pub verbose: bool,
}
data class Settings(
    var name: String,
    var retries: Int = 42,
    var tags: List<String> = listOf(),
    var proxy: String? = null,
    var verbose: Boolean = false,
)

Immutable records

Record fields are var by default. To generate val fields instead, set this in uniffi.toml:

generate_immutable_records = true

Individual records can keep their var fields:

generate_immutable_records = true
mutable_records = ["Cursor", "Draft"]

mutable_records uses the record name as declared in Rust or UDL. Names that don't match a record are ignored.

Enums

A Rust enum without fields becomes a Kotlin enum class:

#[derive(uniffi::Enum)]
pub enum Animal {
    Dog,
    Cat,
}
enum class Animal {
    DOG,
    CAT;
}

Explicit discriminants are kept and exposed as value:

#[derive(uniffi::Enum)]
#[repr(u64)]
pub enum AnimalLargeUInt {
    Dog = 4294967298,
    Cat = 4294967299,
}
enum class AnimalLargeUInt(val value: ULong) {
    DOG(4294967298u),
    CAT(4294967299u);
}

An enum with fields becomes a sealed class with one subclass per variant. Variants without fields are objects, the others data classes:

#[derive(uniffi::Enum)]
pub enum Shape {
    Empty,
    Circle { radius: f64 },
    Rectangle(f64, f64),
}
sealed class Shape {
    object Empty : Shape()
    data class Circle(val radius: Double) : Shape()
    data class Rectangle(val v1: Double, val v2: Double) : Shape()
}

when (shape) {
    Shape.Empty -> 0.0
    is Shape.Circle -> PI * shape.radius * shape.radius
    is Shape.Rectangle -> shape.v1 * shape.v2
}

Unnamed fields are called v1, v2, and so on. Variant fields are always val.

Variant fields can have defaults with #[uniffi(default = ...)], just like record fields.

Methods on records and enums

Records and enums can have exported methods. In Kotlin they are ordinary member functions:

#[derive(uniffi::Record)]
pub struct Greeting {
    pub text: String,
}

#[uniffi::export]
impl Greeting {
    fn shout(&self) -> String {
        self.text.to_uppercase()
    }

    async fn shout_later(&self) -> String {
        self.text.to_uppercase()
    }
}
val greeting = Greeting("hello")
greeting.shout()          // "HELLO"
greeting.shoutLater()     // suspend

Each call serialises the receiver and sends the copy to Rust, so a method can't change the Kotlin value it was called on. To derive toString, equals, hashCode and compareTo from Rust trait implementations, see UniFFI traits.

Records and enums that hold objects

When a record or enum contains an object, the generated class also implements Disposable, and its destroy() releases the objects it holds.

Fixtures