Skip to content

swift: split MoqFFI bindings from the Moq wrapper + Swift-native redesign#1561

Merged
kixelated merged 3 commits into
devfrom
claude/heuristic-mccarthy-9932a3
May 31, 2026
Merged

swift: split MoqFFI bindings from the Moq wrapper + Swift-native redesign#1561
kixelated merged 3 commits into
devfrom
claude/heuristic-mccarthy-9932a3

Conversation

@kixelated
Copy link
Copy Markdown
Collaborator

@kixelated kixelated commented May 30, 2026

Summary

Applies the Python split (#1551) to Swift, then redesigns the wrapper into a Swift-native API. Two motivations, both the user's:

  1. Auto-release MoqFFI via release-plz so consumers float to the latest patch with no wrapper churn, and version the wrapper independently with a registry-gated release workflow.
  2. Make the wrapper ergonomic using Swift conventions a generic moq-ffi can't express.

Packaging split

The Swift integration now ships as two SPM packages, each mirrored to its own repo:

Package Mirror Versioning Driven by
MoqFFI (raw bindings + XCFramework) moq-dev/moq-swift-ffi (new) lockstep w/ the moq-ffi crate moq-ffi-v* tag (release-swift-ffi.yml)
Moq (ergonomic wrapper) moq-dev/moq-swift (existing) independent (swift/VERSION) push to main/dev when VERSION is new (release-swift-lib.yml)
  • The wrapper pins MoqFFI at .upToNextMinor(from: <crate version>) (substituted from rs/moq-ffi/Cargo.toml at package time, the SPM analog of py's moq-ffi ~= 0.2.x), so a moq-ffi patch reaches consumers with no wrapper release.
  • release-swift-lib.yml reads swift/VERSION, checks whether that tag already exists on the mirror (release.sh git-tag-exists, the same registry-gate model release-plz uses), and publishes only when the version is new.
  • Local dev keeps one monolithic swift/Package.swift (both targets, path-based XCFramework) so swift test / Xcode are unchanged. The split exists only in the released artifacts, assembled from Package.swift.template (wrapper) and ffi/Package.swift.template (FFI). The FFI module is named MoqFFI in both layouts, so import MoqFFI in the wrapper compiles identically either way.

Scripts split along the same lines: package{,-ffi}.sh, verify{,-ffi}.sh, publish{,-ffi}.sh. Both verify jobs build a throwaway SPM consumer before any mirror push; the wrapper's verify resolves transitively against the published FFI mirror (and skips gracefully if that FFI version isn't mirrored yet, so the bootstrap doesn't deadlock).

Wrapper redesign (swift/Sources/Moq)

  • Every stateful FFI handle is wrapped in a de-prefixed, Sendable class: Client, Session, Server, Request, OriginProducer/Consumer, Broadcast*, Track*, Group*, Media*, Audio*, Announced*. MoqFFI's Moq-prefixed classes no longer appear in the public API.
  • Consumers conform to AsyncSequence, so for try await frame in mediaConsumer works directly (no .frames adapter); TrackConsumer iterates groups in sequence order with a groupsAsArrived property for arrival order.
  • Throwing initializers (BroadcastProducer() throws), Swift-friendly names (connect(to:), Request.accept()/reject(code:), session.publisher/consumer), and the MoqError.isShutdown helper.
  • Plain data records/enums are typealias re-exported under de-prefixed names (Frame, Catalog, Audio, Container, …), so new catalog/audio fields track the crate automatically; only new FFI methods need a wrapper method (noted in the Cross-Package Sync table).

Targets the new dev FFI surface (announce, Session.publisher()/consumer()).

Test plan

  • just swift check — builds moq-ffi, regenerates bindings, builds the monolithic package, runs swift test (3 tests pass, links against real FFI symbols).
  • ./swift/scripts/package.sh dry-run — wrapper tarball stages, REPLACE_FFI_VERSION resolves to the crate version, swift package dump-package parses.
  • actionlint clean on both workflows; shfmt/shellcheck clean on all scripts; remark clean on changed docs.
  • CI publish-dry-run on both workflows (runs on this PR).
  • FFI package/verify full xcframework path (CI macOS; the assembly logic is lifted verbatim from the existing CI-proven package.sh/verify.sh).

Migration notes

  • moq-dev/moq-swift-ffi must be created (empty) before the first FFI release; the publish script first-pushes main like the existing one.
  • swift/VERSION starts at 0.3.0, opening a new line above the old lockstep 0.2.x wrapper tags (as py's moq-rs did). The FFI mirror starts at the current crate version. Existing consumers pinned from: "0.2.0" on moq-dev/moq-swift will float up to 0.3.0 — the new, breaking API. (Targets dev per Branch Targeting.)

Out of scope

Kotlin and Go still consume moq-ffi-v* lockstep and are untouched; a follow-up can apply the same pattern if this proves out.

(Written by Claude)

kixelated and others added 3 commits May 30, 2026 12:43
…apper

Mirrors the py split (#1551) for Swift, then spends the freedom on a
Swift-native wrapper API.

Packaging: the Swift integration now ships as two SPM packages. The raw
MoqFFI bindings + prebuilt XCFramework mirror to moq-dev/moq-swift-ffi
lockstep with the moq-ffi crate on each moq-ffi-v* tag
(release-swift-ffi.yml). The ergonomic Moq wrapper versions independently
via swift/VERSION and mirrors to moq-dev/moq-swift when that version is new
(release-swift.yml, gated on the mirror's tags like release-plz). The
wrapper pins MoqFFI at .upToNextMinor, so a crate patch reaches consumers
with no wrapper release. Local dev keeps one monolithic swift/Package.swift;
the split lives only in the two release templates.

Wrapper redesign: every stateful FFI handle is wrapped in a de-prefixed,
Sendable class (Client, Session, BroadcastProducer, ...); MoqFFI's
Moq-prefixed classes no longer leak. Consumers conform to AsyncSequence, so
`for try await x in consumer` works directly. Plain data records/enums are
re-exported under de-prefixed names via typealias, so they track the crate
automatically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
git-tag-exists hard-fails when moq-dev/moq-swift-ffi doesn't exist yet (the
bootstrap state). Fall back to exists=false so the verify job skips the
cross-package resolve instead of failing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Symmetric pairing with release-swift-ffi.yml: -ffi builds the bindings
package, -lib publishes the ergonomic library wrapper. Updates the workflow
name, its own path filters, the concurrency group, and the references in
release-swift-ffi.yml, release-brew.yml, swift/README.md, and CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kixelated kixelated merged commit 0e607f4 into dev May 31, 2026
71 of 72 checks passed
@kixelated kixelated deleted the claude/heuristic-mccarthy-9932a3 branch May 31, 2026 02:47
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.

1 participant