Skip to content
Oumuamua Labs

Hekate Swift

On-device zero-knowledge proofs on iOS: one async Swift call runs your Rust prover in a detached task, and Task.cancel() reaches Rust.

hekate-mobile 0.3.0 AGPL-3.0-only source swift/ Updated swift.md
On this page

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.

The wrapper is ~30 lines. Two jobs and nothing more: run the multi-second prove in a detached task, which keeps the caller's actor free, and route Task.cancel() through the FFI CancelToken to make a cooperative cancel actually reach Rust.

§ Installation

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

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

<version> is the release in the line under the title. SwiftPM names a remote package after the last component of its URL, which makes the product's package hekate-mobile.

The package ships only the HekateProver and CancellableToken protocols and the async extension. cargo xtask ios-build writes MyProver.xcframework, HekateProverCdylib.xcframework and the Swift bindings file. The app embeds both frameworks and declares the prover type (MyProver) once over the bindings, as the package README shows.

§ Usage

swift
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, which occupies one thread of Swift's cooperative pool until it returns. .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:

  • parent-driven structured cancellation when a TaskGroup aborts,
  • view-lifecycle cancellation in SwiftUI when the host view disappears,
  • an explicit cancel() from a coordinator or Task handle.

The Rust prover stops at its next poll of the token, and the await throws 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 its next poll and returns. The witness build takes no token, and a cancel that arrives during it is not seen until the prover runs.

§ 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 circuit or witness builder, 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:

swift
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.

§ Next steps