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:
- parent-driven structured cancellation when a
TaskGroupaborts, - view-lifecycle cancellation in SwiftUI when the host view disappears,
- an explicit
cancel()from a coordinator orTaskhandle.
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.