Building a Rust-to-Swift FFI Bridge for a Production CAD Engine

How we use Rust for heavy computation and Swift for native UI in a professional CAD application, connected through a clean FFI boundary.


Most apps don’t need two languages. But when you’re building a CAD engine that needs to process geometry at 60fps while maintaining a native macOS experience, the tradeoffs become clear: Rust for the computation core, Swift for the UI layer, and a well-defined FFI bridge connecting them.

This is how we built it for Archon-CAD, and what we learned along the way.

Why Two Languages

The CAD domain has a specific set of constraints that push you toward this architecture:

  • Geometry operations are CPU-bound. Boolean operations on meshes, NURBS evaluation, constraint solving — these need predictable performance without garbage collection pauses.
  • Users expect native UI. A CAD tool that feels like an Electron app won’t survive. macOS users expect menu bar integration, native file dialogs, proper keyboard shortcuts, and Metal-accelerated rendering.
  • The computation core needs to be portable. The same geometry engine that powers the macOS app should eventually work on Linux and Windows without rewriting anything.

Rust handles the first and third requirements. Swift handles the second. The FFI bridge is where the design decisions get interesting.

The Bridge Architecture

The naive approach is to expose every Rust function through C-compatible FFI. This gets painful fast — you’re manually managing memory, converting types at every boundary, and debugging crashes with no stack traces across the language barrier.

Instead, we use a message-passing architecture at the FFI boundary:

Swift UI Layer
    ↓ (serialized command)
FFI Bridge (thin C layer)
    ↓ (deserialized into Rust type)
Rust Engine (processes command, returns result)
    ↓ (serialized result)
FFI Bridge
    ↓ (deserialized into Swift type)
Swift UI Layer (renders)

The key insight: minimize the surface area of the FFI boundary. Instead of dozens of individual function calls, we have a small number of entry points that accept serialized commands and return serialized results.

Command Protocol

On the Rust side, commands are an enum:

#[derive(Serialize, Deserialize)]
enum CadCommand {
    CreateBody { geometry: GeometryDef },
    BooleanOp { op: BoolOp, a: BodyId, b: BodyId },
    Transform { body: BodyId, matrix: Mat4 },
    Query { body: BodyId, query: QueryType },
}

The Swift side mirrors this structure. The FFI boundary only needs to handle *const u8 byte buffers and lengths. The serialization format is MessagePack — faster than JSON, more compact, and handles binary data natively.

Memory Management

The golden rule: each language owns its own allocations. Rust allocates and frees Rust memory. Swift allocates and frees Swift memory. The FFI bridge copies data across the boundary — no shared ownership, no use-after-free, no mystery crashes.

This costs a memcopy at each crossing, but for our workload (commands are small, results are mesh vertex buffers), the copy overhead is negligible compared to the actual computation.

For large mesh data returned to Swift for rendering, we use a shared memory region mapped by both processes. But even there, ownership is clear: Rust writes, Swift reads, and a generation counter prevents tearing.

What We Learned

Start with the boundary, not the implementation. We designed the command protocol before writing either the Rust engine or the Swift UI. This let both sides develop independently against a contract.

Serialize everything at the boundary. The temptation to pass raw pointers for “performance” leads to the worst debugging sessions of your career. The serialization overhead is almost never the bottleneck.

Test the bridge in isolation. We have a test harness that sends commands through the FFI layer without any UI. This catches serialization bugs, memory leaks, and protocol mismatches before they manifest as mysterious UI crashes.

Don’t fight the type system. Both Rust and Swift have strong type systems. Use them. The command enum on the Rust side and the corresponding Swift enum are the source of truth. If they drift apart, the compiler catches it.

When This Architecture Makes Sense

This isn’t the right choice for every app. The overhead of maintaining two language ecosystems, a serialization protocol, and a build system that compiles both Rust and Swift is real.

It makes sense when:

  • You have a computation-heavy core that benefits from Rust’s performance guarantees
  • You need native platform UI that can’t be compromised
  • The core needs to be portable across platforms
  • The boundary between “computation” and “presentation” is naturally clean

It doesn’t make sense when:

  • Your app is primarily UI with light business logic
  • Performance requirements can be met by Swift alone
  • You’re building for a single platform with no portability needs

For Archon-CAD, the architecture has paid for itself many times over. The Rust core processes complex geometry operations in microseconds, the Swift UI feels indistinguishable from a first-party Apple app, and the boundary between them is the cleanest part of the codebase.