01 / Make some code disappear
Before abracadabra was something you said over a top hat, it was something you wore around your neck. In the ancient Roman Liber Medicinalis, attributed to Quintus Serenus Sammonicus, the prescription was to write the word repeatedly, removing a letter each time, and wear the result as a charm against fever. An unusually literal approach to making a problem smaller.
If you've shipped iOS, Android, and web versions of something, you know the usual routine. Build the feature in Swift. Build it again in Kotlin. Build it again in JavaScript. Each version gets its own validation, requests, loading state, and error handling. They talk to the same backend, so we call them three clients of one product. We still wrote much of the product three times.
Then the feature changes. iOS learns how to recover from a failed save. Android gets the fix a week later. The browser handles the same error differently. Nobody set out to design three behaviors. They just accumulated while everyone was doing normal development.
I want cubacadabra's clients to become thin shells around Rust. Keep the minimum Swift, Kotlin, and JavaScript needed for native controls, device services, and browser integration. Move the portable application logic into shared Rust crates. That includes the boring account screens as well as the game engine. DRY, don't repeat yourself, should apply to the decisions the application makes.
A username field can still be a SwiftUI text field, a Compose text field, or an HTML input. Each forwards edits to Rust and displays the resulting state. Rust decides whether Save is enabled, which request to make, and what its response means. Changing the rule changes one implementation. The three screens can keep looking like they belong on their platforms.
There are good precedents. Litter has native iOS and Android interfaces over a Rust core that owns session state, streaming, and reconnect behavior, exposed through UniFFI bindings. Mozilla's shared Rust components grew out of maintaining separate sync implementations for Firefox's desktop and mobile apps. The reason will sound familiar: duplicated logic was hard to maintain, and differences between implementations caused bugs.
02 / Is all this worth a save button?
I worked through this in a long architecture discussion and the iOS development notes. A senior mobile developer could reasonably ask why a username field needs Rust, a C ABI, JNI, and WASM. Separate native implementations are easier to debug in their own IDEs. Bindings add build work and lifetime bugs. For a small app with a few forms, duplication could be cheaper.
For cubacadabra, I'm convinced the shared core is worth it. Rust already runs the engine and desktop Studio, and the browser already runs it through WASM. As the platform grows, joining a game will involve content compatibility, parental permissions, subscription access, blocked players, and recovery when a connection dies halfway through. I want to work out those interactions once. I don't want every new rule to become a coordination problem between three implementations.
... continue reading