Skip to content

Swift bindings#191

Open
panfilov-vladislav wants to merge 23 commits into
masterfrom
swift-bindings
Open

Swift bindings#191
panfilov-vladislav wants to merge 23 commits into
masterfrom
swift-bindings

Conversation

@panfilov-vladislav

Copy link
Copy Markdown
Collaborator

Adds complete Swift bindings for ouisync, including a code-generated
API layer, build tooling, and a working example app for macOS and iOS.

What's included

Codegen

Auto-generates the full Swift API (Api.swift) from the Rust service
definitions, covering all request/response types, error codes, and
repository/session/file operations, including AsyncStream<T> methods
for streaming/subscription endpoints.

Library

  • Client.swift: async/await IPC over TCP with HMAC auth, with
    streaming subscription support and cancellation
  • StateMonitor.swift: hierarchical state monitoring
  • OuisyncLib Swift package with OuisyncLibCore + OuisyncLib targets
  • build-xcframework.sh: builds a universal XCFramework for macOS + iOS
    (device and simulator)
  • FFIBuilder SPM plugin that invokes the xcframework build as part of
    the Swift package build

Integration tests

Full integration test suite using Swift Testing, exercising repository
create/open/sync/share flows against a live service instance.

Example app

Multi-platform SwiftUI app (macOS + iOS) demonstrating:

  • Starting the ouisync service and connecting a session
  • Creating, listing, sharing, and deleting repositories
  • Browsing and creating files and directories
  • Local network peer discovery and sync
  • Auto-refresh of folder contents when the repository changes

panfilov-vladislav and others added 23 commits May 27, 2026 12:30
- utils/bindgen: add swift.rs backend and Language::Swift subcommand
- bindings/swift: commit generated Api.swift (2513 lines, 121 methods)
- utils: add generate-swift-api.sh convenience script

Run utils/generate-swift-api.sh to regenerate Api.swift after API changes.
…b targets

OuisyncLibCore (Sources/) contains only the generated Api.swift and has no
dependency on the xcframework. OuisyncLib (SourcesFFI/) wraps the FFI layer
and depends on OuisyncLibCore. Tests now build against OuisyncLibCore.
- swift.rs: generate encodeToMsgPack/decodeFromMsgPack on all API types
  (simple enums, structs, complex enums, Request, Response); fix array
  wrapping for single named-field enum variants
- Client.swift: TCP actor with NWConnection, HMAC-SHA256 auth, frame
  encode/decode, CheckedContinuation pending map, cancellation support
- StateMonitor.swift: MonitorId (string-encoded) and StateMonitorNode
- Session.swift: Session.create(configPath:) convenience initializer
- generate-swift-api.sh: suppress cargo build output on stdout
Remove OuisyncClient, OuisyncError, OuisyncFile, OuisyncEntry,
OuisyncRepository, OuisyncMessage, OuisyncLib, and OuisyncSession —
all superseded by generated Api.swift types and the new Client actor.
Extract NotificationStream into Sources/ since it has no FFI deps.
OuisyncFFI.swift is kept as a stub pending step 5.
- build.sh: build ouisync-service instead of ouisync-ffi; link
  libouisync_service.a; run cbindgen with service/cbindgen.toml
- service/cbindgen.toml: new cbindgen config; enum style="type" so
  ErrorCode is exported as typedef uint16_t (clean UInt16 in Swift)
- OuisyncFFI.swift: replace dead FFI symbols with async OuisyncService
  wrapper around start_service / stop_service / init_log
- Session.swift: add public close() that cancels the underlying client
- Package.swift: test target now depends on OuisyncLib (needs xcframework)
- OuisyncLibTests.swift: integration tests — start OuisyncService, connect
  Session, exercise repository list/create/delete and file write/read/truncate
Replace XCTest with the Swift Testing framework (@suite, @test, #expect).
Use a withFixture() helper instead of setUp/tearDown to handle async
service + session lifecycle cleanly without relying on async deinit.
- Fix swift.rs: compare handle.value (UInt64) instead of handle struct
  (which lacks Equatable conformance) in generated == operators
- Fix swift.rs: Request enum named fields use unlabeled call syntax
  (prefix _ ) so Swift 6 callers don't need argument labels
- Fix swift.rs: Type::Bytes encodes as .binary(expr) not .binary([UInt8](expr))
- Regenerate Api.swift with the above fixes
- Fix Client.swift: qualify withUnsafeBytes as Swift.withUnsafeBytes to
  resolve Swift 6 ambiguity with Data's instance method
- Fix Client.swift: remove extraneous 'id:' label on .cancel() call site
- Fix OuisyncFFI.swift: @_exported import OuisyncLibCore so Session,
  OuisyncError etc. are visible to OuisyncLib consumers; qualify
  OuisyncLibCore types to avoid clash with C ErrorCode from OuisyncLibFFI;
  use resume(with:) instead of resumeWith (correct CheckedContinuation API)
- Fix service/cbindgen.toml: remove invalid [enum] section
- Fix Package.swift: link SystemConfiguration framework (required by
  libouisync_service.a via the system_configuration crate)
- Fix Tests: add missing 'import Foundation' for FileManager, Data, UUID
- Add build-xcframework-macos.sh for native macOS host builds
- All 8 integration tests now pass with swift test
Documents the architecture, build pipeline, code generation workflow,
and library usage with examples.
Pins MessagePack.swift at 4.0.0 for reproducible builds.
… exists

The guard in builder.swift required the "Update rust dependencies"
output directory to exist before any build. This worked fine within
the OuisyncLib package itself, but broke any external package that
depended on OuisyncLib (or OuisyncLibCore) — the output directory
lives in OuisyncLib's own .build tree, not in the downstream
package's .build tree.

Fix: also pass the guard when the pre-built xcframework is already
present at output/OuisyncLibFFI.xcframework. Since SKIP=1 in
config.sh the build script exits immediately anyway; the only thing
that needs to exist is the xcframework artifact.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add a Known limitations entry explaining why Repository.subscribe()
and Session.subscribeToNetwork() are absent: the current Client
resolves a single CheckedContinuation per message ID, so streaming
responses are silently dropped after the first. NotificationStream.swift
is already scaffolded; a channels map in the receive loop and two
manual extension functions (mirroring the Kotlin implementation) are
what's needed to complete it.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Mirrors the structure of bindings/kotlin/example. The app starts the
Ouisync service, opens a session, and presents three screens:

- RepositoryListView — create / delete repositories; copy a write-access
  share token to the clipboard via the share button
- FolderView — browse directory contents recursively; create files and
  directories with the + button
- FileView — show file size and SHA-256; write UTF-8 text content via
  the pencil button; poll sync progress (subscribe() pending)

Because swift run launches the process as a command-line tool (no
keyboard focus), a run.sh script builds the binary, wraps it in a
minimal .app bundle, and opens it with open(1).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- fs_util/Cargo.toml: add ios to the libc target cfg so the crate
  links correctly when cross-compiling for iOS targets
- fs_util/src/safe_move.rs: extend blocking_rename_no_replace_atomic
  cfg to any(macos, ios) — renamex_np has been available on iOS 10+
- service/src/logger/mod.rs: route ios through stdout.rs, resolving
  the pre-existing TODO comment
Replaces the macOS-only script with one that reads all targets from
config.sh, builds natively with cargo (no Docker), combines simulator
slices with lipo, and produces a single xcframework containing macOS,
iOS device, and iOS simulator slices.

OVERVIEW.md updated: prerequisites now include the rustup target add
one-liner for iOS toolchains, the build section documents both scripts,
and the iOS limitation bullet is removed from Known Limitations.
Replaces the macOS-only SPM package with a single Xcode project
targeting macOS 14+ and iOS 17+ from one shared codebase. Platform
differences are isolated to #if os(macOS) in four spots.

Notable changes:
- ShareLink with ouisync:// scheme (converted from the API's
  https://ouisync.net/... token) so AirDrop works natively on both
  platforms
- Incoming ouisync:// links registered in Info.plist open the app and
  pre-fill the Create Repository sheet with the token and suggested name
- ouisync:// <-> https://ouisync.net/ conversion at share/receive time
  keeps custom scheme handling simple without Universal Links
Display the first 16 chars of each repository's info hash as a
subtitle in the repo list, and log the full hash on startup. This
makes it easy to verify that two devices share the same repository
token when debugging sync issues.

Also fix repo display names to show only the filename without the
store path and .ouisyncdb extension.
The ouisync service does not persist the sync-enabled flag, so repos
loaded from the store directory on startup come up with sync disabled.
Call setSyncEnabled(true) on each repo after listRepositories() so
syncing resumes every session without requiring explicit user action.
…ifications

Client.swift gains a subscriptions registry (UInt64 → AsyncStream.Continuation)
alongside the existing pending map. A new subscribe() method sends the request,
waits for the server ack via the normal pending path, then returns an
AsyncStream<Response> that receives all subsequent server-pushed events for that
message ID. Cancellation sends Request.cancel to the server; connection drop
finishes all active streams.

swift.rs is updated to generate AsyncStream<T> methods for #[api(stream(T))]
variants instead of skipping them. The generated wrapper filters response cases
and hooks onTermination to cancel the inner forwarding task.

Regenerated Api.swift exposes four new public methods:
  Repository.subscribe() -> AsyncStream<Void>
  Session.subscribeToNetwork() -> AsyncStream<NetworkEvent>
  Session.subscribeToStateMonitor(_:) -> AsyncStream<Void>
  Session.dhtLookup(_:_:) -> AsyncStream<String>

NotificationStream.swift is deleted; it was never wired up and is superseded
by the subscriptions map in Client.
Subscribe to repository change notifications in FolderView so synced files
appear automatically without requiring a manual refresh.

@madadam madadam left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks great!

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