| Crates.io | waterui-ffi |
| lib.rs | waterui-ffi |
| version | 0.2.1 |
| created_at | 2025-12-14 10:06:05.132728+00 |
| updated_at | 2025-12-14 11:32:38.381895+00 |
| description | FFI bindings for the WaterUI cross-platform UI framework |
| homepage | |
| repository | https://github.com/water-rs/waterui |
| max_upload_size | |
| id | 1984140 |
| size | 427,301 |
FFI bindings layer that bridges Rust view trees to native platform backends.
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:
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.
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.
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.
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.
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.
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) }
}
}
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.
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() {
// ...
}
Native backends traverse the view tree using these core functions:
waterui_view_id(view) - Get the 128-bit type IDwaterui_view_body(view, env) - Expand composite views into their bodywaterui_force_as_<type>(view) - Downcast to specific view type (Button, Text, etc.)waterui_view_stretch_axis(view) - Query layout behaviorComposite views (user-defined) are recursively expanded via body(). Leaf views (Text, Button, Image) are downcast and mapped directly to native widgets.
WaterUI's reactive primitives (Binding, Computed) cross the FFI boundary with full functionality:
// 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)")
}
// 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.
┌─────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────┘
The components/ directory contains FFI bindings for each UI component category:
layout - HStack, VStack, ZStack, ScrollView, Spacerbutton - Button with styles (plain, bordered, link)text - Text rendering, fonts, styled textform - TextField, SecureField, Toggle, Slider, Pickernavigation - NavigationStack, TabView, NavigationLinkmedia - Image, Video, Audio, LivePhotolist - List, ForEach, LazyVStacktable - Table with columns and rowsprogress - ProgressView, ProgressIndicatorgpu_surface - High-performance wgpu rendering surfaceEach module defines:
WuiButton)IntoFFI implementations for converting Rust view configsThe 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,
}
}
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)
// }
// 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
// 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)
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 backends (Android, Apple, etc.) must follow a specific initialization sequence when rendering WaterUI views.
┌─────────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────────┘
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.
std (default) - Enable standard library supportcbindgen - Required for the generate_header binarywaterui_init() - Initialize runtime and return Environmentwaterui_app(env) - Create application from user's app() functionwaterui_env_new() - Create new Environment (alternative to init)waterui_clone_env(env) - Clone Environment for child contextswaterui_view_id(view) - Get type ID as 128-bit valuewaterui_view_body(view, env) - Expand composite view to its bodywaterui_view_stretch_axis(view) - Query layout stretch behaviorwaterui_empty_anyview() - Create empty viewwaterui_anyview_id() - Get AnyView type IDwaterui_force_as_<type>(view) - Downcast to specific view typewaterui_<type>_id() - Get type ID for comparisonwaterui_read_binding_<type>(binding) - Read current valuewaterui_set_binding_<type>(binding, value) - Update valuewaterui_watch_binding_<type>(binding, watcher) - Subscribe to changeswaterui_read_computed_<type>(computed) - Read derived valuewaterui_watch_computed_<type>(computed, watcher) - Subscribe to changeswaterui_new_computed_<type>(...) - Create native-controlled computedwaterui_drop_binding_<type>(binding) - Cleanup bindingwaterui_drop_computed_<type>(computed) - Cleanup computedwaterui_drop_<type>(ptr) - Free resource of given typewaterui_drop_retain(retain) - Drop retained valueThe FFI layer involves extensive unsafe code by necessity:
*mut T typically take ownership and will free the memorywaterui_init() must be called once on the main thread onlywaterui_force_as_*() functions assume the view type matchesNative backends are responsible for:
The FFI layer is designed for zero-overhead abstraction:
The reactive system uses reference counting (Rc) on the Rust side and lets native code subscribe via lightweight watcher callbacks.
When adding a new view type to WaterUI:
ffi/src/components/<module>.rscargo run --bin generate_header --features cbindgen --manifest-path ffi/Cargo.tomlbackends/apple) and Kotlin (backends/android)The workflow ensures Rust, C header, and native backends stay synchronized.
MIT