Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/LiveSplit/livesplit-core/llms.txt

Use this file to discover all available pages before exploring further.

The Swift binding uses C interop — Swift’s built-in ability to call C functions through a module map. The binding generator produces two targets that work together:
TargetContentsPurpose
CLiveSplitCorelivesplit_core.h, module.modulemap, stub .cExposes the C API as a Swift module
LiveSplitCoreLiveSplitCore.swiftIdiomatic Swift wrapper classes
The generated output lives in capi/bindings/swift/.

Module map

The CLiveSplitCore/include/module.modulemap file tells the Swift compiler where to find the C header and which system frameworks to link:
module CLiveSplitCore [extern_c] {
    header "livesplit_core.h"
    link "livesplit_core"
    link framework "Carbon"
    link framework "CoreFoundation"
    link framework "CoreGraphics"
    export *
}
LiveSplitCore.swift begins with import CLiveSplitCore and calls the C functions directly through this module.

Class structure

The generator produces three Swift classes per type, reflecting the Rust ownership model:
public class TimerRef {
    var ptr: UnsafeMutableRawPointer?
    // shared-borrow methods (read-only)
}

public class TimerRefMut: TimerRef {
    // mutable-borrow methods
}

public class Timer: TimerRefMut {
    private func drop() {
        if self.ptr != nil {
            CLiveSplitCore.Timer_drop(self.ptr)
            self.ptr = nil
        }
    }
    deinit { self.drop() }
    public func dispose() { self.drop() }
    // static factory / owned methods
}
The owned class (Timer, Run, etc.) has a deinit that calls the C drop function. You can also call dispose() eagerly if you want deterministic cleanup before the object is deallocated by ARC.

Integration steps

Swift Package Manager

  1. Build the static library for your target:
    # Example: macOS arm64
    cargo rustc --release --target aarch64-apple-darwin \
        -p livesplit-core-capi --crate-type staticlib
    
  2. Generate the Swift bindings:
    cd capi/bind_gen
    cargo run
    # Output: capi/bindings/swift/LiveSplitCore/ and capi/bindings/swift/CLiveSplitCore/
    
  3. Copy both target directories into your Swift package or Xcode project.
  4. Add the static library to the linker path in your Package.swift or Xcode build settings.

Xcode project

  1. Add capi/bindings/swift/LiveSplitCore/LiveSplitCore.swift to your target.
  2. Add the CLiveSplitCore folder (header + module map + stub .c) as a separate target or a bridging header group.
  3. In Build Settings → Library Search Paths, add the folder containing liblivesplit_core.a.

Example

import LiveSplitCore

// Run_new is non-nullable → init(ptr:) is called directly; ptr is never nil
let run = Run()
run.setGameName("Celeste")
run.setCategoryName("Any%")

// Segment_new is non-nullable → constructor
run.pushSegment(Segment("Prologue"))

// Timer_new returns a nullable pointer → init?() returns Timer?
guard let timer = Timer(run) else {
    fatalError("Run must have at least one segment")
}

// Control the timer
timer.start()
timer.split()
timer.reset(true) // true = update splits / save attempt

// Dispose eagerly (deinit would also handle this)
timer.dispose()

Memory management

  • The deinit on owned classes calls the C drop function automatically when the Swift ARC reference count reaches zero.
  • Call dispose() if you need the native memory freed before the Swift object is deallocated (e.g. in a tight loop).
  • TimerRef and TimerRefMut are non-owning references — never call dispose() on them directly.
  • After passing an owned object to a consuming function (e.g. Timer(run)), the binding zeroes run.ptr so further calls on run throw instead of corrupting memory.

Build docs developers (and LLMs) love