Cross-platform app development in Rust
Crux ·

Watch the introductory talk | Read the book | Read API docs | Join Zulip community
Cross-platform app development in Rust
- Shared Core for Behavior - Crux helps you share your app's business logic
- Thin Shell for UI - Crux recognizes that the best experiences are built
- Type Generation - the interface with the core has static type checking
- Managed effects - Side effects such as calling an API are captured as values
Getting Started
Start with the book, then work through the examples in order. If you prefer a talk first, watch the introductory Rust Nation 2023 talk.
Read the API documentation.
You can also join the friendly conversation on our Zulip channel.
[!NOTE]
Crux is pre-1.0 and under active development. It is production-ready, but
occasional breaking changes to the API can be expected. We do our best to
limit the extent of these and provide a smooth, gradual migration path
Architectural Overview
Crux has managed side-effects. It strictly separates pure computational tasks from tasks that cause side effects. This is similar to the way Elm works.
Side-effect-free core
In the above diagram, the inner "Core" is compiled and linked to the outer "Shell" on each platform as a library:
- On iOS/macOS as a native static library packaged with BoltFFI Swift bindings
- On Android as a dynamic library packaged with BoltFFI Kotlin bindings and native assets
- In a browser as a WebAssembly module
As such, the core is completely isolated and secure against software supply-chain attacks, as it has no access to any external APIs. All it can do is perform pure calculations and keep internal state.
Following the Elm architecture, the core defines the key component types within the application:
Eventβ anenumdescribing the events which the core can handleModelβ describes the internal state of the applicationEffectβ the kinds of side-effects the core will requestViewModelβ represents information that should be displayed to the user
update function, familiar from Elm,
Redux or other event sourcing architectures, which has this type
signature:
,ignore
fn update(
&self,
msg: Event,
model: &mut Model,
) -> Command<Effect, Event> {
// ...
}
The job of the update function is to process an Event, update the model accordingly, and potentially request some side-effects.
[!NOTE]
To learn more about the Command API, see the page in the book that describes Managed Effects,
or look at the examples:>
1. Counter β get the basics
1. Counter (HTTP) β understand how to talk to your API
1. Weather β see how a real app works
1. Notes β real-time collaboration
1. Counter (Middleware) β use middleware for Rust-side effects
1. Counter (Routing) β use effect routing for type-based, per-effect dispatching
Application Shell
The enclosing platform native "Shell" is written using the language appropriate for the platform, and acts as the runtime environment within which all the non-pure tasks are performed. From the perspective of the core, the shell is the platform on which the core runs.
Testing
Tests can act as another Shell, exercising the Core in the same way a real app would, observing and resolving the requested effects and checking the model and view model are correct. No need for fakes, mocks or stubs.
Communication Between the Application Shell and the Core
Following the Elm architecture, the interface with the core is message based. To perform any task that creates a side-effect (such as an HTTP call or random number generation), the core must request it from the shell as an Effect. Effects support fire-and-forget, request/response, and streaming semantics.
Crux has a concept of Capabilities β reusable interfaces for common side-effects which can be used in the Core as a more ergonomic API.
The only built-in capability is Render. But this repository contains a few capabilities at various stages of maturity, and you can easily write your own if you want to:

Published capabilities
Render(ask UI to render the ViewModel) β
crux_core,
request only
Http(full HTTP implementation) β source,
KeyValue(basic key-value store API) β source,
Time(get current time, notify after duration, notify at instant) β
Example custom capabilities
SSE(basic Server-Sent Events) β
PubSub(pub sub with streaming) β
Foreign Function Interface with type generation
The core API interface is very minimal:
process_event: Event -> Vec<Request>- processes a user interaction event
handle_response: (uuid, SomeResponse) -> Vec<Request>- handles the response
view: () -> ViewModel- provides the shell with the current data for
The Foreign Function Interface allowing the shell to call the above functions is generated by BoltFFI across native, web, and C# shells.
In order to keep the host-facing API consistent across platforms and enforce the message passing semantics, all messages are serialized, sent across the boundary, then deserialized using Bincode. Type definitions and serialization code for the foreign (non-Rust) languages are generated by facet-generate.
This means that changes to types in the core, especially the Event and Request types, propagate out into the shell implementations and cause type errors where appropriate (such as an exhaustive match on an enum check).
Message Types
Three types of message are exchanged between the application and the core.
- Messages of type
Eventare sent from the Shell to the Core in response to an
- Messages of type
Requestare sent from the Core to the Shell to request the
Request messages after receiving an Event message (the driven
side).
- Response messages are sent from the Shell to the Core carrying the result of
Request messages contain the inputs for the requested side-effect, along with an id used by the core to pair requests and their responses together. The exact mechanics are not important, but it is important for the request's id to be passed on to the corresponding response.
Example Message Exchange Cycle
A typical message exchange cycle may look like this:
- User interaction occurs in the Shell, which results in an event
- The Shell handles this event by constructing an
Event - The Shell calls the Core's
process_eventfunction passing theEventas an
- The Core performs the required processing, updating both its inner state and
- The Core returns one or more
Requestmessages to the Shell (inside an enum
In the simplest case, the Core will respond to an Event by returning the single Request - render.
This requests that the Shell re-renders the user interface. When Render is the only response from the Core, the message cycle has completed and the Core has now "settled".
In more complex cases however, the Core may well return multiple Requests; each of which instructs the Shell to perform a side-effect-inducing task such as:
- Make a network call, or
- Fetch the current date/time stamp, or
- Perform biometric authentication, or
- Obtain an image from the camera, or
- Whatever else you can think of...
handle_response
function), which may respond with further requests.
This exchange continues until the core stops requesting further side-effects (typically the last side-effect requested would again be Render).
Talks & Articles
- iOS, Android and Web applications that share a single Rust core β Stuart Harris β Rust Nation, February 2023
- Retiring React Native for Rust β Viktor Charypar β React Native London, August 2023
- Scaling Large Organisations: Empowering Independent Teams with Crux Micro-Frontends β Stuart Harris and Ludovico Rossi at Proton β with accompanying blog post
- Building live collaboration in Rust for millions of users β PhotoRoom engineering blog series:
Sponsors
Crux is kindly sponsored by the following organizations. Your help is very much appreciated.
Red Badger Consulting Limited
Red Badger is the digital product consultancy trusted by blue chips and global brands. Our product design and technical pedigree allow us to craft high-impact digital products customers want. We use modern engineering approaches to deliver sustainable change. And embed digital capabilities to power continuous innovation.
Zulip
Zulip is an open-source modern team chat app designed to keep both live and asynchronous conversations organized.
Zulip sponsors Crux by providing our Zulip server βΒ thank you Zulip!