Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

UniFFI

The UniFFI layer is a boundary adapter.

Rust app types do not have to be FFI-safe. The FFI crate defines explicit foreign-facing event, view, effect, and output enums, then converts between those enums and the Rust protocol.

Events and Views

Events are plain UniFFI enums:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, uniffi::Enum)]
pub enum AccountFfiEvent {
    StartLogin { email: String, password: String },
    SubmitSecondFactor { code: String },
    Logout,
}

#[derive(Debug, Clone, uniffi::Enum)]
pub enum FfiEvent {
    Account(AccountFfiEvent),
    Counter(CounterFfiEvent),
}
}

Views are also plain UniFFI records and enums:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, uniffi::Enum)]
pub enum FfiViewModel {
    LoggedOut {
        account: AccountFfiViewModel,
    },
    LoggedIn {
        account: AccountFfiViewModel,
        counter: CounterFfiViewModel,
    },
}
}

The exported object keeps the same app shape:

#![allow(unused)]
fn main() {
#[uniffi::export]
impl TeaApp {
    #[uniffi::constructor]
    pub fn new(handler: Arc<dyn FfiEffectHandler>) -> Arc<Self> {
        Arc::new(Self {
            app: app_tea::TeaApp::new(Arc::new(ForeignEffectHandler { inner: handler })),
        })
    }

    pub async fn update(&self, event: FfiEvent) {
        self.app.update(event.into()).await;
    }

    pub fn view(&self) -> FfiViewModel {
        self.app.view().into()
    }
}
}

Foreign Effect Handler

The foreign host implements one async handler:

#![allow(unused)]
fn main() {
#[uniffi::export(with_foreign)]
#[async_trait::async_trait]
pub trait FfiEffectHandler: Send + Sync {
    async fn handle(&self, effect: FfiEffect) -> FfiEffectOutput;
}
}

The adapter converts Rust AppEffect into FfiEffect, awaits the host, and converts FfiEffectOutput back into AppEffectOutput:

#![allow(unused)]
fn main() {
#[async_trait::async_trait]
impl EffectHandler<AppEffect> for ForeignEffectHandler {
    async fn handle(&self, effect: AppEffect) -> AppEffectOutput {
        self.inner.handle(effect.into()).await.into()
    }
}
}

This is a direct request/response boundary. The host does not poll for pending effects and does not send a later response tagged with an effect ID. The awaited return value is the response for that effect call.

Explicit FFI Protocols

The FFI protocol is intentionally explicit:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, uniffi::Enum)]
pub enum FfiEffect {
    Account(AccountFfiEffect),
    Counter(CounterFfiEffect),
}

#[derive(Debug, Clone, uniffi::Enum)]
pub enum FfiEffectOutput {
    Account(AccountFfiEffectOutput),
    Counter(CounterFfiEffectOutput),
}
}

That costs a small amount of conversion code, but it keeps the generated foreign API reviewable and stable. The Rust app model is not forced to become whatever a type generator can infer.