Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ad7e730
Add Swift bindings codegen
panfilov-vladislav May 27, 2026
f7345a0
swift codegen: rename FileHandle -> OuisyncFileHandle to avoid Founda…
panfilov-vladislav May 27, 2026
b3fac2f
bindings/swift: split Package.swift into OuisyncLibCore and OuisyncLi…
panfilov-vladislav May 27, 2026
627d188
bindings/swift: update Package.swift and test import for split targets
panfilov-vladislav May 27, 2026
2ba3904
bindings/swift: implement Client, codec, and StateMonitor (step 3b)
panfilov-vladislav May 27, 2026
17019f3
bindings/swift: delete legacy hand-written types (step 4)
panfilov-vladislav May 27, 2026
81d4c78
bindings/swift: wire xcframework build to ouisync-service (step 5)
panfilov-vladislav May 27, 2026
1a60f6d
bindings/swift: integration tests (step 6)
panfilov-vladislav May 27, 2026
6c306c4
bindings/swift: rewrite tests using Swift Testing
panfilov-vladislav May 27, 2026
b415ba9
bindings/swift: fix compilation errors and integration tests
panfilov-vladislav May 27, 2026
ab24dc6
bindings/swift: add OVERVIEW.md
panfilov-vladislav May 27, 2026
1944fc6
bindings/swift: commit Package.resolved
panfilov-vladislav May 27, 2026
969cb9c
bindings/swift: allow FFIBuilder to skip cargo check when xcframework…
panfilov-vladislav May 28, 2026
c3f2a2a
bindings/swift: document missing subscribe() API in OVERVIEW.md
panfilov-vladislav May 28, 2026
4018b88
bindings/swift: add macOS SwiftUI example app
panfilov-vladislav May 28, 2026
51f3aac
bindings/swift: fix iOS build errors in fs_util and logger
panfilov-vladislav Jun 2, 2026
48564c4
bindings/swift: add build-xcframework.sh for all platforms (macOS + iOS)
panfilov-vladislav Jun 2, 2026
7aaaa8e
bindings/swift: replace SPM example with multi-platform Xcode project
panfilov-vladislav Jun 2, 2026
e67bb75
bindings/swift/example: add gitignore
panfilov-vladislav Jun 2, 2026
ba0f95c
bindings/swift/example: show repository info hash in repo list
panfilov-vladislav Jun 8, 2026
e7226bd
bindings/swift/example: re-enable sync on repos after restart
panfilov-vladislav Jun 8, 2026
2975858
bindings/swift: add subscribe() API for repository/session change not…
panfilov-vladislav Jun 8, 2026
3810ded
bindings/swift/example: auto-refresh folder view on repository changes
panfilov-vladislav Jun 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 246 additions & 0 deletions bindings/swift/OuisyncLib/OVERVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
# OuisyncLib — Swift Bindings Overview

Swift bindings for the [ouisync](https://github.com/equalitie/ouisync) P2P file-sync library.

## Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│ OuisyncLib (SourcesFFI/OuisyncFFI.swift) │
│ @_exported import OuisyncLibCore │
│ Wraps start_service / stop_service via CheckedContinuation │
│ Links: OuisyncLibFFI.xcframework + SystemConfiguration.fw │
└────────────────────┬────────────────────────────────────────────┘
│ depends on
┌────────────────────▼────────────────────────────────────────────┐
│ OuisyncLibCore (Sources/) │
│ Session, Repository, File, OuisyncError, … │
│ Client: TCP + HMAC-SHA256 auth + MessagePack framing │
│ Api.swift: generated — do not edit by hand │
└────────────────────┬────────────────────────────────────────────┘
│ uses at link time
┌────────────────────▼────────────────────────────────────────────┐
│ OuisyncLibFFI.xcframework (output/) │
│ libouisync_service.a ← ouisync-service Rust crate │
│ bindings.h ← generated by cbindgen │
│ Exports: start_service, stop_service, init_log │
└─────────────────────────────────────────────────────────────────┘
```

The Rust service owns all state and runs in a background thread. Swift talks to it
over a local TCP socket using HMAC-SHA256 authentication and MessagePack-encoded
request/response frames. The generated `Api.swift` file models every request and
response type; `Client.swift` handles the framing and multiplexing.

## Repository layout

```
bindings/swift/OuisyncLib/
├── Package.swift SPM package definition
├── build-xcframework.sh One-shot build script (all configured platforms)
├── build-xcframework-macos.sh One-shot build script (macOS host only)
├── config.sh Target list for build-xcframework.sh
├── Sources/ OuisyncLibCore target
│ ├── Client.swift TCP client, auth, framing
│ ├── Session.swift Session lifecycle
│ ├── StateMonitor.swift State monitoring helpers
│ ├── NotificationStream.swift Async notification stream
│ └── generated/
│ └── Api.swift ⚠ GENERATED — do not edit
├── SourcesFFI/ OuisyncLib target (wraps C FFI)
│ └── OuisyncFFI.swift OuisyncService start/stop async wrappers
├── Tests/
│ └── OuisyncLibTests.swift Integration tests (Swift Testing)
├── Plugins/
│ ├── Builder/builder.swift SPM build plugin (invoked by Xcode/SPM)
│ └── Updater/updater.swift SPM command plugin (cargo fetch)
└── output/
└── OuisyncLibFFI.xcframework ⚠ BUILT ARTIFACT — not committed
```

Other relevant paths in the repo root:

```
service/cbindgen.toml cbindgen config for the C header
utils/bindgen/src/swift.rs Code-generator backend (produces Api.swift)
utils/generate-swift-api.sh Convenience wrapper around the generator
```

## Prerequisites

| Tool | How to install |
|---|---|
| Rust + Cargo | `curl https://sh.rustup.rs -sSf \| sh` |
| cbindgen | `cargo install cbindgen` |
| Xcode (not just CLT) | From the Mac App Store; then `sudo xcode-select -s /Applications/Xcode.app/Contents/Developer` |

Verify the active developer tools point at Xcode (not CommandLineTools):

```sh
xcode-select -p # must print …/Xcode.app/Contents/Developer
```

To build for iOS targets, add the required Rust toolchains (one-time setup):

```sh
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
```

## 1. Build the xcframework

The xcframework (`OuisyncLibFFI.xcframework`) bundles the compiled Rust static library
together with the C header so SPM and Xcode can consume it.

```sh
# From anywhere inside the repo — builds all targets in config.sh:
bash bindings/swift/OuisyncLib/build-xcframework.sh
```

This script:
1. Reads the enabled targets from `config.sh` (macOS, iOS device, iOS simulator)
2. Runs `cargo build --package ouisync-service --release --target <triple>` for each
3. Runs `cbindgen` to generate `target/swift-include/bindings.h` from the Rust sources
4. Combines simulator slices with `lipo` where needed
5. Creates `OuisyncLib/output/OuisyncLibFFI.xcframework` via `xcodebuild -create-xcframework`

The resulting xcframework contains slices for all enabled platforms, so the same artifact
works for both macOS and iOS builds in Xcode.

> Re-run this script any time the Rust service API changes.

For macOS-only development (faster, skips iOS compilation):

```sh
bash bindings/swift/OuisyncLib/build-xcframework-macos.sh
```

## 2. Regenerate Api.swift

`Sources/generated/Api.swift` is machine-generated from the Rust type definitions. It
is committed to the repository, but must be regenerated after any change to the ouisync
API types or after modifying `utils/bindgen/src/swift.rs`.

```sh
# From the repo root:
bash utils/generate-swift-api.sh
```

This runs `cargo run --package ouisync-bindgen -- swift` and writes the output to
`bindings/swift/OuisyncLib/Sources/generated/Api.swift`.

> Commit the regenerated file together with the Rust changes that prompted it.

## 3. Build and test

```sh
cd bindings/swift/OuisyncLib

# Build only (no tests):
swift build --target OuisyncLib

# Run integration tests (requires the xcframework to be built first):
swift test
```

The test suite (`OuisyncLibTests`) uses **Swift Testing** (`import Testing`). All tests
are end-to-end: each one starts a real Rust service in a temp directory, exercises the
Swift API, then shuts the service down.

## Using the library

Add `OuisyncLib` as a Swift package dependency or embed the package directly. The
xcframework must be present at `output/OuisyncLibFFI.xcframework` for SPM to resolve
the binary target.

Typical lifecycle:

```swift
import OuisyncLib

// 1. Initialise logging (once, before starting the service)
OuisyncService.initLog()

// 2. Start the Rust service
let service = try await OuisyncService.start(configDir: "/path/to/config")

// 3. Open a session (connects to the running service)
let session = try await Session.create(configPath: "/path/to/config")

// 4. Configure storage
try await session.setStoreDirs(["/path/to/store"])

// 5. Use the API
let repo = try await session.createRepository("my-repo")
let file = try await repo.createFile("hello.txt")
try await file.write(0, Data("hello".utf8))
try await file.flush()
try await file.close()

// 6. Tear down
await session.close()
try await service.stop()
```

### Error handling

All async methods throw `OuisyncError` (or a typed subclass). Each error code from the
Rust service maps to a concrete subclass:

```swift
do {
let file = try await repo.openFile("missing.txt")
} catch is OuisyncError.NotFound {
// handle not found
} catch let err as OuisyncError {
print("code=\(err.code), message=\(err.message ?? "-")")
}
```

### Key types

| Type | Description |
|---|---|
| `OuisyncService` | Rust service lifecycle (start / stop) |
| `Session` | Connection to a running service |
| `Repository` | A single ouisync repository |
| `File` | An open file handle within a repository |
| `NetworkSocket` | UDP socket for custom transports |
| `NetworkStream` | TCP stream for custom transports |
| `ShareToken` | Capability token for sharing repositories |
| `OuisyncError` | Base error class with typed subclasses |

## How the code generator works

`utils/bindgen/src/swift.rs` reads the ouisync API definition (request/response enums
and data types) and emits idiomatic Swift:

- **Structs / enums** — mirror the Rust types with `encodeToMsgPack` / `decodeFromMsgPack`
- **`Request` enum** — all named-field cases use `_ label:` parameters (unlabeled call sites)
- **`Response` enum** — decoded from server replies
- **Wrapper classes** (`Session`, `Repository`, `File`, …) — async methods that send a
`Request` and decode the matching `Response`

After editing `swift.rs`, regenerate and verify:

```sh
bash utils/generate-swift-api.sh
swift build --target OuisyncLib
swift test
```

## Known limitations / TODOs

- The `OuisyncLibFFI` binary target path is hard-coded; it should move to a separate
Swift package so downstream apps can integrate without the full repo.
- **`subscribe()` / streaming API is not yet implemented.** `Repository` and `Session`
have no `subscribe()` method even though the underlying protocol supports it
(`Request.repositorySubscribe`, `Request.sessionSubscribeToNetwork` are generated).
The blocker is that `Client.invoke()` resolves a single `CheckedContinuation` per
message ID, so subsequent event notifications are silently dropped. A second
registration map (keyed message ID → `AsyncStream.Continuation`) needs to be added
to the receive loop, and manual `subscribe() -> NotificationStream` methods added to
`Repository` and `Session` — exactly as Kotlin does with its `channels` map and
`Flow`-returning extension functions. `NotificationStream.swift` is already scaffolded
and waiting for this work.
- The SPM `FFIBuilder` plugin rebuilds the Rust library on Xcode/SPM builds; the
`build-xcframework.sh` script is the simpler path for development.
14 changes: 14 additions & 0 deletions bindings/swift/OuisyncLib/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 12 additions & 3 deletions bindings/swift/OuisyncLib/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,26 @@ let package = Package(
.library(name: "OuisyncLib",
type: .static,
targets: ["OuisyncLib"]),
.library(name: "OuisyncLibCore",
type: .static,
targets: ["OuisyncLibCore"]),
],
dependencies: [
.package(url: "https://github.com/a2/MessagePack.swift.git", from: "4.0.0"),
],
targets: [
.target(name: "OuisyncLib",
.target(name: "OuisyncLibCore",
dependencies: [.product(name: "MessagePack",
package: "MessagePack.swift"),
package: "MessagePack.swift")],
path: "Sources"),
.target(name: "OuisyncLib",
dependencies: ["OuisyncLibCore",
"FFIBuilder",
"OuisyncLibFFI"],
path: "Sources"),
path: "SourcesFFI",
linkerSettings: [
.linkedFramework("SystemConfiguration"),
]),
.testTarget(name: "OuisyncLibTests",
dependencies: ["OuisyncLib"],
path: "Tests"),
Expand Down
10 changes: 9 additions & 1 deletion bindings/swift/OuisyncLib/Plugins/Builder/builder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ import PackagePlugin
.removingLastComponent() // ouisync.output
.appending("Update rust dependencies.output")

guard FileManager.default.fileExists(atPath: update.string) else {
// Check whether a pre-built xcframework already exists. When it does, the Rust
// toolchain is not needed for this build even if the cargo home directory hasn't
// been populated yet (e.g. when building an external package that depends on
// OuisyncLibCore but not OuisyncLib itself).
let xcframework = context.package.directory
.appending(["output", "OuisyncLibFFI.xcframework"])
let xcframeworkExists = FileManager.default.fileExists(atPath: xcframework.string)

guard xcframeworkExists || FileManager.default.fileExists(atPath: update.string) else {
Diagnostics.error("Please run `Update rust dependencies` on the OuisyncLib package")
fatalError("Unable to build LibOuisyncFFI.xcframework")
}
Expand Down
8 changes: 4 additions & 4 deletions bindings/swift/OuisyncLib/Plugins/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ cd $PROJECT_HOME
for TARGET in ${(k)TARGETS}; do
"$CARGO_HOME/bin/cross" build \
--frozen \
--package ouisync-ffi \
--package ouisync-service \
--target $TARGET \
--target-dir "$BUILD_OUTPUT" \
$FLAGS || exit 1
Expand All @@ -64,7 +64,7 @@ echo "module OuisyncLibFFI {
header \"bindings.h\"
export *
}" > "$INCLUDE/module.modulemap"
"$CARGO_HOME/bin/cbindgen" --lang C --crate ouisync-ffi > "$INCLUDE/bindings.h" || exit 2
"$CARGO_HOME/bin/cbindgen" --lang C --crate ouisync-service --config "$PROJECT_HOME/service/cbindgen.toml" > "$INCLUDE/bindings.h" || exit 2

# delete previous framework (possibly a stub) and replace with new one that contains the archive
# TODO: some symlinks would be lovely here instead, cargo already create two copies
Expand All @@ -86,15 +86,15 @@ for PLATFORM OUTPUTS in ${(kv)TREE}; do
MATCHED=() # list of libraries compiled for this platform
for TARGET in ${=OUTPUTS}; do
if [[ -v TARGETS[$TARGET] ]]; then
MATCHED+="$BUILD_OUTPUT/$TARGET/$CONFIGURATION/libouisync_ffi.a"
MATCHED+="$BUILD_OUTPUT/$TARGET/$CONFIGURATION/libouisync_service.a"
fi
done
if [ $#MATCHED -eq 0 ]; then # platform not enabled
continue
elif [ $#MATCHED -eq 1 ]; then # single architecture: skip lipo and link directly
LIBRARY=$MATCHED
else # at least two architectures; run lipo on all matches and link the output instead
LIBRARY="$BUILD_OUTPUT/$PLATFORM/libouisync_ffi.a"
LIBRARY="$BUILD_OUTPUT/$PLATFORM/libouisync_service.a"
mkdir -p "$(dirname "$LIBRARY")"
lipo -create $MATCHED[@] -output $LIBRARY || exit 3
fi
Expand Down
Loading
Loading