Repository navigation
feat(ffi): generate Swift over the C ABI, as a clang module - #406
Merged
Merged
Conversation
Quest-Claim: ae11864c64b7b34a
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut reformatting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wift # Conflicts: # ffi/justfile
…ion, and check lent paths from Swift Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…collide Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated
marked this pull request as ready for review
November 2, 2026 01:29
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
kixelated
enabled auto-merge (squash)
November 2, 2026 01:32
|
quest land: this pull request conflicts with a pull request queued ahead of it, in these files:
Once one of them lands, this one conflicts with the base and is ejected when its turn comes. Resolve the overlap before then, such as by coordinating with that author, or by merging the base once it lands. |
This was referenced Nov 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Quest
m1/ffi-export/swift: moq's Swift package still builds on the UniFFI fork. rust2-ffi exports moq's crates as a C ABI with C, C++, and Kotlin over it, but no Swift.Approach
src/swift.rswritesswift/<prefix>.swiftbesideswift/module.modulemap, which imports the C header as the clang module<Prefix>FFI, andswift/<prefix>_atomics.h, the atomics the runtime needs (Swift'sSynchronizationstarts at macOS 15). The generated API is the crate's pub API in Swift's idiom, named as Kotlin's:deinit, or sooner byclose()once no call holds it. A call afterclose()traps. Passing by value or aselfmethod closes them after the call.Sendablestructs. Enums are Swift enums, with each variant's data as associated values.PartialEqis==, through Rust's own.Displayisdescription,DebugisdebugDescription,Defaultisstatic default(), andCloneisclone().Data, with no copy at a call's top level either way. Returned bytes lend Rust's buffer throughData(bytesNoCopy:). Bytes nested in another value are copied into the call's arena.Resultthrows. An exported error type throws as itself, conforming toSwift.Error. Any other error type is wrapped in a generated<T>Error.async throws:PENDINGand parking.CancellationError. A task cancelled before the call takes nothing, even an output ready at the first poll.Sendableprotocols, held in a box that Rust releases when it drops the implementation.@Sendablefunction types, called through a trampoline that captures nothing.@Sendable () -> Void, andmoq/moq.swiftpolls moq's origin driver with aWaiteruntil the origin's last owner closes.<prefix>_contract()and<PREFIX>_CONTRACTare checked once, at the first call into the library. A mismatch traps.Tests
tests/api.swiftruns the shared script, and its transcript matches native Rust line for line.None;ownedchecks show that releasing frees Rust's bytes and handles at once.swiftwherever nixpkgs builds it, so not on aarch64-linux, where the test is ignored and points atquest/m2/uniffi-swift-arm.md.ffi-cppjob addsjust ffi test swift && just ffi moq-swiftwith Apple's toolchain, as C++ runs there.moq/moq.swiftruns moq.c's script, including the QUIC session and the driver. It passes locally.Init, which is Swift's initializer and also the C union member's name (the moq export hit it);async fnready withNone(also hit by the moq export);extended, aDurationsum that checks values pastInt64seconds up to Rust'sDuration::MAX;visit_stray, a closure lent a non-UTF-8 path, which traps.Choices made without asking
Swift.Duration, the only exact Swift type for Rust'sDuration. moq's package says macOS 12.3, so its switch raises it. iOS 16 is already its floor. The alternative, gating every declaration that touchesDurationbehind@available, spreads through every record holding one. Settled by the maintainer on 2026-11-01: ✅ raise to macOS 13 and iOS 16; keep 12.3 and gateDuration; generate an ownDurationtype.Counter.new("c", 5)), so a call reads as Rust's. Records keep a labeled memberwiseinit.catch let e as Api.Error), rather than Kotlin's<E>Exceptionwrapper. Only non-exported error types (String, bounds) get a<T>Errorwrapper.PartialEq, and only when Rust has one, so==keeps Rust's semantics.SocketAddris the runtime's own struct (IP bytes, port, flow label, scope), since Foundation has no socket address value type. Rust checks the family forSocketAddrV4andSocketAddrV6.inittakes a trailing_, as Kotlin's reserved members do.Measured
just ffi benchwith moq's frame path, 100 B frames, in ns per frame. Each cell is the median of 3 rounds, on x86_64 on a busy shared host (load average 32 to 55), so the numbers are noisy:The payload read is
Data(bytesNoCopy:)over Rust's buffer, so it never copies. Its cost is Foundation's storage allocation per read.Generated size for moq:
moq.swiftmoq.swiftwithout doc commentsmoq.kt, for comparisonAs with Kotlin, the export covers moq's crates whole, with every doc comment, rather than moq-ffi's hand-written surface. Compiling
moq.swiftas a module takes about 100 s at-Ononeon this host.Impact
swift/<prefix>.swift,swift/module.modulemap, andswift/<prefix>_atomics.h.ffi/MIGRATE.md:<prefix>_atomic_{load,store,add,exchange,cas}and<PREFIX>_ATOMICS_H.Swiftor a standard library name the Swift uses unqualified (such asData).flake.nix: the default shell addsswiftwhere nixpkgs builds it.ffi/justfile: addsmoq-swift.moqandbenchrun the Swift too, with a stated skip on aarch64-linux.nightly.yml: the macOSffi-cppentry runs the Swift checks.swift/package, through moq's uniffi-fork switch.Alternatives
<prefix>.hpp: it would reuse the C++ header, but Swift's C++ interop is still incomplete for templates and move-only types, and the C ABI is the stable base every other generator uses.Synchronization.Atomic: it needs macOS 15 and iOS 18, far above moq's floor.ManagedBufferfor the signal: Swift 6 makes its subclasses non-Sendable, so a plain class owning an allocated word is simpler.Follow-ups
quest/m2/uniffi-swift-arm.mdnow also asks to drop rust2-ffi's aarch64-linux Swift skips once nixpkgs builds Swift there.Review
quest review, Codex gpt-6.1-sol at medium effort, 3 rounds. Clean at heade702d634cef7a24bf9807b4f2c6546c1023dc668.DurationpastInt64seconds; error wrappers sharing a name; lent paths not checked for UTF-8.ts/justfile's wasm-bindgen, which is from main's feat(ts): pick the browser's randomness, and translate moq-net at moq's main #397 and not this branch.Swiftis refused. Refuted in part: no span and no rejection test, matching Kotlin's naming errors.🤖 Generated with Claude Code