Cactus for Swift Multiplatform¶
Run AI models on-device with a simple Swift API on iOS, macOS, and Android.
Building¶
Build outputs (in apple/):
see the main README.md for how to use CLI & download weight
| File | Description |
|---|---|
cactus-ios.xcframework/ |
iOS framework (device + simulator) |
cactus-macos.xcframework/ |
macOS framework |
libcactus-device.a |
Static library for iOS device |
libcactus-simulator.a |
Static library for iOS simulator |
For Android, build libcactus.so from the android/ directory.
Vendored libcurl (iOS + macOS)¶
To bundle libcurl from this repo instead of relying on system curl, place artifacts under:
libs/curl/include/curl/*.hlibs/curl/ios/device/libcurl.alibs/curl/ios/simulator/libcurl.alibs/curl/macos/libcurl.a
Build scripts auto-detect libs/curl. Override with:
Integration¶
iOS/macOS: XCFramework (Recommended)¶
- Drag
cactus-ios.xcframework(orcactus-macos.xcframework) into your Xcode project - Ensure "Embed & Sign" is selected in "Frameworks, Libraries, and Embedded Content"
- Copy
Cactus.swiftinto your project
iOS/macOS: Static Library¶
- Add
libcactus-device.a(orlibcactus-simulator.a) to "Link Binary With Libraries" - Create a folder with
cactus_ffi.handmodule.modulemap, add to Build Settings: - "Header Search Paths" → path to folder
- "Import Paths" (Swift) → path to folder
- Copy
Cactus.swiftinto your project
Android (Swift SDK)¶
Requires Swift SDK for Android.
- Copy files to your Swift project:
libcactus.so→ your library pathcactus_ffi.h→ your include pathmodule.android.modulemap→ rename tomodule.modulemapin include path-
Cactus.swift→ your sources -
Build with Swift SDK for Android:
-
Bundle
libcactus.sowith your APK injniLibs/arm64-v8a/
Usage¶
Handles are typed as CactusModelT, CactusIndexT, and CactusStreamTranscribeT (all UnsafeMutableRawPointer aliases).
Basic Completion¶
import Foundation
let model = try cactusInit("/path/to/model", nil, false)
defer { cactusDestroy(model) }
let messages = #"[{"role":"user","content":"What is the capital of France?"}]"#
let resultJson = try cactusComplete(model, messages, nil, nil, nil)
// resultJson is a JSON string: {"response":"Paris","success":true,...}
if let data = resultJson.data(using: .utf8),
let result = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
print(result["response"] as? String ?? "")
}
Completion with Options and Streaming¶
let options = #"{"max_tokens":256,"temperature":0.7}"#
let resultJson = try cactusComplete(model, messages, options, nil) { token, _ in
print(token, terminator: "")
}
Audio Transcription¶
// From file
let result = try cactusTranscribe(model, "/path/to/audio.wav", "", nil, nil as ((String, UInt32) -> Void)?, nil as Data?)
// From PCM data (16 kHz mono)
let pcmData: Data = ...
let result = try cactusTranscribe(model, nil, nil, nil, nil as ((String, UInt32) -> Void)?, pcmData)
Streaming Transcription¶
let stream = try cactusStreamTranscribeStart(model, nil as String?)
let partial = try cactusStreamTranscribeProcess(stream, audioChunk)
let final_ = try cactusStreamTranscribeStop(stream)
Embeddings¶
let embedding = try cactusEmbed(model, "Hello, world!", true)
let imageEmbedding = try cactusImageEmbed(model, "/path/to/image.jpg")
let audioEmbedding = try cactusAudioEmbed(model, "/path/to/audio.wav")
Tokenization¶
let tokens = try cactusTokenize(model, "Hello, world!")
let scores = try cactusScoreWindow(model, tokens, 0, tokens.count, min(tokens.count, 512))
VAD¶
RAG¶
Vector Index¶
let index = try cactusIndexInit("/path/to/index", 384)
defer { cactusIndexDestroy(index) }
try cactusIndexAdd(index, [Int32(1), Int32(2)], ["doc1", "doc2"],
[[0.1, 0.2, ...], [0.3, 0.4, ...]], nil)
let results = try cactusIndexQuery(index, [0.1, 0.2, ...], nil)
// results is a JSON string: {"results":[{"id":1,"score":0.99,...},...]}
try cactusIndexDelete(index, [2])
try cactusIndexCompact(index)
API Reference¶
All functions are top-level and mirror the C FFI directly.
Types¶
public typealias CactusModelT = UnsafeMutableRawPointer
public typealias CactusIndexT = UnsafeMutableRawPointer
public typealias CactusStreamTranscribeT = UnsafeMutableRawPointer
All throws functions throw NSError (domain "cactus") on failure.
Init / Lifecycle¶
func cactusInit(_ modelPath: String, _ corpusDir: String?, _ cacheIndex: Bool) throws -> CactusModelT
func cactusDestroy(_ model: CactusModelT)
func cactusReset(_ model: CactusModelT)
func cactusStop(_ model: CactusModelT)
func cactusGetLastError() -> String
Completion¶
func cactusComplete(
_ model: CactusModelT,
_ messagesJson: String,
_ optionsJson: String?,
_ toolsJson: String?,
_ callback: ((String, UInt32) -> Void)?
) throws -> String
Transcription¶
func cactusTranscribe(
_ model: CactusModelT,
_ audioPath: String?,
_ prompt: String?,
_ optionsJson: String?,
_ callback: ((String, UInt32) -> Void)?,
_ pcmData: Data?
) throws -> String
func cactusStreamTranscribeStart(_ model: CactusModelT, _ optionsJson: String?) throws -> CactusStreamTranscribeT
func cactusStreamTranscribeProcess(_ stream: CactusStreamTranscribeT, _ pcmData: Data) throws -> String
func cactusStreamTranscribeStop(_ stream: CactusStreamTranscribeT) throws -> String
Embeddings¶
func cactusEmbed(_ model: CactusModelT, _ text: String, _ normalize: Bool) throws -> [Float]
func cactusImageEmbed(_ model: CactusModelT, _ imagePath: String) throws -> [Float]
func cactusAudioEmbed(_ model: CactusModelT, _ audioPath: String) throws -> [Float]
Tokenization / Scoring¶
func cactusTokenize(_ model: CactusModelT, _ text: String) throws -> [UInt32]
func cactusScoreWindow(_ model: CactusModelT, _ tokens: [UInt32], _ start: Int, _ end: Int, _ context: Int) throws -> String
VAD / RAG¶
func cactusVad(_ model: CactusModelT, _ audioPath: String?, _ optionsJson: String?, _ pcmData: Data?) throws -> String
func cactusRagQuery(_ model: CactusModelT, _ query: String, _ topK: Int) throws -> String
Vector Index¶
func cactusIndexInit(_ indexDir: String, _ embeddingDim: Int) throws -> CactusIndexT
func cactusIndexDestroy(_ index: CactusIndexT)
func cactusIndexAdd(_ index: CactusIndexT, _ ids: [Int32], _ documents: [String], _ embeddings: [[Float]], _ metadatas: [String]?) throws
func cactusIndexDelete(_ index: CactusIndexT, _ ids: [Int32]) throws
func cactusIndexGet(_ index: CactusIndexT, _ ids: [Int32]) throws -> String
func cactusIndexQuery(_ index: CactusIndexT, _ embedding: [Float], _ optionsJson: String?) throws -> String
func cactusIndexCompact(_ index: CactusIndexT) throws
Telemetry¶
func cactusSetTelemetryEnvironment(_ cacheDir: String)
func cactusSetAppId(_ appId: String)
func cactusTelemetryFlush()
func cactusTelemetryShutdown()
Requirements¶
Apple Platforms: - iOS 14.0+ / macOS 13.0+ / tvOS 14.0+ / watchOS 7.0+ - Xcode 14.0+ - Swift 5.7+
Android: - Swift 6.0+ with Swift SDK for Android - Android NDK 27d+ - Android API 28+ / arm64-v8a
See Also¶
- Cactus Engine API — Full C API reference underlying the Swift bindings
- Cactus Index API — Vector database API for RAG applications
- Fine-tuning Guide — Deploy custom fine-tunes to iOS/macOS
- Kotlin/Android SDK — Kotlin alternative for Android
- Flutter SDK — Cross-platform alternative using Dart