Skip to content

sdks/swift: Moss iOS SDK#259

Open
HarshaNalluru wants to merge 2 commits into
mainfrom
harshan/ios-support
Open

sdks/swift: Moss iOS SDK#259
HarshaNalluru wants to merge 2 commits into
mainfrom
harshan/ios-support

Conversation

@HarshaNalluru
Copy link
Copy Markdown
Contributor

@HarshaNalluru HarshaNalluru commented May 22, 2026

Adds sdks/swift/ alongside the existing sdks/javascript and sdks/python so iOS consumers can install the Moss SDK via SwiftPM:

.package(url: "https://github.com/usemoss/moss", from: "0.1.0")
  • Package.swift at root (SwiftPM requires this).
  • sdks/swift/Sources/Moss/ — Swift wrapper.
  • sdks/swift/{README.md, LICENSE}.

The xcframework binary ships as a release asset, verified by SHA-256 in Package.swift.

Next

Once merged: tag v0.1.0 on main + cut release with Moss.xcframework.zip.

The Swift wrapper for the Moss iOS SDK. Consumers add the package via
SwiftPM:

    .package(url: "https://github.com/usemoss/moss", from: "0.1.0")

`Package.swift` lives at the repo root (SwiftPM requires this) and wires
two targets:
  - `MossC` — `.binaryTarget(url:, checksum:)` pointing at the
    `Moss.xcframework.zip` GitHub Release asset on this repo.
  - `Moss` — Swift sources under `sdks/swift/Sources/Moss/`.

The xcframework binary itself is not committed; it ships as a release
asset and Xcode verifies the SHA-256 checksum on download. Bump the URL
+ checksum together on every release.

Mirrors the layout of sdks/javascript and sdks/python — distribution
artifact published elsewhere (npm / PyPI for those; GitHub Releases
here), `README.md` + license stay in-tree.
Copilot AI review requested due to automatic review settings May 22, 2026 16:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Swift Package Manager (SPM) distribution and a Swift wrapper layer for the Moss iOS SDK, exposing an idiomatic async/await API over the precompiled MossC xcframework.

Changes:

  • Adds a root Package.swift defining a Moss library target that depends on a remotely-hosted binary MossC target.
  • Introduces the Swift wrapper implementation (MossClient), authentication protocol + trampoline, error type, and public value types.
  • Adds Swift SDK documentation and license files under sdks/swift/.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
Package.swift Declares the SPM package, product, binary target URL/checksum, and Swift target path.
sdks/swift/Sources/Moss/MossClient.swift Implements the Swift async API over the MossC ABI, including auth callback plumbing and helper utilities.
sdks/swift/Sources/Moss/MossTypes.swift Defines public Swift structs/enums for request options and returned values.
sdks/swift/Sources/Moss/MossError.swift Defines MossError surfaced from native failures.
sdks/swift/Sources/Moss/Authenticator.swift Defines the Authenticator protocol and internal boxing used for the C callback.
sdks/swift/README.md Installation and usage documentation for the Swift SDK.
sdks/swift/LICENSE Adds BSD 2-Clause license for the Swift SDK distribution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +28 to +31
/// Serializes mutations to `handle` / `authUserData` so a concurrent
/// close() can't free state out from under an in-flight operation that
/// has already captured the handle.
private let handleLock = NSLock()
Comment on lines +530 to +539
_ body: (UnsafePointer<UnsafePointer<CChar>?>) throws -> R
) rethrows -> R {
let copies = strings.map { strdup($0) }
defer { copies.forEach { free($0) } }
let ptrs = UnsafeMutablePointer<UnsafePointer<CChar>?>.allocate(capacity: copies.count)
defer { ptrs.deallocate() }
for (i, c) in copies.enumerated() {
ptrs[i] = UnsafePointer(c)
}
return try body(ptrs)
Comment on lines +6 to +15
/// The native runtime calls [getAuthHeader] whenever it needs a fresh bearer
/// token. Implementations typically fetch from a server endpoint and cache.
///
/// Tokens returned must be the raw bearer token (no `Bearer ` prefix).
///
/// Implementations must be safe to call from any thread; the native side may
/// invoke from a background worker.
public protocol Authenticator: AnyObject, Sendable {
func getAuthHeader() async throws -> String
}
public struct QueryResult: Sendable {
public let id: String
public let score: Float
public let text: String
Comment on lines +128 to +135
let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
guard let cacheRoot = caches.first else { return }
let dir = cacheRoot.appendingPathComponent("moss-models", isDirectory: true)
do {
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
} catch {
return
}
Comment thread sdks/swift/README.md Outdated

## Requirements

- iOS 15.1+
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +410 to +415
private func requireHandle() throws -> OpaquePointer {
guard let h = handle else {
throw MossError(code: -1, message: "MossClient already closed")
}
return h
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Race condition: requireHandle() reads handle without holding handleLock, enabling use-after-free

The requireHandle() method at line 410-415 reads the handle property without acquiring handleLock, while close() at line 85-95 frees and nils the handle under the lock. This creates a use-after-free race: Thread A calls an operation (e.g., query()), requireHandle() returns a valid pointer h; Thread B concurrently calls close(), which acquires the lock, calls moss_client_free(h), and sets handle = nil; Thread A's Task.detached then uses the now-freed pointer, causing undefined behavior or a crash.

The class is declared @unchecked Sendable (promising thread safety), its documentation states "The underlying native client is thread-safe — you can share a single MossClient across the app," and the comment on handleLock (sdks/swift/Sources/Moss/MossClient.swift:29-30) explicitly states its purpose is to prevent concurrent close() from freeing state out from under an in-flight operation. Yet requireHandle() doesn't participate in that locking scheme.

Prompt for agents
The requireHandle() method at sdks/swift/Sources/Moss/MossClient.swift:410-415 reads self.handle without holding handleLock, while close() mutates handle under the lock. This creates a TOCTOU race where close() can free the native handle between requireHandle() returning and the Task.detached closure using it.

A minimal fix would be to acquire handleLock inside requireHandle(). However, that alone doesn't prevent close() from freeing the handle after the lock is released but before the C call executes. A more robust approach would be to use a read-write lock pattern or an in-flight operation counter: increment a counter (under lock) in requireHandle(), and have close() wait until the counter drops to zero before freeing. Alternatively, each operation could hold the lock for its entire duration (but this would serialize all operations).

Relevant locations:
- requireHandle() at MossClient.swift:410-415
- close() at MossClient.swift:85-95
- handleLock declaration at MossClient.swift:31
- All callers of requireHandle(): loadIndex, unloadIndex, query, deleteIndex, getIndex, listIndexes, refreshIndex, getJobStatus, createIndex, addDocs, getDocs, onMemoryPressure, deleteDocs
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +457 to +465
fileprivate static func decodeMutationResult(_ json: String) throws -> MutationResult {
struct Wire: Decodable {
let jobId: String
let indexName: String
let docCount: Int
}
let w = try JSONDecoder().decode(Wire.self, from: Data(json.utf8))
return MutationResult(jobId: w.jobId, indexName: w.indexName, docCount: w.docCount)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 decodeMutationResult Wire struct key format depends on undocumented C FFI JSON contract

The Wire struct at line 458-462 uses camelCase property names (jobId, indexName, docCount) without setting keyDecodingStrategy = .convertFromSnakeCase. The C struct MossMutationResult uses snake_case fields (job_id, index_name, doc_count) as seen in deleteDocs at line 399-401. However, the Python SDK bindings (sdks/python/bindings/src/models.rs:126) consistently use #[serde(rename_all = "camelCase")] for JSON serialization, suggesting the _from_json C functions also produce camelCase JSON. This is likely correct, but the contract between the Swift SDK and the moss_client_create_index_from_json / moss_client_add_docs_from_json functions is undocumented and fragile — if the native layer ever changes its JSON format, this will silently break at runtime.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

- Fix real race in MossClient: handle was read without holding the
  lock while close() freed it under the lock. Replaced with borrow/
  return lifecycle (stateCond NSCondition + inFlight counter); close
  now waits for in-flight ops to drain before freeing.
- Rewrite withCStringArray without strdup; allocates with traps on
  failure and copies UTF-8 bytes manually.
- Add QueryResult.metadata so consumers can inspect document
  metadata from search hits (was being dropped at the FFI boundary).
- ensureModelCacheDir now propagates errors with a descriptive
  message instead of failing silently.
- Authenticator doc: worked example showing why to return the raw
  token (not Bearer ...).
- decodeMutationResult: clear comment on the JSON contract.
- README: iOS 15+ to match Package.swift's .iOS(.v15).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants