Getting Started¶
Provision the NNUE network, create the engine, and exchange UCI.
1. Provision the NNUE network¶
The NNUE network (v54-5478683c.nnue) is not embedded in the engine binary —
it is loaded from disk at runtime. The engine returns nil on initialization if the
file is missing or unreadable, so always ensure the net is present before creating
the engine.
RecklessNetworkLoader.ensure(in:) is idempotent: a valid, present net is never
re-downloaded (it verifies the complete SHA-256 on Apple, Linux, and Android,
then returns immediately). Only a missing or invalid file triggers a download.
import Foundation
import SwiftReckless
let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
let netDir = support.appendingPathComponent("reckless-nets")
try await RecklessNetworkLoader().ensure(in: netDir) { progress in
if let fraction = progress.fractionCompleted {
print("Downloading net: \(Int(fraction * 100))%")
} else {
print("Downloading net: \(progress.bytesDownloaded) bytes")
}
}
The progress closure is called only when a download is in progress. On a warm
launch where the net is already present and valid, ensure returns immediately
without calling the closure.
Progress fires once — not a live counter
The loader uses URLSession.downloadTask(with:completionHandler:), which
reports progress inside the completion handler. The closure is called once,
at download completion, with bytesDownloaded == totalBytes (or 0 when the
server omitted Content-Length). It always prints a terminal 100%; it does
not count up incrementally. Add a URLSessionDownloadDelegate if you need
live streaming progress.
2. Create the engine¶
RecklessEngine.init(networkDirectory:) is failable — it returns nil if the
NNUE net is absent from networkDirectory or if the Rust FFI layer could not start
the engine thread.
guard let engine = RecklessEngine(networkDirectory: netDir) else {
fatalError("engine failed to start — net missing or Rust FFI error")
}
One live engine per process
The Rust engine owns process-global state, so SwiftReckless rejects an
overlapping engine with a nil initializer result. Pinned fork tag
swiftreckless-v0.9.1 makes initialization and teardown restart-safe:
after a clean shutdown(), a later engine lifetime may start normally.
3. Read output and send commands¶
engine.cancellationSafeOutput is a cancellation-safe channel of UCI lines,
delivered in order, with no trailing newline. Breaking out of one for await loop
leaves the channel reusable for later reads — which is exactly what the sequential
steps below rely on. Output is buffered, so it is safe to send a command first and
start reading afterwards.
engine.uci() // sends "uci"; engine replies with id/option lines then "uciok"
for await line in engine.cancellationSafeOutput {
print("engine>", line)
if line == "uciok" { engine.isReady() }
if line == "readyok" { break } // engine is ready; proceed below
}
output is single-consumer — pick one surface per engine
engine.output (an AsyncStream<String>) is a compatibility shim for the
shared UCIEngine protocol. It is single-consumer and non-restartable:
breaking out of its loop ends it permanently, and even a first access starts a
forwarding consumer that competes with cancellationSafeOutput for lines. Use
cancellationSafeOutput for stop-and-resume reads like the ones on this page,
and never consume both surfaces on one engine.
4. Run a search¶
Once readyok is received, set a position and start a search. The engine emits
info lines with search progress, then a final bestmove line.
engine.setPosition(fen: "startpos")
engine.go(depth: 20)
for await line in engine.cancellationSafeOutput {
if line.hasPrefix("bestmove ") {
let move = line.split(separator: " ").dropFirst().first.map(String.init)
print("Best move:", move ?? "none")
break
}
}
5. Tear down¶
engine.shutdown() // sends "quit", joins the Rust thread, frees state, finishes output
// deinit also calls shutdown() if explicit teardown was omitted.
Full minimal example¶
import Foundation
import SwiftReckless
@main struct MinimalReckless {
static func main() async throws {
let support = FileManager.default.urls(for: .applicationSupportDirectory,
in: .userDomainMask)[0]
let netDir = support.appendingPathComponent("reckless-nets")
// 1. Ensure the NNUE net is present.
try await RecklessNetworkLoader().ensure(in: netDir)
// 2. Create the engine.
guard let engine = RecklessEngine(networkDirectory: netDir) else {
fatalError("engine init failed")
}
// 3. Handshake then search.
engine.uci()
for await line in engine.cancellationSafeOutput {
if line == "uciok" { engine.isReady() }
if line == "readyok" {
engine.setPosition(fen: "startpos")
engine.go(depth: 18)
}
if line.hasPrefix("bestmove ") {
print(line)
engine.shutdown()
break
}
}
}
}
API summary¶
public final class RecklessEngine: @unchecked Sendable {
public init?(networkDirectory: URL)
/// Cancellation-safe, process-lifetime UCI output. Prefer this surface for
/// any consumer that stops and restarts reads.
public var cancellationSafeOutput: RecklessOutput { get }
/// Compatibility shim for the shared `UCIEngine` protocol; single-consumer
/// and non-restartable. Mutually exclusive with `cancellationSafeOutput` —
/// pick one surface per engine.
public var output: AsyncStream<String> { get }
public func send(_ command: String)
public func uci()
public func isReady()
public func newGame()
public func quit()
public func setPosition(fen: String = "startpos", moves: [String] = [])
public func goInfinite()
public func go(depth: Int)
public func go(wtime: Int, btime: Int, winc: Int = 0, binc: Int = 0)
public func stop()
}
See also¶
- Engine API — full reference for
RecklessEngine. - NNUE Network Loader — the loader in depth.
- Build model — Apple XCFramework vs. Android source build.
- Usage Examples — task-oriented code samples.