Expand description
Cross-platform app development in Rust
Crux helps you share your app’s business logic and behavior across mobile (iOS and Android) and web, as a single, reusable core built with Rust.
Unlike React Native, the user interface layer is built natively, with modern declarative UI frameworks such as Swift UI, Jetpack Compose and React/Vue or a WASM based framework on the web.
The UI layer is as thin as it can be, and all other work is done by the shared core. The interface with the core has static type checking across languages.
§Getting Started
Crux applications are split into two parts: a Core written in Rust and a Shell written in the platform native language (e.g. Swift or Kotlin). It is also possible to use Crux from Rust shells. The Core architecture is based on Elm architecture.
Quick glossary of terms to help you follow the example:
-
Core - the shared core written in Rust
-
Shell - the native side of the app on each platform handling UI and executing side effects
-
App - the main module of the core containing the application logic, especially model changes and side-effects triggered by events. An App can delegate to child apps, mapping Events and Effects.
-
Event - main input for the core, typically triggered by user interaction in the UI
-
Model - data structure (typically tree-like) holding the entire application state
-
View model - data structure describing the current state of the user interface
-
Effect - A side-effect the core can request from the shell. This is typically a form of I/O or similar interaction with the host platform. Updating the UI is considered an effect.
-
Command - A description of a side-effect or a sequence of side-effects to be executed by the shell. Commands can be combined (synchronously with combinators, or asynchronously with Rust async) to run sequentially or concurrently, or any combination thereof.
-
Capability - A user-friendly API used to create Commands for a specific effect type (e.g. HTTP)
Below is a minimal example of a Crux-based application Core:
// src/app.rs
use crux_core::{render::{self, RenderOperation}, App, macros::effect, Command};
use serde::{Deserialize, Serialize};
// Model describing the application state
#[derive(Default)]
struct Model {
count: isize,
}
// Event describing the actions that can be taken
#[derive(Serialize, Deserialize)]
pub enum Event {
Increment,
Decrement,
Reset,
}
// Effects the Core will request from the Shell
#[effect(typegen)]
pub enum Effect {
Render(RenderOperation),
}
#[derive(Default)]
struct Hello;
impl App for Hello {
// Use the above Event
type Event = Event;
// Use the above Model
type Model = Model;
type ViewModel = String;
// Use the above generated Effect
type Effect = Effect;
fn update(&self, event: Event, model: &mut Model) -> Command<Effect, Event> {
match event {
Event::Increment => model.count += 1,
Event::Decrement => model.count -= 1,
Event::Reset => model.count = 0,
};
// Request a UI update
render::render()
}
fn view(&self, model: &Model) -> Self::ViewModel {
format!("Count is: {}", model.count)
}
}§Integrating with a Shell
To use the application from a shell, wrap the Core in a Bridge,
which presents the same interface in serialized form, so that events, effect requests and the
view model can cross the FFI boundary as bytes.
// src/ffi.rs
use crux_core::{
Core,
bridge::{Bridge, EffectId},
};
pub struct CoreFfi {
core: Bridge<Hello>,
}
impl CoreFfi {
pub fn new() -> Self {
Self {
core: Bridge::new(Core::new()),
}
}
/// Send an event to the app, returning the serialized effect requests it caused.
pub fn update(&self, event: &[u8]) -> Vec<u8> {
let mut requests = vec![];
self.core
.update(event, &mut requests)
.expect("event should deserialize");
requests
}
/// Resolve an effect request with the shell's output, returning any follow-up requests.
pub fn resolve(&self, id: u32, output: &[u8]) -> Vec<u8> {
let mut requests = vec![];
self.core
.resolve(EffectId(id), output, &mut requests)
.expect("output should deserialize");
requests
}
/// Get the current view model, serialized.
pub fn view(&self) -> Vec<u8> {
let mut view = vec![];
self.core.view(&mut view).expect("view model should serialize");
view
}
}The three methods above are the entire interface the shell sees. In a real app you would handle the errors rather than panicking on them.
The bindings which let Swift, Kotlin, TypeScript or C# call those methods are generated by
BoltFFI. Annotate the impl block with #[boltffi::export],
describe your targets in a boltffi.toml, and run boltffi pack apple (or android, wasm)
to build the library and generate the foreign code that calls it:
#[boltffi::export]
impl CoreFfi {
// ...as above
}§Type generation
The shell also needs its own definitions of the types crossing the boundary — Event,
ViewModel and the effect payloads. These are generated separately from the FFI bindings,
by deriving Facet on those types and running a codegen binary
against them, behind the facet_typegen feature. See
type_generation::facet
for details.
The counter example shows
all of this end to end, with shells written in Swift, Kotlin, TypeScript, C# and Rust.
Re-exports§
pub use command::Command;pub use type_generation::serde as typegen;pub use crux_macros as macros;
Modules§
- bridge
- capability
- command
- Command represents one or more side-effects, resulting in interactions with the shell.
- effects
- Support for routing effects to explicit, type-based handlers.
- middleware
- Middleware which can be wrapped around the Core to modify its behaviour.
- render
- Built-in capability used to notify the Shell that a UI update is necessary.
- type_
generation
Structs§
- Core
- The Crux core. Create an instance of this type with your App type as the type parameter
- Request
- Request represents an effect request from the core to the shell.
Enums§
- Request
Handle - Resolve is a callback used to resolve an effect request and continue one of the capability Tasks running on the executor.
- Resolve
Error
Traits§
- App
- Implement
Appon your type to make it into a Crux app. Use your type implementingAppas the type argument toCoreorBridge. - Effect
- Implemented automatically with the effect macro from
crux_macros. This is a marker trait to ensure the macro generated traits are present on the effect type. - EffectFFI
- Implemented automatically with the effect macro from
crux_macros. - Resolvable