waterui-ffi

Crates.iowaterui-ffi
lib.rswaterui-ffi
version0.2.1
created_at2025-12-14 10:06:05.132728+00
updated_at2025-12-14 11:32:38.381895+00
descriptionFFI bindings for the WaterUI cross-platform UI framework
homepage
repositoryhttps://github.com/water-rs/waterui
max_upload_size
id1984140
size427,301
Lexo Liu (lexoliu)

documentation

README

waterui-ffi

FFI bindings layer that bridges Rust view trees to native platform backends.

Overview

waterui-ffi is the C FFI layer at the heart of WaterUI's cross-platform architecture. It provides a type-safe, efficient bridge between Rust application logic and native UI backends, enabling WaterUI apps to render as true native widgets rather than custom-drawn pixels.

This crate serves three critical roles:

  1. Entry Point Generation: The export!() macro generates the C ABI entry points (waterui_init, waterui_app) that native backends call to initialize and run Rust applications.

  2. Type Conversion: Implements IntoFFI and IntoRust traits to safely convert between Rust types (views, reactive values, colors, fonts) and C-compatible representations that can cross the FFI boundary.

  3. C Header Generation: Uses cbindgen to automatically generate waterui.h, which is consumed by Swift (Apple backend) and Kotlin/JNI (Android backend) to understand the FFI contract.

The FFI layer is designed to work in no_std environments and minimizes unsafe code through carefully designed abstractions.

Installation

Add to your Cargo.toml:

[dependencies]
waterui = "0.2"
waterui-ffi = "0.1"

For applications, you typically only need the export!() macro - all other FFI details are handled internally by WaterUI.

Quick Start

Every WaterUI application uses the FFI layer to expose itself to native platforms:

use waterui::prelude::*;
use waterui::app::App;

// Your application entry point
pub fn app(env: Environment) -> App {
    App::new(main, env)
}

fn main() -> impl View {
    text("Hello, WaterUI!")
}

// This macro generates waterui_init() and waterui_app() FFI entry points
waterui_ffi::export!();

The export!() macro expands to:

#[no_mangle]
pub unsafe extern "C" fn waterui_init() -> *mut WuiEnv {
    // Initialize runtime, logging, executors
    let env = waterui::Environment::new();
    env.into_ffi()
}

#[no_mangle]
pub unsafe extern "C" fn waterui_app(env: *mut WuiEnv) -> WuiApp {
    let env = env.into_rust();
    let app = app(env);  // Call your app() function
    app.into_ffi()
}

Native backends then call these functions to initialize and retrieve the root view tree.

Core Concepts

FFI Conversion Traits

The crate defines two fundamental traits for crossing the FFI boundary:

IntoFFI - Converts Rust types to FFI-compatible representations:

pub trait IntoFFI: 'static {
    type FFI: 'static;
    fn into_ffi(self) -> Self::FFI;
}

// Example: Converting a String to a C-compatible byte array
impl IntoFFI for Str {
    type FFI = WuiStr;
    fn into_ffi(self) -> Self::FFI {
        WuiStr(WuiArray::new(self))
    }
}

IntoRust - Safely converts FFI types back to Rust types:

pub trait IntoRust {
    type Rust;
    unsafe fn into_rust(self) -> Self::Rust;
}

// Example: Converting C byte array back to String
impl IntoRust for WuiStr {
    type Rust = Str;
    unsafe fn into_rust(self) -> Self::Rust {
        let bytes = unsafe { self.0.into_rust() };
        unsafe { Str::from_utf8_unchecked(bytes) }
    }
}

Opaque Types

For types where the internal structure isn't relevant to native code, use the OpaqueType trait:

impl OpaqueType for Environment {}

// Automatically implements:
// - IntoFFI converting to *mut WuiEnv
// - IntoRust converting from *mut WuiEnv

This pattern is used for Environment, AnyView, Binding<T>, and Computed<T>, which native code treats as opaque pointers.

Type Identification

The FFI layer uses WuiTypeId for O(1) type identification across the FFI boundary:

#[repr(C)]
pub struct WuiTypeId {
    pub low: u64,
    pub high: u64,
}

In normal builds, this wraps Rust's TypeId. In hot reload builds (with waterui_hot_reload_lib cfg), it uses a 128-bit FNV-1a hash of the type name, ensuring type IDs remain stable across dylib reloads.

Native backends use type IDs to determine which view type they're rendering:

let viewId = waterui_view_id(view)

if viewId == waterui_text_id() {
    let textConfig = waterui_force_as_text(view)
    return Text(textConfig.content.get())
} else if viewId == waterui_button_id() {
    // ...
}

View Rendering Protocol

Native backends traverse the view tree using these core functions:

  1. waterui_view_id(view) - Get the 128-bit type ID
  2. waterui_view_body(view, env) - Expand composite views into their body
  3. waterui_force_as_<type>(view) - Downcast to specific view type (Button, Text, etc.)
  4. waterui_view_stretch_axis(view) - Query layout behavior

Composite views (user-defined) are recursively expanded via body(). Leaf views (Text, Button, Image) are downcast and mapped directly to native widgets.

Reactive System FFI

WaterUI's reactive primitives (Binding, Computed) cross the FFI boundary with full functionality:

Binding (Read/Write Reactive State)

// Rust side
let counter = Binding::int(42);

// FFI functions generated by ffi_binding! macro:
// - waterui_read_binding_i32(binding) -> i32
// - waterui_set_binding_i32(binding, value)
// - waterui_watch_binding_i32(binding, watcher) -> guard
// - waterui_drop_binding_i32(binding)

Native code can read, write, and subscribe to changes:

let binding: UnsafeMutablePointer<WuiBinding<Int32>>
let value = waterui_read_binding_i32(binding)
waterui_set_binding_i32(binding, value + 1)

// Watch for changes
let guard = waterui_watch_binding_i32(binding) { newValue, metadata in
    print("Counter changed to \(newValue)")
}

Computed (Read-Only Derived State)

// Rust side
let doubled = counter.map(|n| n * 2);

// FFI functions generated by ffi_computed! macro:
// - waterui_read_computed_i32(computed) -> i32
// - waterui_watch_computed_i32(computed, watcher) -> guard
// - waterui_clone_computed_i32(computed) -> computed
// - waterui_drop_computed_i32(computed)

Computed values can also be created from native code using waterui_new_computed_<type>(), enabling native-driven reactivity.

Architecture

Data Flow

┌─────────────────────────────────────────────────────┐
│ Rust Application (waterui)                         │
│ - View tree definition                             │
│ - Reactive state (Binding, Computed)               │
│ - Business logic                                    │
└─────────────────┬───────────────────────────────────┘
                  │ .into_ffi()
                  ▼
┌─────────────────────────────────────────────────────┐
│ FFI Layer (waterui-ffi)                            │
│ - Entry points: waterui_init(), waterui_app()      │
│ - Type conversion: IntoFFI, IntoRust               │
│ - View traversal: waterui_view_id(), _body()       │
│ - Reactive primitives: Binding, Computed FFI       │
└─────────────────┬───────────────────────────────────┘
                  │ C ABI (waterui.h)
                  ▼
┌─────────────────────────────────────────────────────┐
│ Native Backend (Swift/Kotlin)                      │
│ - Apple: UIView/NSView                             │
│ - Android: Android View                            │
│ - Maps Rust views to platform widgets               │
└─────────────────────────────────────────────────────┘

Component FFI Modules

The components/ directory contains FFI bindings for each UI component category:

  • layout - HStack, VStack, ZStack, ScrollView, Spacer
  • button - Button with styles (plain, bordered, link)
  • text - Text rendering, fonts, styled text
  • form - TextField, SecureField, Toggle, Slider, Picker
  • navigation - NavigationStack, TabView, NavigationLink
  • media - Image, Video, Audio, LivePhoto
  • list - List, ForEach, LazyVStack
  • table - Table with columns and rows
  • progress - ProgressView, ProgressIndicator
  • gpu_surface - High-performance wgpu rendering surface

Each module defines:

  1. C-compatible struct representations (e.g., WuiButton)
  2. IntoFFI implementations for converting Rust view configs
  3. FFI view macros generating type ID and downcast functions

Helper Macros

The crate provides several code generation macros to reduce boilerplate:

opaque!(Name, RustType, ident) - Generate opaque pointer FFI for a type:

opaque!(WuiEnv, waterui::Environment, env);
// Generates: WuiEnv wrapper, IntoFFI, IntoRust, waterui_drop_env()

ffi_view!(RustView, FFIStruct, ident) - Generate view FFI functions:

ffi_view!(ButtonConfig, WuiButton, button);
// Generates: waterui_button_id(), waterui_force_as_button()

ffi_reactive!(Type, FFIType, ident) - Generate binding + computed FFI:

ffi_reactive!(i32, i32, i32);
// Generates: read/write/watch/drop for both Binding<i32> and Computed<i32>

into_ffi!{} - Derive IntoFFI for structs and enums:

into_ffi! {
    ButtonStyle,
    pub enum WuiButtonStyle {
        Plain, Bordered, Link,
    }
}

Examples

Creating a New FFI View Type

To add FFI support for a new view component:

// 1. Define the Rust view config (in components/controls/src/rating.rs)
pub struct RatingConfig {
    pub value: Computed<f32>,
    pub max: f32,
    pub color: Color,
}

// 2. Add FFI bindings (in ffi/src/components/controls.rs)
use crate::{IntoFFI, reactive::WuiComputed, color::WuiColor};

into_ffi! {
    RatingConfig,
    pub struct WuiRating {
        value: *mut WuiComputed<f32>,
        max: f32,
        color: *mut WuiColor,
    }
}

ffi_view!(RatingConfig, WuiRating, rating);

// 3. Regenerate waterui.h
// cargo run --bin generate_header --features cbindgen --manifest-path ffi/Cargo.toml

// 4. Implement in Swift (backends/apple/Sources/WaterUI/Views/Rating.swift)
// if viewId == waterui_rating_id() {
//     let config = waterui_force_as_rating(view)
//     return RatingView(config: config)
// }

Implementing Custom Reactive Properties

// Generate FFI for a custom type in reactive state
use waterui_color::Color;

ffi_reactive!(Color, *mut WuiColor, color);
// Now Binding<Color> and Computed<Color> can cross FFI

Creating Native-Controlled Computed Values

// Swift side creates a computed that Rust can read
let computed = waterui_new_computed_i32(
    dataPtr,
    { ptr in return getCurrentValue(ptr) },
    { ptr, watcher in return watchValue(ptr, watcher) },
    { ptr in cleanup(ptr) }
)

// Pass to Rust view
let text = waterui_text(computed)

C Header Generation

The crate includes a generate_header binary that uses cbindgen to produce waterui.h:

cargo run --bin generate_header --features cbindgen --manifest-path ffi/Cargo.toml

This generates the C header and automatically copies it to:

  • backends/apple/Sources/CWaterUI/include/waterui.h (SwiftUI backend)
  • backends/android/runtime/src/main/cpp/waterui.h (Android backend)

The header is checked into version control, and CI verifies it's always up-to-date with the Rust code.

Native Backend Render Pipeline

Native backends (Android, Apple, etc.) must follow a specific initialization sequence when rendering WaterUI views.

Required Sequence

┌─────────────────────────────────────────────────────────────────────┐
│ 1. waterui_init()                                                   │
│    - Initializes panic hooks and global executors                   │
│    - Returns an Environment pointer                                 │
│    - MUST be called first before any other waterui_* functions      │
├─────────────────────────────────────────────────────────────────────┤
│ 2. Theme installation (recommended)                                 │
│    - Install appearance: waterui_theme_install_color_scheme()       │
│    - Install colors:     waterui_theme_install_color()              │
│    - Install fonts:      waterui_theme_install_font()               │
│    - Legacy: waterui_env_install_theme() is deprecated              │
├─────────────────────────────────────────────────────────────────────┤
│ 3. waterui_app(env)                                                 │
│    - Creates the application from user's app(env) function          │
│    - Returns WuiApp with windows and environment                    │
│    - MUST be called AFTER waterui_init() and theme installation     │
├─────────────────────────────────────────────────────────────────────┤
│ 4. Render Loop (for each view)                                      │
│    a. waterui_view_id(view) → Get the type ID                       │
│    b. Check if it's a "raw view" (Text, Button, etc.)               │
│       - If raw: waterui_force_as_*(view) → Extract native data      │
│       - If composite: waterui_view_body(view, env) → Get body view  │
│    c. Render the native widget or recurse into body                 │
└─────────────────────────────────────────────────────────────────────┘

Raw Views vs Composite Views

WaterUI distinguishes between two kinds of views:

  • Raw Views: Leaf components that map directly to native widgets. Examples: Text, Button, Color, TextField, Toggle, Slider, Stepper, Progress, Spacer, Picker, ScrollView.

  • Composite Views: User-defined views that have a body() method returning other views. When you encounter a view that isn't in the raw view registry, call waterui_view_body(view, env) to get its body and continue rendering recursively.

Features

  • std (default) - Enable standard library support
  • cbindgen - Required for the generate_header binary

API Overview

Core Entry Points

  • waterui_init() - Initialize runtime and return Environment
  • waterui_app(env) - Create application from user's app() function
  • waterui_env_new() - Create new Environment (alternative to init)
  • waterui_clone_env(env) - Clone Environment for child contexts

View Traversal

  • waterui_view_id(view) - Get type ID as 128-bit value
  • waterui_view_body(view, env) - Expand composite view to its body
  • waterui_view_stretch_axis(view) - Query layout stretch behavior
  • waterui_empty_anyview() - Create empty view
  • waterui_anyview_id() - Get AnyView type ID

Type Downcasting

  • waterui_force_as_<type>(view) - Downcast to specific view type
  • waterui_<type>_id() - Get type ID for comparison

Reactive Primitives

  • waterui_read_binding_<type>(binding) - Read current value
  • waterui_set_binding_<type>(binding, value) - Update value
  • waterui_watch_binding_<type>(binding, watcher) - Subscribe to changes
  • waterui_read_computed_<type>(computed) - Read derived value
  • waterui_watch_computed_<type>(computed, watcher) - Subscribe to changes
  • waterui_new_computed_<type>(...) - Create native-controlled computed
  • waterui_drop_binding_<type>(binding) - Cleanup binding
  • waterui_drop_computed_<type>(computed) - Cleanup computed

Memory Management

  • waterui_drop_<type>(ptr) - Free resource of given type
  • waterui_drop_retain(retain) - Drop retained value

Safety Considerations

The FFI layer involves extensive unsafe code by necessity:

  1. Pointer Validity: Callers must ensure pointers from FFI functions remain valid for their usage
  2. Ownership Transfer: Functions taking *mut T typically take ownership and will free the memory
  3. Thread Safety: waterui_init() must be called once on the main thread only
  4. Type Downcasting: waterui_force_as_*() functions assume the view type matches

Native backends are responsible for:

  • Properly managing the lifetimes of FFI pointers
  • Calling drop functions when resources are no longer needed
  • Not using pointers after they've been consumed

Performance

The FFI layer is designed for zero-overhead abstraction:

  • Type IDs: O(1) comparison (128-bit integer equality)
  • View Traversal: Single pointer dereference per level
  • Reactive Updates: Direct function pointer callbacks, no allocation
  • Arrays: Zero-copy views into Rust data via vtable-based slicing

The reactive system uses reference counting (Rc) on the Rust side and lets native code subscribe via lightweight watcher callbacks.

Development Workflow

When adding a new view type to WaterUI:

  1. Define the Rust view struct in the appropriate component crate
  2. Add FFI bindings in ffi/src/components/<module>.rs
  3. Regenerate the C header: cargo run --bin generate_header --features cbindgen --manifest-path ffi/Cargo.toml
  4. Implement the native renderer in Swift (backends/apple) and Kotlin (backends/android)
  5. Update tests to verify FFI contract

The workflow ensures Rust, C header, and native backends stay synchronized.

License

MIT

Commit count: 183

cargo fmt