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):
- Closure runs on
tokio::task::spawn_blocking(dedicated blocking thread pool). tokio::select!withtokio::time::interval(30s)emits keepalive frames.- When
spawn_blockingcompletes, the select loop exits.
Swift (runWithKeepalive):
- Background
Taskemits progress frames in a loop withTask.sleep(30s). - Main async operation runs concurrently.
- When the main operation completes, the background
Taskis 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.