Oumuamua Labs

Hekate Swift

Async / cancellation facade over the UniFFI-generated Swift bindings. Maps Swift Concurrency (Task, TaskPriority, structured cancellation) onto the synchronous prove(inputs:cancel:) C-ABI call that the prover crate exports through UniFFI.

Read the Hekate Swift sources on GitHub →

The wrapper is ~30 lines. Two jobs and nothing more: hop off the cooperative pool so a multi-second prove doesn't starve actor-bound work, and route Task.cancel() through the FFI CancelToken so a cooperative cancel actually reaches Rust.

Installation

Package.swift lives at the repository root. Add the package by repo URL:

dependencies: [
    .package(url: "https://github.com/oumuamua-labs/hekate-mobile.git", from: "0.1.0"),
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [
            .product(name: "Hekate", package: "hekate-swift"),
        ]
    )
]

The package ships only the HekateProver protocol and its async extension. The concrete prover type (MyProver, generated by cargo xtask ios-build from the dev's outer crate) and its companion HekateProverCdylib.xcframework are vendored separately.

Usage

import Hekate

let output = try await MyProver.prove(
    inputs: myInputs,
    priority: .userInitiated
)

That's the surface. No cancel tokens, no completion handlers, no DispatchQueue.global().async { ... }, no JNI-style typed-buffer plumbing. The try covers every failure mode the Rust prover can produce, the await suspends the caller until the proof is ready or the parent task is cancelled.

priority is forwarded to the detached Task that runs the synchronous FFI call. .background is the right choice for batch or off-screen proving. .userInitiated is acceptable for interactive flows but risks the iOS background-CPU killer if the app suspends mid-prove.

Cancellation

Task.cancel() on the enclosing Swift task automatically calls CancelToken.request() through withTaskCancellationHandler. That covers every form of cancellation Swift Concurrency produces:

Rust's prover loop polls the token at instruction boundaries inside the sumcheck rounds and unwinds with ProveError.Cancelled. There is no manual cancel-token plumbing on the call site, and there is no Thread.cancel() analogue in Swift to misuse.

The cancellation propagation is cooperative on both sides: Rust observes the flag at the next polling boundary, finishes any in-flight constraint evaluation, and returns. Latency from Task.cancel() to ProveError.Cancelled is bounded by the time between polls (microseconds, not milliseconds, on M-series silicon).

Panic isolation

The Rust prove entry point wraps the prover invocation in catch_unwind(AssertUnwindSafe(...)). If the prover panics for any reason, debug assertion, integer overflow in debug_assertions, a bug in the dev's impl Air<Block128>, catch_unwind captures it, formats the panic payload, and returns Err(ProveError::Panic(message)). UniFFI maps that variant to a typed Swift enum ProveError case:

do {
    let output = try await MyProver.prove(inputs: myInputs)
} catch ProveError.Panic(let message) {
    // log + surface; the prove call did NOT corrupt your process
} catch ProveError.Cancelled {
    // user-initiated or structured cancel
} catch {
    // every other typed prover error variant
}

No abort() crosses the FFI boundary. The Rust panic unwinds locally inside catch_unwind, the FFI return frame is intact, and the Swift caller sees a typed error instead of a SIGABRT on the next stack frame.