In this section

Differences between capdag (Rust) and capdag-objc (Swift/Objective-C) and how to write cartridges in each.

Module coverage

Shared modules (both languages)

Both implementations cover the same core functionality:

Module Rust location Swift location
URN types (CapUrn, MediaUrn, CapMatrix) capdag-rs/src/urn/ capdag-objc/Sources/CapDAG/
Cap definitions and validation capdag-rs/src/cap/ capdag-objc/Sources/CapDAG/
CapDAG protocol (Frame, IO) capdag-rs/src/bifaci/frame.rs, io.rs capdag-objc/Sources/Bifaci/
CartridgeRuntime capdag-rs/src/bifaci/cartridge_runtime.rs capdag-objc/Sources/Bifaci/CartridgeRuntime.swift
CartridgeHost capdag-rs/src/bifaci/host_runtime.rs capdag-objc/Sources/Bifaci/CartridgeHost.swift
Relay and RelaySwitch capdag-rs/src/bifaci/relay.rs, relay_switch.rs capdag-objc/Sources/Bifaci/Relay.swift, RelaySwitch.swift
Planner: plan model, cap graph and path finding, cardinality, argument binding and requirements, progress mapping capdag-rs/src/planner/ capdag-objc/Sources/CapDAG/Planner/
Orchestrator: resolved-graph types and the plan converter capdag-rs/src/orchestrator/types.rs, plan_converter.rs capdag-objc/Sources/Bifaci/Orchestrator/
In-process host capdag-rs/src/bifaci/in_process_host.rs capdag-objc/Sources/Bifaci/InProcessCartridgeHost.swift

Rust only

  • Orchestrator executor (capdag-rs/src/orchestrator/executor.rs, execute_plan.rs): runs a plan — process spawning, Unix socket management, relay lifecycle. This is deeply platform-dependent and not ported.
  • Machine notation (capdag-rs/src/machine/): the grammar (machine.pest), parser, resolver and serializer. Go, Python and JavaScript have it too; Swift reads plans, not notation.
  • Planner search and plan building (capdag-rs/src/planner/plan_engine.rs, plan_builder.rs): Swift has the cap graph and its path finding. Building a plan, and analyzing what a run of it is asked for, are Rust only.
  • Input resolver (capdag-rs/src/input_resolver/): File type detection and media adapters.

Swift only

  • Objective-C foundation layer (capdag-objc/Sources/CapDAG/): CSCapUrn, CSCap, etc.: C-compatible API for interoperability with Objective-C code and the macOS app shell.

Language-specific patterns

Async runtime

Aspect Rust Swift
Runtime tokio multi-threaded structured concurrency (async/await)
Entry point #[tokio::main] async fn main() @main struct, synchronous main()
Task spawning tokio::spawn(async { ... }) Task { ... }
Blocking work tokio::task::spawn_blocking(closure) Background Task + cancellation
Channels tokio::sync::mpsc AsyncStream, Foundation channels

The key architectural difference: Rust’s spawn_blocking moves work to a dedicated thread pool, explicitly freeing the async workers. Swift’s structured concurrency does not have an equivalent separation, so blocking work inside a Task can stall the cooperative thread pool. This is why the Swift keepalive uses a separate background Task rather than a thread pool handoff.

Error handling

Aspect Rust Swift
Pattern Result<T, E> with ? operator throws / try / catch
Error types thiserror derive macros Enums conforming to Error + LocalizedError
Interop — NSError for Objective-C boundaries

Both implementations define matching error variants (e.g., RuntimeError::NoHandler ↔ CartridgeRuntimeError.noHandler). The error semantics are identical: the same conditions produce the same errors.

Op registration

Rust: Factory closure returning a trait object:

runtime.register_op("cap:...", || Box::new(MyOp::default()));
// or
runtime.register_op_type::<MyOp>("cap:...");

Swift: Closure returning an Op conformer:

runtime.register_op_type(capUrn: "cap:...", make: MyOp.init)

Cap URN sourcing

Rust: Cap URNs can be extracted from the manifest at runtime via iterator lookup, or constructed with CapUrnBuilder, or obtained from standard helpers like generate_thumbnail_urn().

Swift: Cap URNs are typically hardcoded string constants. This is less dynamic but simpler for cartridges with a fixed cap set.

Concurrency primitives

Concept Rust Swift
Thread-safe sharing Arc<T> + Send + Sync traits Sendable protocol, @unchecked Sendable
Mutability Arc<Mutex<T>>, AtomicBool NSLock, actors, OSAllocatedUnfairLock
Async channels tokio::sync::mpsc, tokio::sync::oneshot AsyncStream, Foundation channels
Worker threads tokio::task::spawn_blocking No direct equivalent (use Task or GCD)

Keepalive differences

graph TD
    subgraph Rust
        R_CALL["run_with_keepalive()"] --> R_SB["spawn_blocking<br/>(blocking thread pool)"]
        R_CALL --> R_SEL["tokio::select!<br/>interval(30s)"]
        R_SEL -->|every 30s| R_PROG["Progress frame"]
        R_SB -->|complete| R_DONE["select! exits"]
    end

    subgraph Swift
        S_CALL["runWithKeepalive()"] --> S_TASK["Background Task<br/>loop { sleep(30s) }"]
        S_CALL --> S_OP["Async operation"]
        S_TASK -->|every 30s| S_PROG["Progress frame"]
        S_OP -->|complete| S_CANCEL["Cancel background Task"]
    end

Both implementations emit keepalive frames every 30 seconds during blocking operations. The mechanism differs:

Rust (run_with_keepalive):

  1. Closure runs on tokio::task::spawn_blocking (dedicated blocking thread pool).
  2. tokio::select! with tokio::time::interval(30s) emits keepalive frames.
  3. When spawn_blocking completes, the select loop exits.

Swift (runWithKeepalive):

  1. Background Task emits progress frames in a loop with Task.sleep(30s).
  2. Main async operation runs concurrently.
  3. When the main operation completes, the background Task is cancelled.

The Rust approach explicitly frees tokio workers (critical because the writer task needs them). The Swift approach relies on structured concurrency’s cooperative scheduling.

Manifest encoding

Rust: CapManifest struct serialized to JSON via serde_json::to_vec(). Passed to CartridgeRuntime::new() or CartridgeRuntime::with_manifest().

Swift: Manifest struct encoded via JSONEncoder().encode() to Data. Passed to CartridgeRuntime(manifest:).

The JSON format is identical on the wire; the host sees no difference between a Rust and Swift cartridge’s manifest.

Testing

Rust: Tests run via cargo test. The in_process_host.rs module provides InProcessCartridgeHost and FrameHandler for testing handlers without process spawning. Integration tests in integration_tests.rs test the full frame protocol.

Swift: Tests run via Xcode test targets. The capdag-objc package includes test targets for URN types, planner modules, and protocol handling.