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.