diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 8ff80b40f..6fa92f63c 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -181,7 +181,8 @@ jobs: - name: Clang run: nix develop .#clang --command bash -c 'export CC=gcc; just ffi test cpp && just ffi moq-cpp' - # The C++ header with the compilers the job above lacks, through the same checks. + # The C++ header with the compilers the job above lacks, through the same checks, and on + # macOS the Swift with Apple's toolchain. ffi-cpp: name: Export moq-net to C++ (${{ matrix.name }}) runs-on: ${{ matrix.os }} @@ -234,6 +235,12 @@ jobs: - name: Export run: just ffi moq-cpp + # The Swift through the checks the merge gate runs with nixpkgs' Swift on Linux, then + # moq's session, reusing the builds above. + - name: Swift + if: runner.os == 'macOS' + run: just ffi test swift && just ffi moq-swift + # The Kotlin on ART, shrunk by R8 as an app shrinks it. x86_64, as moq's Android build: # this image ships the Android SDK and NDK, and its KVM runs an x86_64 emulator at speed. ffi-android: diff --git a/ffi/MIGRATE.md b/ffi/MIGRATE.md index ab8ae0f12..39aa41576 100644 --- a/ffi/MIGRATE.md +++ b/ffi/MIGRATE.md @@ -2,6 +2,11 @@ ## Unreleased +- Every export writes `swift/.swift`, whose runtime claims the C names + `_atomic_load`, `_atomic_store`, `_atomic_add`, + `_atomic_exchange`, `_atomic_cas`, and `_ATOMICS_H`. An item that + clashes with one, or whose Swift name clashes with another's or with a standard library + name the Swift uses, such as `Data`, takes a `rename`. - Kotlin returns bytes as a `Bytes`, not a `ByteArray`: Rust's buffer as `bytes.buffer`, a read-only `ByteBuffer`, copied up to 1 KiB and otherwise a direct one lent uncopied until `bytes.close()` frees it. Read it in `use { }`, or copy it out with diff --git a/ffi/README.md b/ffi/README.md index 22996c12d..7c41e2eab 100644 --- a/ffi/README.md +++ b/ffi/README.md @@ -27,7 +27,8 @@ A `[replace]` from the source it replaces, which no `[patch]` can carry, is an e Beside the C header, `include/.hpp` holds the same API in C++, and `kotlin/.kt` holds it in Kotlin, over JNI functions the crate builds with `--features kotlin`, and `kotlin/.pro` the R8 and ProGuard rules keeping what -those functions look up by name. +those functions look up by name, and `swift/.swift` holds it in Swift, over the C +header as a clang module. ```toml # ffi.toml, beside the package's Cargo.toml unless --manifest-path says otherwise. @@ -272,6 +273,52 @@ in as a `ByteArray`, bytes returned as their owner, and anything else encoded in Android library ships it as a consumer rule (`consumerProguardFiles`), which its AAR carries to each app that shrinks it. +## Swift + +`swift/.swift` is Swift over the C ABI, for Apple's platforms and Linux. It imports +the header as the clang module `FFI`, which `swift/module.modulemap` declares +beside `swift/_atomics.h`, the atomics its runtime needs that Swift's standard +library has only from macOS 15: a build puts `swift/` on its import path and links the +static library. It needs Swift 6 and, for `Swift.Duration`, macOS 13 or iOS 16, and builds +in either language mode. + +- **Names** are as Kotlin's, at the module's top level: types in PascalCase without the + prefix, members in camelCase without their type's name (`TrackProducer.appendGroup`). A + keyword is quoted in backticks, and a member named as one Swift gives its type, such as + `init`, takes a trailing `_`. Parameters have no labels, so a call reads as Rust's, and a + constructor such as `new` stays a static function: `Counter.new(1)`. +- **Objects** are final classes owning their handle, freed in `deinit`, or sooner by + `close()` once no call holds it; a call after it traps. A `self` method, or passing the + object by value, closes it once the call returns. `Clone` is `clone()`, and `PartialEq` + is `==`. A field is a computed property, and its setter a function, `setFirst(value)`, + since setting an object moves it in and closes its handle, which an assignment would + hide. An object is `Sendable`, since Rust checks each borrow at run time. +- **Records** are structs and **enums** Swift enums, with a variant's data as associated + values, all `Sendable`. `PartialEq` is `==`, through Rust's; `Display` is `description`, + `Debug` `debugDescription`, and `Default` a static `default()`. +- **Bytes** are `Data`. Passed in at a call's top level, Rust reads Swift's own; nested in + another value, they are copied in. Bytes Rust returns lend its buffer until Swift + releases the `Data`, though `Data` copies a few bytes inline, freeing Rust's at once. + Strings are `String`, copied, `char` `Unicode.Scalar`, `usize` `UInt`, `Option` `T?`, + a tuple a Swift tuple, though a tuple of one is its element, and `Duration` + `Swift.Duration`. An `Err` is thrown: an exported type as itself, which conforms to + `Swift.Error`, and any other in a generated `Error` holding it. +- A **path** is a `String`: one Rust returns that is not UTF-8 traps. A `SocketAddr`, + `SocketAddrV4`, or `SocketAddrV6` is the runtime's `SocketAddr`, its IP's 4 or 16 bytes + and its port, flow label, and scope, whose family Rust checks. A `Vec` or `&[T]` is + an array, and passing a `Vec` by value closes each object in it once the call returns. +- **Traits** are `Sendable` protocols, which Rust releases once it drops an implementation. +- **Closures** lent for a call are `@Sendable` function types, which Rust calls only during + the call, from any thread. An object it lends is closed once the call returns. +- **Futures** are `async throws` functions, which resume on the task's executor, never + inside a wake. Cancelling the task frees the future, losing nothing, and throws + `CancellationError`. Each call is its own future, so it has one poller. The call polls it + once before suspending, and makes the signal it parks on only if that poll pends; the + signal latches a wake that lands before it parks. +- A **waker** is a `@Sendable () -> Void`, which Rust may call from any thread. +- **The contract** is checked once, at the first call into the library: a Swift file and a + library from different builds trap. + ## IR [`src/ir.rs`](src/ir.rs) is the API as each generator reads it, with no language decided: @@ -280,10 +327,12 @@ its Rust path, its span, and its docs. A type is in the exact form Rust takes or (`&str` apart from `String`, `Box` apart from `impl Trait`), so a generator can borrow where Rust borrows. [`src/abi.rs`](src/abi.rs) decides the C ABI from it once, names and signatures, so [`src/c.rs`](src/c.rs), [`src/cpp.rs`](src/cpp.rs), -[`src/kotlin.rs`](src/kotlin.rs), and [`src/glue.rs`](src/glue.rs) agree; the runtime the -glue includes is [`runtime/rt.rs`](runtime/rt.rs), the one the C++ header includes -[`runtime/rt.hpp`](runtime/rt.hpp), and the ones Kotlin and its JNI glue include -[`runtime/rt.kt`](runtime/rt.kt) and [`runtime/jni.rs`](runtime/jni.rs). +[`src/kotlin.rs`](src/kotlin.rs), [`src/swift.rs`](src/swift.rs), and +[`src/glue.rs`](src/glue.rs) agree; the runtime the glue includes is +[`runtime/rt.rs`](runtime/rt.rs), the one the C++ header includes +[`runtime/rt.hpp`](runtime/rt.hpp), the ones Kotlin and its JNI glue include +[`runtime/rt.kt`](runtime/rt.kt) and [`runtime/jni.rs`](runtime/jni.rs), and the one the +Swift includes [`runtime/rt.swift`](runtime/rt.swift). ## Census @@ -296,12 +345,13 @@ refusal, which is a bug in the exporter's inference rather than in the crate. ## Tests ```bash -just ffi test # export the fixtures, build them, and drive api from C, C++, and Kotlin -just ffi moq # export moq's crates at the pinned revision, build them, and run moq/moq.c, moq/moq.cpp, and moq/moq.kt +just ffi test # export the fixtures, build them, and drive api from C, C++, Kotlin, and Swift +just ffi moq # export moq's crates at the pinned revision, build them, and run moq/moq.c, moq/moq.cpp, moq/moq.kt, and moq/moq.swift just ffi moq-cpp # the same export, running only moq/moq.cpp +just ffi moq-swift # the same export, running only moq/moq.swift just ffi census # census each into target/census nix develop .#android --command just ffi android # the Kotlin test, shrunk by R8, on the Android device adb reaches -nix develop .#bench --command just ffi bench # frame writes and reads, through the export from C, C++, and Kotlin, and moq's rs/moq-c +nix develop .#bench --command just ffi bench # frame writes and reads, through the export from C, C++, Kotlin, and Swift, and moq's rs/moq-c ``` The `api` fixture covers each rule once, and names the `dep` fixture's types, which @@ -316,7 +366,12 @@ each runtime check, which must throw or abort with its message. `just ffi androi same on ART: the Kotlin and `tests/api.kt`, shrunk by R8 into an app's dex with only the generated rules keeping what JNI looks up, against the library built by the NDK for the device's ABI, which ANDROID_HOME and ANDROID_NDK_HOME locate. Nightly runs it on an -x86_64 emulator at API 24, the oldest moq's Android library supports, and at API 35. The `reject` +x86_64 emulator at API 24, the oldest moq's Android library supports, and at API 35. +`tests/api.swift` runs the script through the Swift, built as a module in Swift 6's +language mode and type-checked in Swift 5's, checks its futures the same way and that +releasing frees, and breaks each runtime check, which must trap or abort with its message. +It runs with nixpkgs' Swift on Linux, which builds none for aarch64-linux +([quest](/quest/m2/uniffi-swift-arm.md)), and nightly with Apple's on macOS. The `reject` fixture's expected errors, each with its notes, are `fixtures/reject/expected.txt`. moq's crates export through `target/moq-ffi`, an empty library on each that `just ffi moq` writes in place of moq-ffi's remainder. `moq/moq.c` publishes and subscribes to a moq-net @@ -327,7 +382,8 @@ serves a broadcast from a moq-tokio server bound to port 0 to a client that read QUIC, both on moq-tokio's fallback runtime, whose threads wake C's. `moq/moq.cpp` does the same through the C++ header, short of the catalog's field, at C++17 without exceptions or RTTI, as OBS builds it, serving on an executor's thread and blocking on each read, and -`moq/moq.kt` through the Kotlin, serving in a coroutine and suspending on each read. +`moq/moq.kt` through the Kotlin, serving in a coroutine and suspending on each read, and +`moq/moq.swift` through the Swift, serving in a task and awaiting each read. The C++ builds take `CXX`, or `c++`, with every warning an error and `-Wshadow`, which stands in for MSVC's warnings that a name hides another. On Windows they take MSVC's `cl` diff --git a/ffi/fixtures/api/src/lib.rs b/ffi/fixtures/api/src/lib.rs index e68769f70..60b8215d4 100644 --- a/ffi/fixtures/api/src/lib.rs +++ b/ffi/fixtures/api/src/lib.rs @@ -66,6 +66,22 @@ pub enum Event { Done, } +/// An enum whose variant `Init` names an initializer in Swift, and the C union member +/// holding its data. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Stage { + Init(u32), + Done, +} + +/// The stage after `stage`: `Init` counts down to `Done`. +pub fn advance(stage: Stage) -> Stage { + match stage { + Stage::Init(0) | Stage::Done => Stage::Done, + Stage::Init(n) => Stage::Init(n - 1), + } +} + /// The error a [`Counter`] returns. #[derive(Clone, Debug, PartialEq)] pub enum Error { @@ -480,6 +496,11 @@ impl Channel { } } +/// `n` halved if even, at once: a future whose output may be `None`. +pub async fn halved(n: u32) -> Option { + n.is_multiple_of(2).then_some(n / 2) +} + /// `value`, on its third poll: each before wakes its waker before it returns pending, so /// the wake lands before the poller waits for one, even past a first poll Kotlin makes /// with no waker. @@ -1305,6 +1326,17 @@ pub fn stray() -> std::path::PathBuf { ])) } +/// Lends [`stray`]'s path to `visit`. +#[cfg(unix)] +pub fn visit_stray(visit: impl Fn(&std::path::Path)) { + visit(&stray()); +} + +/// `duration` plus `by`, unless past [`Duration::MAX`]: Rust's seconds are a `u64`. +pub fn extended(duration: Duration, by: Duration) -> Option { + duration.checked_add(by) +} + /// Each path's parent, if it has one. pub fn parents(paths: Vec) -> Vec> { paths diff --git a/ffi/justfile b/ffi/justfile index 058ba5934..b9164e584 100644 --- a/ffi/justfile +++ b/ffi/justfile @@ -46,7 +46,7 @@ run *args: census checkout="": (standin checkout) cargo run --locked --quiet -- --config moq/ffi.toml --manifest-path target/moq-ffi/Cargo.toml --census target/census -# Time frame writes through the export, from C, C++, and Kotlin, and through moq's hand-written rs/moq-c, in the bench shell: nix develop .#bench. +# Time frame writes through the export, from C, C++, Kotlin, and Swift, and through moq's hand-written rs/moq-c, in the bench shell: nix develop .#bench. bench checkout="": (moq checkout) #!/usr/bin/env bash set -euo pipefail @@ -62,14 +62,23 @@ bench checkout="": (moq checkout) c++ -O2 -std=c++17 -Wall -Wextra -Werror -I "$out/include" moq/bench/export.cpp "$out/target/release/libmoq.a" -lpthread -ldl -lm -o target/bench-export-cpp rm -rf target/bench-kt kotlinc -Werror -cp "$KOTLINX_COROUTINES" "$out/kotlin/moq.kt" moq/bench/export.kt -d target/bench-kt + swift=true + if [ "{{ os() }}-{{ arch() }}" = linux-aarch64 ]; then + echo "skipping the Swift: nixpkgs builds no Swift for aarch64-linux (quest/m2/uniffi-swift-arm.md)" >&2 + swift=false + else + {{ just_executable() }} swift-module -O + swiftc -O -warnings-as-errors $({{ just_executable() }} swift-target 15) -I target/swift -I "$out/swift" moq/bench/export.swift target/swift/libMoq.a "$out/target/release/libmoq.a" -lpthread -ldl -lm -o target/bench-export-swift + fi for _ in 1 2 3; do target/bench-moq-c target/bench-export target/bench-export-cpp kotlin -Djava.library.path="$out/target/release" -cp "target/bench-kt:$KOTLINX_COROUTINES" ExportKt + if $swift; then target/bench-export-swift; fi done -# Export moq's crates' C ABI, build it, and run moq/moq.c, moq/moq.cpp, and moq/moq.kt against it, from the pinned moq revision or a checkout. +# Export moq's crates' C ABI, build it, and run moq/moq.c, moq/moq.cpp, moq/moq.kt, and moq/moq.swift against it, from the pinned moq revision or a checkout. moq checkout="": (moq-cpp checkout) #!/usr/bin/env bash set -euo pipefail @@ -81,6 +90,11 @@ moq checkout="": (moq-cpp checkout) rm -rf target/moq-kt kotlinc -Werror -cp "$KOTLINX_COROUTINES" "$out/kotlin/moq.kt" moq/moq.kt -d target/moq-kt kotlin -Djava.library.path="$out/target/release" -cp "target/moq-kt:$KOTLINX_COROUTINES" MoqKt + if [ "{{ os() }}-{{ arch() }}" = linux-aarch64 ]; then + echo "skipping moq/moq.swift: nixpkgs builds no Swift for aarch64-linux (quest/m2/uniffi-swift-arm.md)" >&2 + else + {{ just_executable() }} moq-swift-run + fi # Export and build moq's crates' C ABI as `just ffi moq` does, and run only moq/moq.cpp against it, built by $CXX or the platform's compiler: MSVC's on Windows, from a developer environment. moq-cpp checkout="": (moq-export checkout) @@ -98,6 +112,36 @@ moq-cpp checkout="": (moq-export checkout) fi target/moq-cpp-test +# Export and build moq's crates' C ABI as `just ffi moq` does, and run only moq/moq.swift against it, through Swift on PATH: Apple's on macOS. +moq-swift checkout="": (moq-export checkout) moq-swift-run + +# Build the exported Swift as the module Moq, and run moq/moq.swift against it. +[private] +moq-swift-run: + #!/usr/bin/env bash + set -euo pipefail + out=target/export/moq + read -ra native < target/moq-native + {{ just_executable() }} swift-module + swiftc -warnings-as-errors $({{ just_executable() }} swift-target 15) -parse-as-library -I target/swift -I "$out/swift" moq/moq.swift target/swift/libMoq.a "$out/target/release/libmoq.a" "${native[@]}" -o target/moq-swift-test + target/moq-swift-test + +# Build target/export/moq's Swift as the module Moq in target/swift, at Swift 6's language mode. Its deployment target is the oldest its `Swift.Duration` allows. +[private] +swift-module *flags: + #!/usr/bin/env bash + set -euo pipefail + out=target/export/moq/swift + rm -rf target/swift + mkdir -p target/swift + swiftc -warnings-as-errors $({{ just_executable() }} swift-target 13) -swift-version 6 {{ flags }} -parse-as-library -module-name Moq -emit-module -emit-module-path target/swift/Moq.swiftmodule -emit-library -static -I "$out" "$out/moq.swift" -o target/swift/libMoq.a + +# swiftc's target at this macOS deployment target on macOS, and nothing elsewhere. +[private] +swift-target macos: + #!/usr/bin/env bash + if [ "{{ os() }}" = macos ]; then echo "-target $(uname -m)-apple-macos{{ macos }}"; fi + # Export moq's crates' C ABI into target/export/moq and build it, with the libraries a program linking it needs besides in target/moq-native. [private] moq-export checkout="": (standin checkout) diff --git a/ffi/moq/bench/export.swift b/ffi/moq/bench/export.swift new file mode 100644 index 000000000..28bb752fa --- /dev/null +++ b/ffi/moq/bench/export.swift @@ -0,0 +1,116 @@ +// bench/export.cpp's frame writes and reads through the generated Swift, so the two differ +// by what the Swift wrapper adds. + +import Foundation +import Moq + +let groups = 10000 +let frames = 100 + +/// Nanoseconds per frame since `start`. +func perFrame(_ start: UInt64) -> Double { + Double(DispatchTime.now().uptimeNanoseconds - start) / Double(groups * frames) +} + +/// A value a task sets and this thread reads once the task signals. +final class Cell: @unchecked Sendable { + var value: T? +} + +/// `body`'s result, waiting on this thread for it to finish on the global executor. +func block(_ body: @escaping @Sendable () async throws -> T) -> T { + let done = DispatchSemaphore(value: 0) + let result = Cell() + Task { + result.value = try! await body() + done.signal() + } + done.wait() + return result.value! +} + +func round() throws -> String { + let broadcast = BroadcastInfo.new().produce() + let track = try broadcast.createTrack("bench", TrackInfo.default().withTimescale(Timescale.MICRO)) + + // A frame of 100 zero bytes, as bench/export.c writes. + let frame = Data(count: 100) + var micros: UInt64 = 0 + + // Each frame stamped through a fresh timestamp handle, as a caller holding a u64 must. + var start = DispatchTime.now().uptimeNanoseconds + for _ in 0.. String? { + data.map { String(decoding: $0, as: UTF8.self) } +} + +/// A value a task sets and the script reads, under a lock. +final class Cell: @unchecked Sendable { + private let lock = NSLock() + private var stored: T + + init(_ value: T) { + stored = value + } + + var value: T { + get { + lock.lock() + defer { lock.unlock() } + return stored + } + set { + lock.lock() + defer { lock.unlock() } + stored = newValue + } + } +} + +/// The jobs tasks resume with, run when the script drains them. +final class Queue: TaskExecutor, @unchecked Sendable { + private let lock = NSLock() + private var jobs: [UnownedJob] = [] + + func enqueue(_ job: UnownedJob) { + lock.lock() + defer { lock.unlock() } + jobs.append(job) + } + + var size: Int { + lock.lock() + defer { lock.unlock() } + return jobs.count + } + + func drain() { + while true { + lock.lock() + let job = jobs.isEmpty ? nil : jobs.removeFirst() + lock.unlock() + guard let job else { return } + job.runSynchronously(on: asUnownedTaskExecutor()) + } + } + + func launch(_ body: @escaping @Sendable () async throws -> Void) { + Task(executorPreference: self, operation: body) + } +} + +/// `body`'s result, waiting on this thread for it to finish on the global executor. +func block(_ body: @escaping @Sendable () async throws -> T) throws -> T { + let done = DispatchSemaphore(value: 0) + let result = Cell?>(nil) + Task { + do { + result.value = .success(try await body()) + } catch { + result.value = .failure(error) + } + done.signal() + } + done.wait() + return try result.value!.get() +} + +func script() throws { + let q = Queue() + + let broadcast = BroadcastInfo.new().produce() + let track = try broadcast.createTrack("video", nil) + expect(track.name() == "video", "a track has its name") + + // A second track of the same name is an error, printed as Rust prints it. + do { + _ = try broadcast.createTrack("video", nil) + expect(false, "a duplicate track is an error") + } catch let e as Moq.Error { + expect("\(e)" == "duplicate", "a duplicate track's error is Rust's") + } + + let subscriber = try block { try await track.consume().subscribe(nil) } + + // A group's arrival wakes the subscriber's pending receive. + let group = Cell(nil) + q.launch { group.value = try await subscriber.recvGroup() } + q.drain() + expect(group.value == nil, "a receive waits for a group") + let writer = try track.appendGroup() + expect(q.size == 1, "a group's arrival wakes the receive") + q.drain() + let first = group.value! + expect(first.index() == 0, "the first group is 0") + + // So does a frame's. + let frame = Cell(nil) + q.launch { frame.value = try await first.readFrame() } + q.drain() + expect(frame.value == nil, "a read waits for a frame") + try writer.writeFrame(nil, Data("hello".utf8)) + expect(q.size == 1, "a frame's arrival wakes the read") + q.drain() + let hello = frame.value! + expect(text(hello.payload) == "hello", "a frame has its payload") + expect(hello.timestamp == nil, "an untimed frame has no timestamp") + // When it arrived: moq-clock's `Instant`, from moq-clock's own export. + expect(now().checkedDurationSince(hello.arrived()) != nil, "a frame arrived before now") + + // A timestamp is a `Copy` value behind a handle: passing it copies it out. The track is + // untimed, so a timestamped frame is refused. + let at = try Timestamp.fromMillis(1500) + expect(at.asSecs() == 1, "1500 ms is 1 s") + do { + try writer.writeFrame(at, Data("late".utf8)) + expect(false, "a timestamped frame on an untimed track is an error") + } catch let e as Moq.Error { + expect("\(e)" == "frame timestamp doesn't fit its track", "the late frame's error is Rust's") + } + expect(at.asSecs() == 1, "a copied timestamp stays usable") + + // The last frame, bytes that are not UTF-8, then the group's end, which reads as nil. + let world = Data([0xff]) + Data("world".utf8) + try writer.writeFrame(nil, world) + try writer.finish() + try block { + expect(try await first.readFrame()?.payload == world, "the last frame reads") + expect(try await first.readFrame() == nil, "a finished group reads nil") + } + + // An origin and the driver that runs it, a pair. Its root, which it lends, is returned + // as a new handle to a clone. + let (origin, driver) = OriginProducer.new(OriginConfig.default()) + expect(origin.root().asStr() == "", "an origin's root is empty") + + // Scoped beneath a root to patterns, which moq-pattern's own export builds. + let patterns = Patterns.new() + expect(try patterns.insert(Pattern.subtree("alice")), "a pattern inserts") + let scoped = try origin.scope("room", patterns) + expect(scoped.root().asStr() == "room", "a scope has its root") + expect(try scoped.absolute("alice").asStr() == "room/alice", "a scope's paths are absolute") + let allowed = scoped.allowed() + expect(allowed.matches("alice/cam"), "a scope allows its patterns") + expect(!allowed.matches("bob"), "a scope allows nothing else") + let consumer = scoped.consume() + expect(try consumer.absolute("bob").asStr() == "room/bob", "a scope's consumer shares its root") + consumer.close() + scoped.close() + patterns.close() + + // A broadcast the origin creates is stamped with its path, which its info lends. Each + // handle to it is closed, since an open one keeps it, and so the origin, alive. + do { + let publication = try origin.createBroadcast("room/alice") + defer { publication.close() } + let created = publication.broadcast() + let path = created.info().path + created.close() + expect(path.asStr() == "room/alice", "a broadcast has its path") + + // Paths own their segments, so Swift builds them, and relative ones, from a string. + do { + _ = try Path.new("/cam/") + expect(false, "a path with an empty segment is an error") + } catch let e as PathInvalid { + expect(e == .emptySegment, "an empty segment is its own error") + } + let cam = try Path.new("cam") + expect(cam.asStr() == "cam", "a path builds from its segments") + let joined = try path.join(cam) + expect(joined.asStr() == "room/alice/cam", "paths join") + expect(joined.stripPrefix(path) == cam, "a path strips its prefix") + expect(path.stripPrefix(joined) == nil, "a path strips no longer prefix") + expect(try joined.resolve(Relative.new("../bob")).asStr() == "room/bob", "a relative path resolves") + } + + // The driver, polled as a language's event loop would: at the time, and with a waiter + // made from the waker and held across polls, since what a poll registers lives only as + // long as its waiter. It runs until every owner of the origin is gone. + let wakes = Cell(0) + let waiter = Waiter.new { wakes.value += 1 } + _ = try driver.poll(now(), waiter) + // A client publishes the origin through moq-net's generic `with_publisher`, at each + // type that implements its bound, by reference, so Swift keeps its handle. + Client.new().withPublisherProducer(origin).close() + let before = wakes.value + origin.close() + // The last owner's close wakes the driver, which then finishes, cleanly. + expect(wakes.value > before, "the last owner's close wakes the driver") + do { + _ = try driver.poll(now(), waiter) + expect(false, "a driver whose origin is gone finishes") + } catch let e as Moq.Error { + expect("\(e)" == "closed", "a finished driver's error is Rust's") + } + + // moq-mux reads an OpusHead into hang's catalog config: 44.1 kHz in, stereo, which the + // catalog lists at the decoder's 48 kHz, keeping the head as its description. + let head = Data("OpusHead".utf8) + Data([1, 2, 0x38, 0x01, 0x44, 0xac, 0, 0, 0, 0, 0]) + let audio = try codecOpusAudioConfig(head) + expect(audio.sampleRate == 48000, "Opus decodes at 48 kHz") + expect(audio.channelCount == 2, "the head is stereo") + expect(audio.codec == .opus, "the codec is Opus") + expect(audio.description == head, "the head is the description") + // A truncated head is moq-mux's error. + do { + _ = try codecOpusAudioConfig(head.prefix(8)) + expect(false, "a truncated head is an error") + } catch let e as MuxError { + expect(!"\(e)".isEmpty, "a truncated head's error has a message") + } + + // A subscription's ranges, from any iterable of them: an array. + let groups = TrackRange.groups(BoundsU64(start: .included(2), end: .excluded(6))) + let ranged = TrackSubscription.default().withRanges([TrackRange.live(), groups]).ranges + expect(ranged == [TrackRange.live(), groups], "a subscription keeps its ranges") + expect(groups.start == .absolute(2), "groups start at their first") + expect(groups.end == .absolute(6), "groups end before their end") + + // A catalog at moq-mux's untyped extension, from the config Swift builds, whose generic + // `is_claimed` is named by the last segment of its argument, and whose `mutate` lends + // the catalog to Swift. + let publisher = BroadcastInfo.new().produce() + let extra = CatalogConfig.default().withCatalogExtra(HangCatalogExtra.default()) + let catalog = try CatalogProducerExtra.new(publisher, extra) + expect(!catalog.isClaimedBinaryConfig("state"), "an unclaimed name is not claimed") + let read = Cell(0) + try catalog.mutate { lent in + if try! lent.toJson().hasPrefix("{") { + read.value += 1 + } + } + expect(read.value == 1, "mutate lends the catalog once") + + // A JSON track the catalog lists, published through moq-mux's generic `json_snapshot` at + // a JSON document, and a section Swift parses from text, set in the lent catalog and + // read back as text. + let status = try catalog.jsonSnapshotConfig( + try publisher.createTrack("status", nil), JsonConfig.default(), JsonOptions.default()) + expect(status.name() == "status", "a JSON snapshot has its track's name") + expect(catalog.isClaimedJsonConfig("status"), "the JSON snapshot claims its name") + // A value Swift parses is published as the track's latest, which a subscriber reads + // back as JSON text. + let on = try Json.parse(#"{"on": true}"#) + try status.update(on) + on.close() + try block { + let frame = try await status.consume().recvGroup()?.readFrame() + expect(text(frame?.payload) == #"{"on":true}"#, "a subscriber reads the latest value") + } + try catalog.mutate { lent in + let sections = lent.ext + try! sections.set("app", Json.parse(#"{"v": [1, true]}"#)) + lent.setExt(sections) + } + let app = Cell(nil) + try catalog.mutate { lent in + expect(try! lent.toJson().contains("\"status\""), "the catalog lists the JSON track") + app.value = lent.ext.get("app")?.toText() + } + expect(app.value == #"{"v":[1,true]}"#, "the catalog reads its section back as text") + // Malformed text fails to parse, and a reserved section is moq-mux's error. + do { + try Json.parse(#"{"v":"#).close() + expect(false, "malformed JSON fails to parse") + } catch let e as StringError { + expect(e.error == "EOF while parsing a value at line 1 column 5", "a parse error is serde_json's") + } + do { + try CatalogHangExtra.default().set("json", Json.parse("1")) + expect(false, "a reserved section is an error") + } catch let e as MuxError { + expect("\(e)" == "reserved catalog section: json", "a reserved section is moq-mux's error") + } + + // moq-tokio's terminal connect errors, which moq-net's `Error` shares no names with. + expect(ConnectError.unauthorized.isAuth(), "unauthorized is an auth error") + expect("\(ConnectError.unauthorized)" == "unauthorized", "a connect error prints as Rust's") + + // Session config's foreign types. A URL is parsed from text, failing with why. + do { + try Url.parse("relay.example").close() + expect(false, "a relative URL is an error") + } catch let e as StringError { + expect(e.error == "relative URL without a base", "a URL's parse error is Rust's") + } + // A session's config, its URL and pinned fingerprints set as Rust assigns them. + let connect = ConnectConfig.default() + let tls = connect.tls + tls.setFingerprint(["ab:cd"]) + connect.setTls(tls) + tls.close() + connect.setUrl(try Url.parse("https://relay.example:4443/demo")) + expect(connect.tls.fingerprint == ["ab:cd"], "a TLS config keeps its fingerprints") + let url = connect.url! + connect.close() + let versions = Versions.default() + let tokens = AuthTokens.new([]) + let dial = tokens.dial(url, versions) + let dialed = dial.url + expect(dialed == url, "a dial keeps its URL") + expect("\(dialed)" == "https://relay.example:4443/demo", "a URL prints as Rust's") + dialed.close() + dial.close() + tokens.close() + url.close() + // A `Vec`, as an array. + expect(!ALPNS.isEmpty, "a constant slice crosses as an array") + let alpns = versions.alpns() + expect(!alpns.isEmpty && alpns.allSatisfy { !$0.isEmpty }, "each version has an ALPN") + versions.close() + // A socket address, crossing in a listener's bind. + let loopback: [UInt8] = Array(repeating: 0, count: 15) + [1] + let bind = ListenBind.addr(SocketAddr(ip: loopback, port: 4443)) + expect("\(bind)" == "[::1]:4443", "an IPv6 bind prints as Rust's") + let v4 = SocketAddr(ip: [127, 0, 0, 1], port: 0) + expect("\(ListenBind.addr(v4))" == "127.0.0.1:0", "an IPv4 bind prints as Rust's") + let listen = ListenConfig.default() + listen.setBind(bind) + // The preferred address of each family, which takes only that family. + listen.setPreferredV4(v4) + expect(listen.preferredV4 == v4 && listen.preferredV6 == nil, "a listener keeps its preferred IPv4 address") + expect(listen.bind == bind, "a listener's config keeps its bind") + listen.close() + // TLS files, as OS paths, and fingerprints, as strings: none by default. + let defaults = TlsConnect.default() + expect(defaults.root.isEmpty && defaults.fingerprint.isEmpty, "a default TLS config lists none") + defaults.close() + + // A session, as moq.c runs it: a server bound to port 0 reads back the port it got and + // serves with one call, in a task of its own, and a client dials it with one call and + // reads a frame back, each async func resuming once moq-tokio's fallback runtime wakes + // it from its threads. A stall fails the script. + DispatchQueue.global().asyncAfter(deadline: .now() + 30) { + FileHandle.standardError.write(Data("failed: the session stalled\n".utf8)) + exit(1) + } + try block { + let served = originSpawn() + let live = try served.createBroadcast("live") + try live.announce(OriginRoute.default()) + let liveTrack = try live.broadcast().createTrack("video", nil) + let liveGroup = try liveTrack.appendGroup() + try liveGroup.writeFrame(nil, Data("hello".utf8)) + try liveGroup.finish() + + let serverConfig = ServerConfig.default() + let listen = serverConfig.listen + listen.setBind(.addr(SocketAddr(ip: Array(repeating: 0, count: 16), port: 0))) + let tls = listen.tls + tls.setGenerate(["localhost"]) + listen.setTls(tls) + serverConfig.setListen(listen) + let server = try serverConfig.init_() + let local = try server.localAddr() + expect(local.port != 0, "a server bound to port 0 reads back the port it got") + let serving = Task { try await server.servePublish(served.consume()) } + + let connect = ConnectConfig.default() + connect.setUrl(try Url.parse("moqt://localhost:\(local.port)")) + let insecure = connect.tls + insecure.setInsecure(true) + connect.setTls(insecure) + let clientConfig = ClientConfig.default() + clientConfig.setConnect(connect) + let client = try clientConfig.init_() + let received = originSpawn() + let remote = received.consume() + let connection = client.clone().withReconnect(false).consume(received) + expect(connection != nil, "a client with a URL dials it") + let established = try await connection!.established() + + let subscriber = try await remote.requestBroadcast("live", nil).track("video").subscribe(nil) + let frame = try await subscriber.recvGroup()?.readFrame() + expect(text(frame?.payload) == "hello", "a session reads the frame the server serves") + + try await established.close_() + try await client.close_() + serving.cancel() + } +} + +@main +struct Main { + static func main() { + do { + try script() + } catch { + expect(false, "the script threw \(error)") + } + if failures == 0 { + print("ok") + } + exit(failures == 0 ? 0 : 1) + } +} diff --git a/ffi/runtime/rt.swift b/ffi/runtime/rt.swift new file mode 100644 index 000000000..ec04a3d48 --- /dev/null +++ b/ffi/runtime/rt.swift @@ -0,0 +1,483 @@ +// The runtime every generated Swift file includes, after `Native` names the export's C +// types and functions: the handles objects own, the arena a call lowers its values into, +// bytes and text across the C ABI, and the signal a future parks on. + +/// An IP socket address, as Rust's `SocketAddr` holds it. +public struct SocketAddr: Hashable, Sendable { + /// The IP in network order: 4 bytes for IPv4, or 16 for IPv6. + public var ip: [UInt8] + public var port: UInt16 + /// IPv6's flow label, zero for IPv4. + public var flowinfo: UInt32 + /// IPv6's scope, zero for IPv4. + public var scopeId: UInt32 + + public init(ip: [UInt8], port: UInt16, flowinfo: UInt32 = 0, scopeId: UInt32 = 0) { + self.ip = ip + self.port = port + self.flowinfo = flowinfo + self.scopeId = scopeId + } +} + +extension Native { + /// Checked before any other call: a Swift file and a library from different builds + /// must not mix. + static let checked: Bool = { + if !contract() { + fatalError("\(file) and the library are from different builds") + } + return true + }() + + @inline(__always) + static func check() { + _ = checked + } + + /// A non-null pointer for empty bytes, which C reads as present, unlike a null one. + nonisolated(unsafe) static let empty: UnsafeMutablePointer = { + let empty = UnsafeMutablePointer.allocate(capacity: 1) + empty.initialize(to: 0) + return empty + }() + + static var none: Buf { Buf(ptr: nil, len: 0, owner: nil) } + + /// `bytes` as a view C reads during `body`, without a copy. + @inline(__always) + static func view(_ bytes: Data, _ body: (Buf) throws -> R) rethrows -> R { + try bytes.withUnsafeBytes { raw in + let ptr = raw.baseAddress?.assumingMemoryBound(to: UInt8.self) ?? UnsafePointer(empty) + return try body(Buf(ptr: ptr, len: raw.count, owner: nil)) + } + } + + @inline(__always) + static func view(_ bytes: Data?, _ body: (Buf) throws -> R) rethrows -> R { + guard let bytes else { return try body(none) } + return try view(bytes, body) + } + + /// `text`'s UTF-8 as a view C reads during `body`, without a copy for a native string. + @inline(__always) + static func view(_ text: Swift.String, _ body: (Buf) throws -> R) rethrows -> R { + var text = text + return try text.withUTF8 { utf8 in + try body(Buf(ptr: utf8.baseAddress ?? UnsafePointer(empty), len: utf8.count, owner: nil)) + } + } + + @inline(__always) + static func view(_ text: Swift.String?, _ body: (Buf) throws -> R) rethrows -> R { + guard let text else { return try body(none) } + return try view(text, body) + } + + /// Bytes Rust returned, lent to `Data` without a copy and freed with it. + static func data(_ buf: Buf) -> Data { + guard let ptr = buf.ptr, buf.len > 0 else { + free(buf) + return Data() + } + return Data( + bytesNoCopy: UnsafeMutableRawPointer(mutating: ptr), count: buf.len, + deallocator: .custom { _, _ in free(buf) }) + } + + static func data(option buf: Buf) -> Data? { + buf.ptr == nil ? nil : data(buf) + } + + /// Bytes C lends a callback for its call, copied. + static func data(lent buf: Buf) -> Data { + buf.len == 0 ? Data() : Data(bytes: buf.ptr!, count: buf.len) + } + + static func data(lentOption buf: Buf) -> Data? { + buf.ptr == nil ? nil : data(lent: buf) + } + + /// Text Rust returned, copied, then freed. + static func string(_ buf: Buf) -> Swift.String { + defer { free(buf) } + return string(lent: buf) + } + + static func string(option buf: Buf) -> Swift.String? { + buf.ptr == nil ? nil : string(buf) + } + + /// Text C lends a callback for its call, which Rust made UTF-8, copied. + static func string(lent buf: Buf) -> Swift.String { + Swift.String(decoding: UnsafeBufferPointer(start: buf.ptr, count: buf.len), as: UTF8.self) + } + + static func string(lentOption buf: Buf) -> Swift.String? { + buf.ptr == nil ? nil : string(lent: buf) + } + + /// A path Rust returned, copied, then freed. + static func path(_ buf: Buf) -> Swift.String { + defer { free(buf) } + return path(lent: buf) + } + + static func path(option buf: Buf) -> Swift.String? { + buf.ptr == nil ? nil : path(buf) + } + + /// A path from Rust, copied, which on Unix may hold bytes no `String` holds: those trap. + static func path(lent buf: Buf) -> Swift.String { + var bytes = UnsafeBufferPointer(start: buf.ptr, count: buf.len).makeIterator() + var parser = Unicode.UTF8.ForwardParser() + while true { + switch parser.parseScalar(from: &bytes) { + case .valid: continue + case .emptyInput: return string(lent: buf) + case .error: fatalError("a path from Rust is not UTF-8") + } + } + } + + static func path(lentOption buf: Buf) -> Swift.String? { + buf.ptr == nil ? nil : path(lent: buf) + } + + static func scalar(_ code: UInt32) -> Unicode.Scalar { + guard let scalar = Unicode.Scalar(code) else { + fatalError("a char is not a Unicode scalar") + } + return scalar + } + + /// Rust's longest `Duration`: `u64::MAX` seconds and 999,999,999 nanoseconds. + static var longest: Swift.Duration { + .seconds(Int64.max) * 2 + .seconds(1) + .nanoseconds(999_999_999) + } + + static func duration(_ value: Swift.Duration) -> CDuration { + if value < .zero { + fatalError("a negative duration has no Rust Duration") + } + if value > longest { + fatalError("a duration past Rust's Duration::MAX") + } + // In halves, since `components` gives Int64's seconds, and Rust's are UInt64's. + let half = value / 2 + let (secs, attos) = half.components + let odd = (value - half * 2).components.attoseconds + var nanos = (attos * 2 + odd) / 1_000_000_000 + var whole = UInt64(secs) * 2 + if nanos >= 1_000_000_000 { + nanos -= 1_000_000_000 + whole += 1 + } + return CDuration(secs: whole, nanos: UInt32(nanos)) + } + + static func duration(_ value: CDuration) -> Swift.Duration { + // Past Int64's seconds, as Rust's may be, in halves. + let half = Swift.Duration.seconds(Int64(value.secs >> 1)) + return half * 2 + .seconds(Int64(value.secs & 1)) + .nanoseconds(Int64(value.nanos)) + } + + static func socketAddr(_ addr: SocketAddr) -> CAddr { + let v6: Bool + switch addr.ip.count { + case 4: v6 = false + case 16: v6 = true + default: fatalError("an IP address is 4 bytes or 16, not \(addr.ip.count)") + } + var c = CAddr() + withUnsafeMutableBytes(of: &c.ip) { ip in ip.copyBytes(from: addr.ip) } + c.port = addr.port + c.v6 = v6 + c.flowinfo = addr.flowinfo + c.scope_id = addr.scopeId + return c + } + + static func socketAddr(_ c: CAddr) -> SocketAddr { + var c = c + let ip = withUnsafeBytes(of: &c.ip) { ip in Array(ip.prefix(c.v6 ? 16 : 4)) } + return SocketAddr(ip: ip, port: c.port, flowinfo: c.flowinfo, scopeId: c.scope_id) + } + + /// The C array Rust returned, as its items each lifted by `lift`, which takes what + /// each holds; `free` then frees the array alone. + static func list(_ ptr: UnsafePointer?, _ len: Int, _ lift: (C) throws -> T) rethrows -> [T] { + guard let ptr else { return [] } + return try UnsafeBufferPointer(start: ptr, count: len).map(lift) + } + + /// A handle an object owns, or is lent for a call: each call holds it, so `close` + /// frees it only once no call does, and a call after `close` traps. + struct Handle { + let raw: OpaquePointer? + /// The holds, one for the owner until closed and one per call, shifted past a bit + /// set once closed. + let state: UnsafeMutablePointer + /// Null for a handle Rust lends, which it frees itself. + let free: (@convention(c) (OpaquePointer?) -> Void)? + let type: StaticString + + static let closed = 1 + static let held = 2 + + init(_ raw: OpaquePointer?, _ free: (@convention(c) (OpaquePointer?) -> Void)?, _ type: StaticString) { + self.raw = raw + self.free = free + self.type = type + state = UnsafeMutablePointer.allocate(capacity: 1) + state.initialize(to: Handle.held) + } + + func hold() -> OpaquePointer? { + var n = load(state) + while true { + if n & Handle.closed != 0 { + fatalError("\(type) is closed") + } + let seen = swap(state, n, n + Handle.held) + if seen == n { + return raw + } + n = seen + } + } + + func release() { + if add(state, -Handle.held) < Handle.held { + free?(raw) + } + } + + /// Run `body` with the raw handle, held until it returns. + @inline(__always) + func with(_ body: (OpaquePointer?) throws -> R) rethrows -> R { + let raw = hold() + defer { release() } + return try body(raw) + } + + func close() { + var n = load(state) + while n & Handle.closed == 0 { + let seen = swap(state, n, (n | Handle.closed) - Handle.held) + if seen == n { + if (n | Handle.closed) - Handle.held < Handle.held { + free?(raw) + } + return + } + n = seen + } + } + + /// Close it, for its owner's `deinit`, when no call can hold it. + func drop() { + close() + state.deallocate() + } + } + + /// Run `body` with the raw handle of `handle`, or null for none, held until it returns. + @inline(__always) + static func with(_ handle: Handle?, _ body: (OpaquePointer?) throws -> R) rethrows -> R { + guard let handle else { return try body(nil) } + return try handle.with(body) + } + + /// What a call lowers nested values into, kept until it returns: copied bytes, arrays, + /// and the handles it holds. + final class Arena { + private var blocks: [UnsafeMutableRawPointer] = [] + private var held: [Handle] = [] + + func copy(_ bytes: UnsafeRawBufferPointer) -> Buf { + guard bytes.count > 0 else { + return Buf(ptr: UnsafePointer(empty), len: 0, owner: nil) + } + let block = UnsafeMutableRawPointer.allocate(byteCount: bytes.count, alignment: 1) + block.copyMemory(from: bytes.baseAddress!, byteCount: bytes.count) + blocks.append(block) + return Buf(ptr: block.assumingMemoryBound(to: UInt8.self), len: bytes.count, owner: nil) + } + + func buf(_ bytes: Data) -> Buf { + bytes.withUnsafeBytes { copy($0) } + } + + func buf(_ text: Swift.String) -> Buf { + var text = text + return text.withUTF8 { copy(UnsafeRawBufferPointer($0)) } + } + + func buf(_ bytes: Data?) -> Buf { + bytes.map { buf($0) } ?? none + } + + func buf(_ text: Swift.String?) -> Buf { + text.map { buf($0) } ?? none + } + + func hold(_ handle: Handle) -> OpaquePointer? { + let raw = handle.hold() + held.append(handle) + return raw + } + + func hold(_ handle: Handle?) -> OpaquePointer? { + handle.map { hold($0) } ?? nil + } + + /// `items` as a C array, each lowered by `lower`: never null, so an empty one is + /// told apart from a missing one. + func list(_ items: [T], _ lower: (T) throws -> C) rethrows -> (UnsafePointer, Int) { + let array = UnsafeMutablePointer.allocate(capacity: Swift.max(items.count, 1)) + blocks.append(UnsafeMutableRawPointer(array)) + for (i, item) in items.enumerated() { + (array + i).initialize(to: try lower(item)) + } + return (UnsafePointer(array), items.count) + } + + /// Release what the call held, once it returns. + func finish() { + for handle in held { + handle.release() + } + for block in blocks { + block.deallocate() + } + } + } + + /// An array C passes, lowered into `arena`. + @inline(__always) + static func vec( + _ list: (UnsafePointer, Int), _ make: (UnsafePointer?, Int, UnsafeMutableRawPointer?) -> V + ) -> V { + make(list.0, list.1, nil) + } + + /// What Rust holds of a Swift value: a callback's implementation, or a waker's closure. + final class Box { + let value: T + + init(_ value: T) { + self.value = value + } + + static func unbox(_ data: UnsafeMutableRawPointer?) -> T { + Unmanaged>.fromOpaque(data!).takeUnretainedValue().value + } + + static func release(_ data: UnsafeMutableRawPointer?) { + Unmanaged>.fromOpaque(data!).release() + } + + /// `value`, retained until Rust releases it. + static func give(_ value: T) -> UnsafeMutableRawPointer { + Unmanaged.passRetained(Box(value)).toOpaque() + } + } + + /// `wake` as a waker Rust holds until it drops it, calling it from any thread. + static func waker(_ wake: @escaping @Sendable () -> Void) -> CWaker { + CWaker( + wake: { data in Box<@Sendable () -> Void>.unbox(data)() }, + drop: { data in Box<@Sendable () -> Void>.release(data) }, + data: Box.give(wake)) + } + + /// The waker of a future's first poll, which wakes nothing: a poll that pends polls + /// again with its signal's. + static var idle: CWaker { CWaker(wake: nil, drop: nil, data: nil) } + + /// What a future parks on: it latches a wake that lands between a poll that pends and + /// the park, so the next poll sees it. + final class Signal: @unchecked Sendable { + static let idle = 0 + static let woken = 1 + static let parked = 2 + + /// Which of those it is, changed only atomically. + private let state = UnsafeMutablePointer.allocate(capacity: 1) + /// The parked task, written before `state` is `parked` and read after. + private var parked: UnsafeContinuation? + + init() { + state.initialize(to: Signal.idle) + } + + deinit { + state.deallocate() + } + + /// This as a waker: given, holding a reference Rust drops, the first time; lent, + /// which Rust reuses as the same waker, after. + func waker(given: Bool) -> CWaker { + let data = given ? Unmanaged.passRetained(self).toOpaque() : Unmanaged.passUnretained(self).toOpaque() + return CWaker( + wake: { data in Unmanaged.fromOpaque(data!).takeUnretainedValue().wake() }, + drop: { data in Unmanaged.fromOpaque(data!).release() }, + data: data) + } + + /// Resume the parked task, or else latch the wake for the next park. The task + /// resumes on its own executor, never inside the wake. + func wake() { + if exchange(state, Signal.woken) == Signal.parked { + parked!.resume() + } + } + + /// Forget any wake before the next poll, which sees whatever it was for. + func reset() { + store(state, Signal.idle) + } + + /// Suspend until a wake since `reset`, at once if one already landed. + func park() async { + await withUnsafeContinuation { (continuation: UnsafeContinuation) in + parked = continuation + if swap(state, Signal.idle, Signal.parked) != Signal.idle { + continuation.resume() + } + } + } + } + + /// Await the future `raw`: poll it until `poll` returns its output, then free it. A + /// cancelled task frees it unfinished, which cancels it in Rust and loses nothing, and + /// throws `CancellationError`. Its first poll makes no signal, which only a future + /// that pends needs. + static func run( + _ raw: OpaquePointer?, _ free: @convention(c) (OpaquePointer?) -> Void, + _ poll: (OpaquePointer?, CWaker) throws -> T? + ) async throws -> T { + defer { free(raw) } + // A task cancelled already takes nothing, even what is ready. + try Task.checkCancellation() + if let output = try poll(raw, idle) { + return output + } + let signal = Signal() + var given = false + return try await withTaskCancellationHandler { + while true { + signal.reset() + try Task.checkCancellation() + if let output = try poll(raw, signal.waker(given: !given)) { + return output + } + given = true + await signal.park() + } + } onCancel: { + signal.wake() + } + } +} diff --git a/ffi/src/abi.rs b/ffi/src/abi.rs index f983bf813..e2cc094a4 100644 --- a/ffi/src/abi.rs +++ b/ffi/src/abi.rs @@ -371,7 +371,7 @@ impl<'a> Abi<'a> { } /// A type's name within the prefix, for naming the structs and instances built from it. - fn key(&self, ty: &Type) -> String { + pub fn key(&self, ty: &Type) -> String { let strip = |name: String| name[self.prefix.len() + 1..].to_string(); match ty { Type::Unit => "unit".to_string(), @@ -601,8 +601,8 @@ impl<'a> Abi<'a> { .map(|r| r.span) .or_else(|| api.objects.first().map(|o| o.span)) .unwrap_or(rustc_span::DUMMY_SP); - // C's runtime, then what C++'s adds in the namespace its types share, and Kotlin's - // in its package. + // C's runtime, then what C++'s adds in the namespace its types share, Kotlin's in its + // package, and Swift's in its module and the atomics header its runtime adds. for runtime in [ "contract", "buf", @@ -619,10 +619,15 @@ impl<'a> Abi<'a> { "detail", "native", "bytes", + "atomic_load", + "atomic_store", + "atomic_add", + "atomic_exchange", + "atomic_cas", ] { self.claim(self.rt(runtime), "the runtime".to_string(), crate_span); } - for constant in ["CONTRACT", "POLL_PENDING", "POLL_READY", "POLL_ERROR"] { + for constant in ["CONTRACT", "POLL_PENDING", "POLL_READY", "POLL_ERROR", "ATOMICS_H"] { let name = format!("{}_{constant}", self.prefix.to_uppercase()); self.claim(name, "the runtime".to_string(), crate_span); } diff --git a/ffi/src/glue.rs b/ffi/src/glue.rs index c6e20ea3f..6d3babebf 100644 --- a/ffi/src/glue.rs +++ b/ffi/src/glue.rs @@ -39,7 +39,8 @@ impl Crate<'_> { } /// Write the crate into `out`: `Cargo.toml`, `src/`, `include/.h` and `.hpp`, - /// and `kotlin/.kt`, whose JNI functions the crate's `kotlin` feature builds. + /// `kotlin/.kt`, whose JNI functions the crate's `kotlin` feature builds, and + /// `swift/.swift` with the clang module it imports the header through. pub fn write(&self, out: &Path) -> Result<(), String> { // Cargo unifies in what the configured package enables in each other export. let dependencies: String = (self.exports.iter()) @@ -101,6 +102,7 @@ impl Crate<'_> { let (header, contract) = crate::c::header(self.abi, &self.packages()); let cpp = crate::cpp::header(self.abi, &self.packages())?; let kotlin = crate::kotlin::generate(self.abi, &self.packages(), contract)?; + let swift = crate::swift::generate(self.abi, &self.packages(), contract)?; let mut generator = Gen { abi: self.abi, packages: self.packages(), @@ -123,6 +125,13 @@ impl Crate<'_> { crate::driver::write( &out.join("kotlin").join(format!("{}.pro", self.abi.prefix)), &kotlin.rules, + )?; + let swift_dir = out.join("swift"); + crate::driver::write(&swift_dir.join(format!("{}.swift", self.abi.prefix)), &swift.swift)?; + crate::driver::write(&swift_dir.join("module.modulemap"), &swift.modulemap)?; + crate::driver::write( + &swift_dir.join(format!("{}_atomics.h", self.abi.prefix)), + &swift.atomics, ) } } diff --git a/ffi/src/main.rs b/ffi/src/main.rs index 21ee5416f..4d1b0afaf 100644 --- a/ffi/src/main.rs +++ b/ffi/src/main.rs @@ -29,6 +29,7 @@ mod driver; mod glue; mod ir; mod kotlin; +mod swift; use std::process::ExitCode; diff --git a/ffi/src/swift.rs b/ffi/src/swift.rs new file mode 100644 index 000000000..19f0e9930 --- /dev/null +++ b/ffi/src/swift.rs @@ -0,0 +1,1525 @@ +//! Swift over the C ABI: `swift/.swift`, which imports the C header as the clang +//! module `swift/module.modulemap` declares, beside the atomics its runtime needs. +//! +//! Each name is its C name without the prefix, as Swift spells it: `moq_track_producer` +//! is `TrackProducer`, and its method `moq_track_producer_append_group` is +//! `TrackProducer.appendGroup`. A keyword is quoted in backticks, and a member named as one +//! Swift gives its type takes a trailing `_`. An object is a final class owning its handle, +//! freed in `deinit` or sooner by `close`; a record a struct; an enum a Swift enum, with +//! its data as associated values; a trait a protocol; a closure a function type; a future +//! an `async` function; an error a thrown `Swift.Error`. The runtime they share, +//! [`runtime/rt.swift`](../runtime/rt.swift), holds the handles, the arena a call lowers +//! nested values into, and the signal a future parks on. +//! +//! A call passes bytes and text at its top level as views of Swift's own memory, and +//! returned bytes are `Data` lending Rust's buffer, so neither is copied; nested in another +//! value, bytes passed in are copied into the call's arena. + +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Write; + +use crate::abi::{self, Abi, Ty}; +use crate::ir::{Access, Function, Int, Literal, Receiver, Type}; + +const RUNTIME: &str = include_str!("../runtime/rt.swift"); + +/// What the generator writes: the Swift file, the clang module map it imports the C +/// header through, and the header of atomics that module adds. +pub struct Output { + pub swift: String, + pub modulemap: String, + pub atomics: String, +} + +/// The name of the clang module of the C header: `api` is `ApiFFI`. +fn module(prefix: &str) -> String { + format!("{}FFI", pascal(prefix)) +} + +/// The Swift for `abi`, whose header's contract is `contract`, or the names two items +/// claim alike. +pub fn generate(abi: &Abi<'_>, packages: &str, contract: u64) -> Result { + let mut g = Gen { + abi, + types: String::new(), + top: String::new(), + natives: String::new(), + names: BTreeMap::new(), + errors: Vec::new(), + thrown: BTreeMap::new(), + conforms: BTreeSet::new(), + fresh: 0, + }; + g.write(); + if !g.errors.is_empty() { + return Err(g.errors.join("\n")); + } + let p = &abi.prefix; + let upper = p.to_uppercase(); + let module = module(p); + let mut swift = format!( + "// Generated by rust2-ffi: Swift over the C ABI of the pub API of {packages}. Do not edit.\n\n\ + #if canImport(FoundationEssentials)\nimport FoundationEssentials\n#else\nimport Foundation\n#endif\nimport {module}\n\n" + ); + let _ = write!( + swift, + "/// The runtime, and its names for the export's C types and functions.\nenum Native {{\n\ + \tstatic let file = \"{p}.swift\"\n\n\ + \ttypealias Buf = {p}_buf\n\ + \ttypealias CWaker = {p}_waker\n\ + \ttypealias CDuration = {p}_duration\n\ + \ttypealias CAddr = {p}_socket_addr\n\n\ + \tstatic func contract() -> Swift.Bool {{\n\t\t{p}_contract() == 0x{contract:016x} && {upper}_CONTRACT == 0x{contract:016x}\n\t}}\n\n\ + \tstatic func free(_ buf: Buf) {{\n\t\t{p}_buf_free(buf)\n\t}}\n\n\ + \tstatic func load(_ state: UnsafeMutablePointer) -> Swift.Int {{\n\t\t{p}_atomic_load(state)\n\t}}\n\n\ + \tstatic func store(_ state: UnsafeMutablePointer, _ value: Swift.Int) {{\n\t\t{p}_atomic_store(state, value)\n\t}}\n\n\ + \tstatic func add(_ state: UnsafeMutablePointer, _ value: Swift.Int) -> Swift.Int {{\n\t\t{p}_atomic_add(state, value)\n\t}}\n\n\ + \tstatic func exchange(_ state: UnsafeMutablePointer, _ value: Swift.Int) -> Swift.Int {{\n\t\t{p}_atomic_exchange(state, value)\n\t}}\n\n\ + \t/// Set `state` to `new` if it is `old`, returning what it was.\n\ + \tstatic func swap(_ state: UnsafeMutablePointer, _ old: Swift.Int, _ new: Swift.Int) -> Swift.Int {{\n\t\t{p}_atomic_cas(state, old, new)\n\t}}\n}}\n\n" + ); + swift.push_str(RUNTIME); + swift.push('\n'); + swift.push_str(&g.types); + swift.push_str(&g.top); + if !g.natives.is_empty() { + let _ = write!(swift, "extension Native {{\n{}}}\n", g.natives); + } + while swift.ends_with("\n\n") { + swift.pop(); + } + let modulemap = format!( + "// Generated by rust2-ffi: the C ABI of {packages} as a clang module, which {p}.swift imports. Do not edit.\n\n\ + module {module} {{\n\theader \"../include/{p}.h\"\n\theader \"{p}_atomics.h\"\n\texport *\n}}\n" + ); + let atomics = format!( + "/* Generated by rust2-ffi: the atomics {p}.swift's runtime uses, which Swift's standard\n * library lacks before macOS 15 and iOS 18. Do not edit. */\n\n\ + #ifndef {upper}_ATOMICS_H\n#define {upper}_ATOMICS_H\n\n#include \n\n\ + static inline intptr_t {p}_atomic_load(const intptr_t *p) {{\n\treturn __atomic_load_n(p, __ATOMIC_ACQUIRE);\n}}\n\n\ + static inline void {p}_atomic_store(intptr_t *p, intptr_t v) {{\n\t__atomic_store_n(p, v, __ATOMIC_RELEASE);\n}}\n\n\ + static inline intptr_t {p}_atomic_add(intptr_t *p, intptr_t v) {{\n\treturn __atomic_add_fetch(p, v, __ATOMIC_ACQ_REL);\n}}\n\n\ + static inline intptr_t {p}_atomic_exchange(intptr_t *p, intptr_t v) {{\n\treturn __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL);\n}}\n\n\ + static inline intptr_t {p}_atomic_cas(intptr_t *p, intptr_t old, intptr_t new_) {{\n\t__atomic_compare_exchange_n(p, &old, new_, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE);\n\treturn old;\n}}\n\n\ + #endif\n" + ); + Ok(Output { + swift: swift + "\n", + modulemap, + atomics, + }) +} + +/// The standard library's names the runtime and the generated code use unqualified, which +/// an exported type or function would shadow, and `Swift`, which qualifies the rest. The +/// runtime's own are the C ABI's: `Native` and `SocketAddr` are claimed as +/// `_native` and `_socket_addr`. +const TAKEN: &[&str] = &[ + "Array", + "Bool", + "CustomDebugStringConvertible", + "CustomStringConvertible", + "Data", + "Equatable", + "Hashable", + "Int", + "Int64", + "Never", + "OpaquePointer", + "Optional", + "Sendable", + "StaticString", + "Swift", + "Task", + "UInt", + "UInt16", + "UInt32", + "UInt64", + "UInt8", + "UTF8", + "Unicode", + "Unmanaged", + "UnsafeBufferPointer", + "UnsafeContinuation", + "UnsafeMutablePointer", + "UnsafeMutableRawPointer", + "UnsafePointer", + "UnsafeRawBufferPointer", + "Void", + "fatalError", + "withTaskCancellationHandler", + "withUnsafeBytes", + "withUnsafeContinuation", + "withUnsafeMutableBytes", + "withUnsafeMutablePointer", + "withoutActuallyEscaping", +]; + +/// One parameter, as the call passes it: the closure it runs inside, holding what it +/// passes until it returns, the C arguments, and what closes once the call returns. +#[derive(Default)] +struct Arg { + open: Option<(String, String)>, + exprs: Vec, + after: Vec, +} + +struct Gen<'a> { + abi: &'a Abi<'a>, + /// The types, then the top-level functions and constants, then `Native`'s additions. + types: String, + top: String, + natives: String, + /// Each top-level Swift name, with what claimed it. + names: BTreeMap, + errors: Vec, + /// The struct each error type that is no exported type throws as, by name. + thrown: BTreeMap, + /// The exported types thrown as errors, which conform to `Swift.Error`. + conforms: BTreeSet, + fresh: usize, +} + +impl<'a> Gen<'a> { + fn fresh(&mut self, base: &str) -> String { + self.fresh += 1; + format!("{base}{}_", self.fresh) + } + + /// A C name without the prefix. + fn bare<'n>(&self, c: &'n str) -> &'n str { + &c[self.abi.prefix.len() + 1..] + } + + /// The Swift name of the C type `c`. + fn class(&self, c: &str) -> String { + pascal(self.bare(c)) + } + + /// Claim the top-level Swift name `name`, a function's [`full`] one, for `what`. + fn claim(&mut self, name: &str, what: String) { + if TAKEN.contains(&name.split('(').next().unwrap_or(name)) { + self.errors.push(format!( + "{what} is `{name}` in Swift, which its runtime names: rename it in the config" + )); + } else if let Some(first) = self.names.insert(name.to_string(), what.clone()) { + self.errors.push(format!( + "{first} and {what} are both `{name}` in Swift: rename one in the config" + )); + } + } + + /// Check that no two members of the type `rust` share a full Swift name, as [`full`] + /// spells it. + fn distinct(&mut self, rust: &str, members: &[String]) { + let mut seen = BTreeSet::new(); + for member in members { + if !seen.insert(member) { + self.errors.push(format!( + "two members of `{rust}` are both `{member}` in Swift: rename one in the config" + )); + } + } + } + + /// The Swift type of `ty`, as a value, a parameter, or a result. + fn sw(&self, ty: &Type) -> String { + let abi = self.abi; + match ty { + Type::Unit => "Void".to_string(), + Type::Bool => "Swift.Bool".to_string(), + Type::Int(int) => sw_int(*int).to_string(), + Type::F32 => "Swift.Float".to_string(), + Type::F64 => "Swift.Double".to_string(), + Type::Char => "Swift.Unicode.Scalar".to_string(), + Type::Str | Type::String | Type::Path | Type::PathBuf => "Swift.String".to_string(), + Type::Slice | Type::Vec | Type::Bytes => "Data".to_string(), + Type::Duration => "Swift.Duration".to_string(), + Type::SocketAddr(_) => "SocketAddr".to_string(), + Type::List(inner) => format!("[{}]", self.sw(inner)), + Type::Waker => "@escaping @Sendable () -> Void".to_string(), + Type::Option(inner) => format!("{}?", self.sw(inner)), + Type::Box(inner) | Type::Ref(_, inner) => self.sw(inner), + Type::Record(i) => self.class(&abi.record(*i)), + Type::Enum(i) => self.class(&abi.enumeration(*i)), + Type::Object(i) => self.class(&abi.object(*i)), + Type::Callback(i, _) => format!("any {}", self.class(&abi.callback(*i))), + Type::Closure(params, ret) => { + let params: Vec = params.iter().map(|p| self.sw(p)).collect(); + format!("@Sendable ({}) -> {}", params.join(", "), self.sw(ret)) + } + Type::Range(_) | Type::Bound(_) | Type::Bounds(_) => self.class(&abi.range(ty)), + Type::Tuple(elements) => match &elements[..] { + // Swift has no tuple of one: it is its element, which loses nothing. + [element] => self.sw(element), + elements => { + let elements: Vec = elements.iter().map(|e| self.sw(e)).collect(); + format!("({})", elements.join(", ")) + } + }, + } + } + + /// The Swift spelling of a C type, for a local the call writes. + fn c_sw(&self, ty: &Ty) -> String { + let abi = self.abi; + match ty { + Ty::Bool => "Swift.Bool".to_string(), + Ty::Int(int) => c_int(*int).to_string(), + Ty::F32 => "Swift.Float".to_string(), + Ty::F64 => "Swift.Double".to_string(), + Ty::Char => "Swift.UInt32".to_string(), + Ty::Buf => abi.rt("buf"), + Ty::Duration => abi.rt("duration"), + Ty::SocketAddr => abi.rt("socket_addr"), + Ty::Struct(n) | Ty::Enum(n) => n.clone(), + Ty::Handle { .. } | Ty::Future(_) => "OpaquePointer?".to_string(), + Ty::Waker => abi.rt("waker"), + other => unreachable!("no local is {other:?}"), + } + } + + /// The zero a local of C type `ty` starts as. + fn zero(&self, ty: &Ty) -> String { + match ty { + Ty::Bool => "false".to_string(), + Ty::Int(_) | Ty::F32 | Ty::F64 | Ty::Char => "0".to_string(), + Ty::Handle { .. } | Ty::Future(_) => "nil".to_string(), + Ty::Enum(n) => format!("{n}(rawValue: 0)"), + other => format!("{}()", self.c_sw(other)), + } + } + + /// Whether lowering a `ty` nested in another value needs the call's arena: to copy + /// bytes, build an array, or hold a handle. + fn arena(&self, ty: &Type) -> bool { + let api = self.abi.api; + match ty { + ty if Abi::is_buf(ty) => true, + Type::List(_) | Type::Object(_) => true, + Type::Option(inner) | Type::Box(inner) | Type::Ref(_, inner) => self.arena(inner), + Type::Tuple(elements) => elements.iter().any(|e| self.arena(e)), + Type::Record(i) => api.records[*i].fields.iter().any(|f| self.arena(&f.ty)), + Type::Enum(i) => (api.enums[*i].variants.iter()).any(|v| v.fields.iter().any(|f| self.arena(&f.ty))), + _ => false, + } + } + + /// The arena argument a value's `_lower` takes, if it needs one. + fn arena_arg(&self, ty: &Type) -> &'static str { + match self.arena(ty) { + true => "a_", + false => "", + } + } + + /// The C value of the Swift value `e` of type `ty`, nested in a value the call passes: + /// bytes copied into the arena `a_`, and handles held by it. + fn lower(&mut self, ty: &Type, e: &str) -> String { + let abi = self.abi; + if Abi::is_buf(ty) { + return format!("a_.buf({e})"); + } + match ty { + Type::Unit => unreachable!("no value is unit"), + Type::Bool | Type::F32 | Type::F64 => e.to_string(), + Type::Int(int) => lower_int(*int, e), + Type::Char => format!("{e}.value"), + Type::Duration => format!("Native.duration({e})"), + Type::SocketAddr(_) => format!("Native.socketAddr({e})"), + Type::Range(_) | Type::Bound(_) | Type::Bounds(_) => format!("{e}._lower()"), + Type::Waker => format!("Native.waker({e})"), + Type::List(inner) => { + let x = self.fresh("x"); + let item = self.lower(inner, &x); + format!( + "Native.vec(a_.list({e}) {{ {x} in {item} }}, {}.init(ptr:len:owner:))", + abi.list(inner) + ) + } + Type::Option(inner) => match inner.base() { + Type::Object(_) => format!("a_.hold({e}?.handle)"), + _ if Abi::is_buf(inner) => format!("a_.buf({e})"), + Type::List(elem) => { + let v = self.fresh("v"); + let some = self.lower(inner, &v); + format!("({e}.map {{ {v} in {some} }} ?? {}())", abi.list(elem)) + } + _ => { + let c = abi.option(inner); + let v = self.fresh("v"); + let some = self.lower(inner, &v); + format!("({e}.map {{ {v} in {c}(some: true, value: {some}) }} ?? {c}())") + } + }, + Type::Box(inner) | Type::Ref(_, inner) => self.lower(inner, e), + Type::Record(_) | Type::Enum(_) => format!("{e}._lower({})", self.arena_arg(ty)), + Type::Object(_) => format!("a_.hold({e}.handle)"), + Type::Callback(i, _) => format!("Native.lower_{}({e})", abi.callback(*i)), + Type::Tuple(elements) => { + let c = abi.tuple(elements); + let fields: Vec = match &elements[..] { + [element] => vec![format!("_0: {}", self.lower(element, e))], + elements => (elements.iter().enumerate()) + .map(|(n, ty)| format!("_{n}: {}", self.lower(ty, &format!("{e}.{n}")))) + .collect(), + }; + format!("{c}({})", fields.join(", ")) + } + Type::Closure(..) => unreachable!("a closure lowers to its trampoline"), + _ => unreachable!("a buffer is above"), + } + } + + /// The Swift value of the C value `e` of type `ty`, which it takes ownership of. `e` + /// is named, so it may be read more than once. + fn lift(&mut self, ty: &Type, e: &str) -> String { + let abi = self.abi; + match ty { + Type::Str | Type::String => format!("Native.string({e})"), + Type::Path | Type::PathBuf => format!("Native.path({e})"), + Type::Slice | Type::Vec | Type::Bytes => format!("Native.data({e})"), + Type::Bool | Type::F32 | Type::F64 => e.to_string(), + Type::Int(int) => lift_int(*int, e), + Type::Char => format!("Native.scalar({e})"), + Type::Duration => format!("Native.duration({e})"), + Type::SocketAddr(_) => format!("Native.socketAddr({e})"), + Type::Range(_) | Type::Bound(_) | Type::Bounds(_) => format!("{}._lift({e})", self.sw(ty)), + Type::List(inner) => { + let (v, c) = (self.fresh("v"), self.fresh("c")); + let item = self.lift(inner, &c); + format!( + "{{ () -> {} in var {v} = {e}; defer {{ {v}.len = 0; {}_free({v}) }}; return Native.list({v}.ptr, {v}.len) {{ {c} in {item} }} }}()", + self.sw(ty), + abi.list(inner) + ) + } + Type::Option(inner) => match inner.base() { + Type::Object(_) => format!("{e}.map {{ {}(raw: $0) }}", self.sw(inner)), + Type::Str | Type::String => format!("Native.string(option: {e})"), + Type::Path | Type::PathBuf => format!("Native.path(option: {e})"), + Type::Slice | Type::Vec | Type::Bytes => format!("Native.data(option: {e})"), + Type::List(_) => format!( + "({e}.ptr == nil ? nil : {} as {}?)", + self.lift(inner, e), + self.sw(inner) + ), + _ => format!( + "({e}.some ? {} as {}? : nil)", + self.lift(inner, &format!("{e}.value")), + self.sw(inner) + ), + }, + Type::Box(inner) | Type::Ref(_, inner) => self.lift(inner, e), + Type::Record(_) | Type::Enum(_) => format!("{}._lift({e})", self.sw(ty)), + Type::Object(_) => format!("{}(raw: {e})", self.sw(ty)), + Type::Tuple(elements) => match &elements[..] { + [element] => self.lift(element, &format!("{e}._0")), + elements => { + let lifted: Vec = (elements.iter().enumerate()) + .map(|(n, ty)| self.lift(ty, &format!("{e}._{n}"))) + .collect(); + format!("({})", lifted.join(", ")) + } + }, + other => unreachable!("{other:?} is never returned"), + } + } + + /// The Swift value of the C value `e` of type `ty`, which C lends a callback or closure + /// for its call: bytes and text copied, and a scalar as it is. + fn lift_lent(&mut self, ty: &Type, e: &str) -> String { + match ty { + Type::Str | Type::String => format!("Native.string(lent: {e})"), + Type::Path | Type::PathBuf => format!("Native.path(lent: {e})"), + Type::Slice | Type::Vec | Type::Bytes => format!("Native.data(lent: {e})"), + Type::Option(inner) => match inner.base() { + Type::Str | Type::String => format!("Native.string(lentOption: {e})"), + Type::Path | Type::PathBuf => format!("Native.path(lentOption: {e})"), + Type::Slice | Type::Vec | Type::Bytes => format!("Native.data(lentOption: {e})"), + _ => self.lift(ty, e), + }, + Type::Ref(_, inner) => self.lift_lent(inner, e), + ty => self.lift(ty, e), + } + } + + fn write(&mut self) { + let abi = self.abi; + let api = abi.api; + for (c, ty) in &abi.ranges.clone() { + self.range(c, ty); + } + for constant in &api.constants { + let name = self.bare(&abi.name(&constant.path)).to_uppercase(); + self.claim(&name, format!("const `{}`", constant.path.join("::"))); + let (ty, value) = match constant.value { + Literal::Bool(b) => ("Swift.Bool".to_string(), b.to_string()), + Literal::Int(int, v) => (sw_int(int).to_string(), v.to_string()), + }; + let _ = write!( + self.top, + "{}public let {name}: {ty} = {value}\n\n", + docs(&constant.docs, "") + ); + } + for i in 0..api.records.len() { + self.record(i); + } + for i in 0..api.enums.len() { + self.enumeration(i); + } + for i in 0..api.objects.len() { + self.object(i); + } + for i in 0..api.callbacks.len() { + self.callback(i); + } + for function in &api.functions { + if let Some((code, name)) = self.function(function, None, None) { + self.claim(&name, format!("fn `{}`", function.path.join("::"))); + self.top.push_str(&code); + self.top.push('\n'); + } + } + for (name, ty) in &self.thrown.clone() { + let sw = self.sw(ty); + let _ = write!( + self.types, + "/// A `{}` Rust returned as an error.\npublic struct {name}: Swift.Error, CustomStringConvertible {{\n\tpublic let error: {sw}\n\n\tpublic var description: Swift.String {{\n\t\t\"\\(error)\"\n\t}}\n}}\n\n", + sw.trim_start_matches("Swift.") + ); + } + for name in &self.conforms.clone() { + let _ = write!(self.types, "extension {name}: Swift.Error {{}}\n\n"); + } + } + + /// A struct for a range or pair of bounds, or an enum for a bound, and its conversions. + fn range(&mut self, c: &str, ty: &Type) { + let name = self.class(c); + self.claim(&name, format!("`{c}`")); + let p = &self.abi.prefix; + let upper = format!("{}_BOUND", p.to_uppercase()); + let code = match ty { + Type::Range(int) => { + let sw = sw_int(*int); + let lower = |e: &str| lower_int(*int, e); + let lift = |e: &str| lift_int(*int, e); + format!( + "/// Rust's `Range`: from `start`, up to but not including `end`.\npublic struct {name}: Hashable, Sendable {{\n\tpublic var start: {sw}\n\tpublic var end: {sw}\n\n\tpublic init(start: {sw}, end: {sw}) {{\n\t\tself.start = start\n\t\tself.end = end\n\t}}\n\n\tstatic func _lift(_ c: {c}) -> {name} {{\n\t\t{name}(start: {}, end: {})\n\t}}\n\n\tfunc _lower() -> {c} {{\n\t\t{c}(start: {}, end: {})\n\t}}\n}}\n\n", + lift("c.start"), + lift("c.end"), + lower("start"), + lower("end"), + ) + } + Type::Bound(int) => { + let sw = sw_int(*int); + format!( + "/// Rust's `Bound`: what a range's start or end bound does.\npublic enum {name}: Hashable, Sendable {{\n\tcase unbounded\n\tcase included({sw})\n\tcase excluded({sw})\n\n\tstatic func _lift(_ c: {c}) -> {name} {{\n\t\tswitch c.kind {{\n\t\tcase {upper}_UNBOUNDED: return .unbounded\n\t\tcase {upper}_INCLUDED: return .included({})\n\t\tcase {upper}_EXCLUDED: return .excluded({})\n\t\tdefault: fatalError(\"{c}: a bound's kind is invalid\")\n\t\t}}\n\t}}\n\n\tfunc _lower() -> {c} {{\n\t\tswitch self {{\n\t\tcase .unbounded: return {c}(kind: {upper}_UNBOUNDED, value: 0)\n\t\tcase .included(let value): return {c}(kind: {upper}_INCLUDED, value: {})\n\t\tcase .excluded(let value): return {c}(kind: {upper}_EXCLUDED, value: {})\n\t\t}}\n\t}}\n}}\n\n", + lift_int(*int, "c.value"), + lift_int(*int, "c.value"), + lower_int(*int, "value"), + lower_int(*int, "value"), + ) + } + Type::Bounds(int) => { + let bound = self.class(&self.abi.range(&Type::Bound(*int))); + format!( + "/// A range's start and end bounds, as Rust's `RangeBounds` gives them.\npublic struct {name}: Hashable, Sendable {{\n\tpublic var start: {bound}\n\tpublic var end: {bound}\n\n\tpublic init(start: {bound}, end: {bound}) {{\n\t\tself.start = start\n\t\tself.end = end\n\t}}\n\n\tstatic func _lift(_ c: {c}) -> {name} {{\n\t\t{name}(start: {bound}._lift(c.start), end: {bound}._lift(c.end))\n\t}}\n\n\tfunc _lower() -> {c} {{\n\t\t{c}(start: start._lower(), end: end._lower())\n\t}}\n}}\n\n" + ) + } + other => unreachable!("{other:?} is no range"), + }; + self.types.push_str(&code); + } + + /// The members a type's std traits export, with the protocols they conform it to: + /// `Display` is `description`, `Debug` `debugDescription`, `PartialEq` `==`, `Default` + /// a static `default()`, and `Clone` an object's `clone()`. + fn traits( + &mut self, + ty: &Type, + c: &str, + t: crate::ir::Traits, + body: &mut Vec, + members: &mut Vec, + protocols: &mut Vec<&'static str>, + ) { + let name = self.sw(ty); + let object = matches!(ty, Type::Object(_)); + // The receiver: a held handle, or the value lowered. + let (open, this, close) = match object { + true => ("self.handle.with { h_ in ".to_string(), "h_", " }"), + false => { + let arena = match self.arena(ty) { + true => "let a_ = Native.Arena()\n\t\tdefer { a_.finish() }\n\t\t", + false => "", + }; + ( + format!( + "Native.check()\n\t\t{arena}var c_ = self._lower({})\n\t\treturn ", + self.arena_arg(ty) + ), + "&c_", + "", + ) + } + }; + let text = |open: &str, call: &str| match object { + true => format!("Native.string({open}{call}{close})"), + false => format!("{open}Native.string({call})"), + }; + if t.display { + protocols.push("CustomStringConvertible"); + members.push("description".to_string()); + body.push(format!( + "\tpublic var description: Swift.String {{\n\t\t{}\n\t}}\n", + text(&open, &format!("{c}_to_string({this})")) + )); + } + if t.debug { + protocols.push("CustomDebugStringConvertible"); + members.push("debugDescription".to_string()); + body.push(format!( + "\t/// Rust's `Debug` form.\n\tpublic var debugDescription: Swift.String {{\n\t\t{}\n\t}}\n", + text(&open, &format!("{c}_debug({this})")) + )); + } + if object && t.clone { + members.push("clone".to_string()); + body.push(format!( + "\t/// A new handle to a clone.\n\tpublic func clone() -> {name} {{\n\t\t{name}(raw: self.handle.with {{ {c}_clone($0) }})\n\t}}\n" + )); + } + if t.eq { + protocols.push("Equatable"); + let compare = match object { + true => format!("a.handle.with {{ a_ in b.handle.with {{ b_ in {c}_eq(a_, b_) }} }}"), + false => { + let arena = match self.arena(ty) { + true => "let a_ = Native.Arena()\n\t\tdefer { a_.finish() }\n\t\t", + false => "", + }; + let arg = self.arena_arg(ty); + format!( + "Native.check()\n\t\t{arena}var ca_ = a._lower({arg})\n\t\tvar cb_ = b._lower({arg})\n\t\treturn {c}_eq(&ca_, &cb_)" + ) + } + }; + body.push(format!( + "\tpublic static func == (a: {name}, b: {name}) -> Swift.Bool {{\n\t\t{compare}\n\t}}\n" + )); + } + if t.default { + members.push("static default".to_string()); + let lift = match ty { + Type::Object(_) => format!("{name}(raw: {c}_default())"), + _ => format!("{name}._lift({c}_default())"), + }; + body.push(format!( + "\t/// Rust's `Default`.\n\tpublic static func `default`() -> {name} {{\n\t\tNative.check()\n\t\treturn {lift}\n\t}}\n" + )); + } + } + + /// Each method of a type, as members of its Swift type. + fn methods( + &mut self, + methods: &[Function], + owner: &Ty, + ty: &Type, + body: &mut Vec, + members: &mut Vec, + ) { + for method in methods { + if let Some((code, member)) = self.function(method, Some(owner), Some(ty)) { + members.push(member); + body.push(indent(&code)); + } + } + } + + fn record(&mut self, i: usize) { + let abi = self.abi; + let record = &abi.api.records[i]; + let c = abi.record(i); + let name = self.class(&c); + let rust = abi.api.rust(&record.path, &record.args); + self.claim(&name, format!("struct `{rust}`")); + let ty = Type::Record(i); + let fields: Vec<(String, String)> = (record.fields.iter()) + .map(|f| (field_name(&f.name), self.sw(&f.ty))) + .collect(); + let mut members: Vec = fields.iter().map(|(f, _)| unquote(f)).collect(); + let mut body: Vec = (record.fields.iter().zip(&fields)) + .map(|(f, (name, sw))| format!("{}\tpublic var {name}: {sw}\n", docs(&f.docs, "\t"))) + .collect(); + let params: Vec = fields + .iter() + .map(|(f, sw)| match record.tuple { + true => format!("_ {f}: {sw}"), + false => format!("{f}: {sw}"), + }) + .collect(); + let assigns: String = fields.iter().map(|(f, _)| format!("\t\tself.{f} = {f}\n")).collect(); + body.push(format!("\tpublic init({}) {{\n{assigns}\t}}\n", params.join(", "))); + let mut protocols = vec!["Sendable"]; + self.traits(&ty, &c, record.traits, &mut body, &mut members, &mut protocols); + self.methods(&record.methods, &Ty::Struct(c.clone()), &ty, &mut body, &mut members); + self.distinct(&rust, &members); + // Its conversions. + let lifted: Vec = (record.fields.iter().zip(&fields)) + .map(|(f, (name, _))| { + let e = format!("c.{}", kw(&abi::escape(&f.name))); + let value = self.lift(&f.ty, &e); + match record.tuple { + true => value, + false => format!("{}: {value}", unquote(name)), + } + }) + .collect(); + let lowered: Vec = (record.fields.iter().zip(&fields)) + .map(|(f, (name, _))| { + let value = self.lower(&f.ty, &format!("self.{name}")); + format!("{}: {value}", abi::escape(&f.name)) + }) + .collect(); + let arena = match self.arena(&ty) { + true => "_ a_: Native.Arena", + false => "", + }; + body.push(format!( + "\tstatic func _lift(_ c: {c}) -> {name} {{\n\t\t{name}({})\n\t}}\n", + lifted.join(", ") + )); + body.push(format!( + "\tfunc _lower({arena}) -> {c} {{\n\t\t{c}({})\n\t}}\n", + lowered.join(", ") + )); + let text = format!("{rust}\n\n{}", record.docs); + let _ = write!( + self.types, + "{}public struct {name}: {} {{\n{}}}\n\n", + docs(text.trim(), ""), + protocols.join(", "), + body.join("\n") + ); + } + + fn enumeration(&mut self, i: usize) { + let abi = self.abi; + let e = &abi.api.enums[i]; + let c = abi.enumeration(i); + let name = self.class(&c); + let rust = abi.api.rust(&e.path, &e.args); + self.claim(&name, format!("enum `{rust}`")); + let ty = Type::Enum(i); + let upper = c.to_uppercase(); + let mut members = Vec::new(); + let mut cases = Vec::new(); + let mut lifts = Vec::new(); + let mut lowers = Vec::new(); + for variant in &e.variants { + let snake = abi::snake(&variant.name); + let case = member(&snake, &["init"]); + let labels = variant.fields.iter().map(|f| match variant.tuple { + true => "_".to_string(), + false => unquote(&field_name(&f.name)), + }); + members.push(full(true, &case, labels)); + let tag = format!("{upper}_{}", snake.to_uppercase()); + let member = kw(&abi::escape(&snake)); + let fields: Vec = (variant.fields.iter()) + .map(|f| match variant.tuple { + true => self.sw(&f.ty), + false => format!("{}: {}", field_name(&f.name), self.sw(&f.ty)), + }) + .collect(); + match fields.is_empty() { + true => cases.push(format!("\tcase {case}\n")), + false => cases.push(format!("\tcase {case}({})\n", fields.join(", "))), + } + if e.plain() { + lifts.push(format!("\t\tcase {tag}: return .{case}")); + lowers.push(format!("\t\tcase .{case}: return {tag}")); + continue; + } + let lifted: Vec = (variant.fields.iter()) + .map(|f| { + let value = self.lift(&f.ty, &format!("c.{member}.{}", kw(&abi::escape(&f.name)))); + match variant.tuple { + true => value, + false => format!("{}: {value}", unquote(&field_name(&f.name))), + } + }) + .collect(); + let binds: Vec = (0..variant.fields.len()).map(|k| format!("let p{k}_")).collect(); + let payload = format!("{c}_{snake}"); + let lowered: Vec = (variant.fields.iter().enumerate()) + .map(|(k, f)| format!("{}: {}", abi::escape(&f.name), self.lower(&f.ty, &format!("p{k}_")))) + .collect(); + match variant.fields.is_empty() { + true => { + lifts.push(format!("\t\tcase {tag}: return .{case}")); + lowers.push(format!("\t\tcase .{case}: c.tag = {tag}")); + } + false => { + lifts.push(format!("\t\tcase {tag}: return .{case}({})", lifted.join(", "))); + lowers.push(format!( + "\t\tcase .{case}({}):\n\t\t\tc.tag = {tag}\n\t\t\tc.{member} = {payload}({})", + binds.join(", "), + lowered.join(", ") + )); + } + } + } + let mut body = cases; + let mut protocols = vec!["Sendable"]; + self.traits(&ty, &c, e.traits, &mut body, &mut members, &mut protocols); + let owner = abi.ty(&ty); + self.methods(&e.methods, &owner, &ty, &mut body, &mut members); + self.distinct(&rust, &members); + let (matched, invalid) = match e.plain() { + true => ("c", "an invalid value"), + false => ("c.tag", "an invalid tag"), + }; + body.push(format!( + "\tstatic func _lift(_ c: {c}) -> {name} {{\n\t\tswitch {matched} {{\n{}\n\t\tdefault: fatalError(\"{c}: {invalid}\")\n\t\t}}\n\t}}\n", + lifts.join("\n") + )); + let arena = match self.arena(&ty) { + true => "_ a_: Native.Arena", + false => "", + }; + body.push(match e.plain() { + true => format!( + "\tfunc _lower() -> {c} {{\n\t\tswitch self {{\n{}\n\t\t}}\n\t}}\n", + lowers.join("\n") + ), + false => format!( + "\tfunc _lower({arena}) -> {c} {{\n\t\tvar c = {c}()\n\t\tswitch self {{\n{}\n\t\t}}\n\t\treturn c\n\t}}\n", + lowers.join("\n") + ), + }); + let text = format!("{rust}\n\n{}", e.docs); + let _ = write!( + self.types, + "{}public enum {name}: {} {{\n{}}}\n\n", + docs(text.trim(), ""), + protocols.join(", "), + body.join("\n") + ); + } + + fn object(&mut self, i: usize) { + let abi = self.abi; + let object = &abi.api.objects[i]; + let c = abi.object(i); + let name = self.class(&c); + let rust = abi.api.rust(&object.path, &object.args); + self.claim(&name, format!("type `{rust}`")); + let ty = Type::Object(i); + let mut members = vec!["close".to_string(), "handle".to_string()]; + let mut body = vec![format!( + "\tlet handle: Native.Handle\n\n\tinit(raw: OpaquePointer?) {{\n\t\thandle = Native.Handle(raw, {c}_free, \"{name}\")\n\t}}\n\n\tinit(lent raw: OpaquePointer?) {{\n\t\thandle = Native.Handle(raw, nil, \"{name}\")\n\t}}\n\n\tdeinit {{\n\t\thandle.drop()\n\t}}\n\n\t/// Free the handle once no call holds it, rather than once Swift releases this: a\n\t/// call after it traps.\n\tpublic func close() {{\n\t\thandle.close()\n\t}}\n" + )]; + let mut protocols = vec!["@unchecked Sendable"]; + self.traits(&ty, &c, object.traits, &mut body, &mut members, &mut protocols); + let owner = Ty::Handle { + name: c.clone(), + constant: false, + }; + self.methods(&object.methods, &owner, &ty, &mut body, &mut members); + self.distinct(&rust, &members); + let text = format!("{rust}\n\n{}", object.docs); + let _ = write!( + self.types, + "{}public final class {name}: {} {{\n{}}}\n\n", + docs(text.trim(), ""), + protocols.join(", "), + body.join("\n") + ); + } + + fn callback(&mut self, i: usize) { + let abi = self.abi; + let callback = &abi.api.callbacks[i]; + let c = abi.callback(i); + let name = self.class(&c); + let rust = callback.path.join("::"); + self.claim(&name, format!("trait `{rust}`")); + let mut body = Vec::new(); + let mut members = Vec::new(); + let mut table = vec![format!("free: {{ data in Box.release(data) }}")]; + for method in &callback.methods { + let member = member(&abi::snake(method.name()), &[]); + members.push(full(false, &member, method.params.iter().map(|_| "_".to_string()))); + let params: Vec = (method.params.iter()) + .map(|p| format!("_ {}: {}", kw(&camel(&p.name)), self.sw(&p.ty))) + .collect(); + let ret = match method.ok { + Type::Unit => String::new(), + _ => format!(" -> {}", self.sw(&method.ok)), + }; + body.push(format!( + "{}\tfunc {member}({}){ret}", + docs(&method.docs, "\t"), + params.join(", ") + )); + let ps: Vec = (0..method.params.len()).map(|k| format!("p{k}_")).collect(); + let args: Vec = (method.params.iter().zip(&ps)) + .map(|(p, e)| self.lift_lent(&p.ty, e)) + .collect(); + let call = format!("Box.unbox(data).{member}({})", args.join(", ")); + let call = match method.ok { + Type::Unit => call, + _ => self.lower(&method.ok, &call), + }; + let mut closure_params = vec!["data".to_string()]; + closure_params.extend(ps); + table.push(format!( + "{}: {{ {} in {call} }}", + method.name(), + closure_params.join(", ") + )); + } + self.distinct(&rust, &members); + let text = format!( + "{rust}: implemented in Swift. Rust calls its methods from any thread, and releases\nthe implementation once it drops it.\n\n{}", + callback.docs + ); + let _ = write!( + self.types, + "{}public protocol {name}: Sendable {{\n{}\n}}\n\n", + docs(text.trim(), ""), + body.join("\n\n") + ); + let _ = write!( + self.natives, + "\tnonisolated(unsafe) static let vtable_{c}: UnsafeMutablePointer<{c}_vtable> = {{\n\t\tlet vtable = UnsafeMutablePointer<{c}_vtable>.allocate(capacity: 1)\n\t\tvtable.initialize(to: {c}_vtable(\n\t\t\t{}))\n\t\treturn vtable\n\t}}()\n\n\tstatic func lower_{c}(_ implementation: any {name}) -> {c} {{\n\t\t{c}(vtable: vtable_{c}, data: Box.give(implementation))\n\t}}\n\n", + table.join(",\n\t\t\t") + ); + } + + /// The Swift statement closing each object the value `e` of type `ty` holds, once a + /// call moved them out. + fn close(&mut self, ty: &Type, e: &str) -> String { + match ty { + Type::Option(inner) => { + let v = self.fresh("v"); + let close = self.close(inner, &v); + format!("if let {v} = {e} {{ {close} }}") + } + Type::Box(inner) => self.close(inner, e), + Type::Tuple(elements) if elements.len() == 1 => self.close(&elements[0], e), + Type::Tuple(elements) => { + let closes: Vec = (elements.iter().enumerate()) + .filter(|(_, ty)| moved(self.abi, ty)) + .map(|(k, ty)| self.close(ty, &format!("{e}.{k}"))) + .collect(); + closes.join("; ") + } + Type::List(inner) => { + let v = self.fresh("v"); + let close = self.close(inner, &v); + format!("for {v} in {e} {{ {close} }}") + } + _ => format!("{e}.close()"), + } + } + + /// How the call passes `param`, the C parameter `c`, whose Swift value is `e`, the + /// `k`th, which `function` takes. + fn arg(&mut self, ty: &Type, c: &Ty, e: &str, k: usize, tr: &str) -> (Arg, bool) { + let abi = self.abi; + let mut arg = Arg::default(); + let mut arena = false; + if moved(abi, ty) { + arg.after.push(self.close(ty, e)); + } + match (ty, c) { + (Type::Closure(lent, ret), Ty::Fn(..)) => { + let (f, g, d) = (format!("f{k}_"), format!("g{k}_"), format!("d{k}_")); + arg.open = Some(( + format!( + "{tr}withoutActuallyEscaping({e}) {{ {f} in\n\tvar {g} = {f}\n\treturn {tr}withUnsafeMutablePointer(to: &{g}) {{ {d} in" + ), + "}\n}".to_string(), + )); + arg.exprs.push(self.trampoline(ty, lent, ret)); + arg.exprs.push(d); + } + (Type::Callback(i, _), _) => arg.exprs.push(format!("Native.lower_{}({e})", abi.callback(*i))), + (Type::Waker, _) => arg.exprs.push(format!("Native.waker({e})")), + (ty, Ty::Buf) if Abi::is_buf(ty) || matches!(ty, Type::Option(inner) if Abi::is_buf(inner)) => { + let b = format!("b{k}_"); + arg.open = Some((format!("{tr}Native.view({e}) {{ {b} in"), "}".to_string())); + arg.exprs.push(b); + } + (ty, Ty::Handle { .. }) => { + let h = format!("h{k}_"); + let open = match ty { + Type::Option(_) => format!("{tr}Native.with({e}?.handle) {{ {h} in"), + _ => format!("{tr}{e}.handle.with {{ {h} in"), + }; + arg.open = Some((open, "}".to_string())); + arg.exprs.push(h); + } + (ty, _) => { + arena = self.arena(ty); + arg.exprs.push(self.lower(ty, e)); + } + } + (arg, arena) + } + + /// The C function C calls for a closure of type `ty`, lent for a call, taking `lent` + /// and returning `ret`: it lends each object as its class, closed once it returns. + fn trampoline(&mut self, ty: &Type, lent: &[Type], ret: &Type) -> String { + let ps: Vec = (0..lent.len()).map(|k| format!("p{k}_")).collect(); + let mut body = vec![format!( + "let f_ = data_!.assumingMemoryBound(to: ({}).self).pointee", + self.sw(ty) + )]; + let mut args = Vec::new(); + for (k, (ty, p)) in lent.iter().zip(&ps).enumerate() { + match ty { + Type::Ref(_, object) if matches!(**object, Type::Object(_)) => { + let l = format!("l{k}_"); + body.push(format!("let {l} = {}(lent: {p})", self.sw(object))); + body.push(format!("defer {{ {l}.close() }}")); + args.push(l); + } + ty => args.push(self.lift_lent(ty, p)), + } + } + let call = format!("f_({})", args.join(", ")); + body.push(match ret { + Type::Unit => call, + ret => format!("return {}", self.lower(ret, &call)), + }); + let mut params = vec!["data_".to_string()]; + params.extend(ps); + format!("{{ {} in\n\t{}\n}}", params.join(", "), body.join("\n\t")) + } + + /// `function`'s Swift form and its member name, a member of `owner` (whose IR type is + /// `owner_ty`) if it has one. + fn function( + &mut self, + function: &Function, + owner: Option<&Ty>, + owner_ty: Option<&Type>, + ) -> Option<(String, String)> { + let abi = self.abi; + let export = abi.export(function, owner); + let c_name = export.call.name.clone(); + let bare = self.bare(&c_name).to_string(); + let owner_c = match owner { + Some(Ty::Struct(n) | Ty::Enum(n) | Ty::Handle { name: n, .. }) => Some(n.clone()), + _ => None, + }; + let own = match &owner_c { + Some(o) => c_name.strip_prefix(o.as_str()).and_then(|r| r.strip_prefix('_')), + None => None, + }; + let reserved: &[&str] = match owner_ty { + Some(Type::Object(_)) => &["close", "handle", "init", "deinit"], + Some(_) => &["init"], + None => &[], + }; + let snake = own.unwrap_or(&bare); + let mut member = member(snake, reserved); + + let receiver = function.receiver.filter(|_| owner.is_some()); + let object = matches!(owner, Some(Ty::Handle { .. })) && receiver.is_some(); + // Only a future or a `Result` throws, and a future only once it is awaited. + let throws = function.future || function.err.is_some(); + let tr = match function.err.is_some() && !function.future { + true => "try ", + false => "", + }; + let mut prelude = Vec::new(); + if !object { + prelude.push("Native.check()".to_string()); + } + let mut arena = false; + let mut args: Vec = Vec::new(); + let mut c_params = export.call.params.iter(); + if let Some(receiver) = receiver { + let (_, c) = c_params.next().expect("a method has its receiver"); + let ty = owner_ty.expect("a method has its type").clone(); + let mut arg = Arg::default(); + match c { + Ty::Handle { .. } => { + arg.open = Some((format!("{tr}self.handle.with {{ h_ in"), "}".to_string())); + arg.exprs.push("h_".to_string()); + let copy = matches!(ty, Type::Object(i) if abi.api.objects[i].copy); + if receiver == Receiver::Owned && !copy { + arg.after.push("self.close()".to_string()); + } + } + _ => { + arena |= self.arena(&ty); + prelude.push(format!("var c_self_ = self._lower({})", self.arena_arg(&ty))); + arg.exprs.push("&c_self_".to_string()); + } + } + args.push(arg); + } + let mut params = Vec::new(); + for (k, param) in function.params.iter().enumerate() { + let (_, c) = c_params.next().expect("each parameter has its C one"); + if let Type::Closure(..) = param.ty { + c_params.next().expect("a closure has its data"); + } + let name = kw(&camel(¶m.name)); + params.push(format!("_ {name}: {}", self.sw(¶m.ty))); + let (arg, needs) = self.arg(¶m.ty, c, &name, k, tr); + arena |= needs; + args.push(arg); + } + let outs: Vec<(String, Ty)> = c_params.cloned().collect(); + if arena { + prelude.insert( + 1.min(prelude.len()), + "let a_ = Native.Arena()\ndefer { a_.finish() }".to_string(), + ); + } + let after: Vec = args.iter().flat_map(|a| a.after.clone()).collect(); + if !after.is_empty() { + prelude.push(format!("defer {{ {} }}", after.join("; "))); + } + let exprs: Vec = args.iter().flat_map(|a| a.exprs.clone()).collect(); + + // A tuple of one returned whole is its element, written through its one out pointer. + let ok = match &function.ok { + Type::Tuple(elements) if elements.len() == 1 => strip_refs(&elements[0]), + ok => strip_refs(ok), + }; + let err = function.err.as_ref().map(strip_refs); + let thrown = err.as_ref().map(|e| self.thrown(e)); + + let mut inner = Vec::new(); + let tail; + if let Some((_, poll, free)) = &export.future { + inner.push(format!("{c_name}({})", exprs.join(", "))); + let poll_outs: Vec<(String, Ty)> = poll.params[2..].to_vec(); + let mut body: Vec = poll_outs.iter().map(|(n, ty)| self.out_decl(n, ty)).collect(); + let mut call_args = vec!["f_".to_string(), "w_".to_string()]; + call_args.extend(poll_outs.iter().map(|(n, _)| format!("&c_{n}_"))); + let ready = match ok { + Type::Unit => "()".to_string(), + _ => self.lift_outs(&function.ok, &ok), + }; + let upper = abi.prefix.to_uppercase(); + // `.some` explicitly, since a `nil` output would read as the poll's pending. + let mut cases = vec![format!("case {upper}_POLL_READY: return .some({ready})")]; + if let (Some(err), Some(thrown)) = (&err, &thrown) { + let lifted = self.lift(err, "c_err_"); + cases.push(format!( + "case {upper}_POLL_ERROR: throw {}", + thrown_value(thrown, &lifted) + )); + } + cases.push("default: return nil".to_string()); + body.push(format!( + "switch {}({}) {{\n{}\n}}", + poll.name, + call_args.join(", "), + cases.join("\n") + )); + tail = Some(format!( + "return try await Native.run(f_, {}) {{ f_, w_ in\n{}\n}}", + free.name, + indent(&body.join("\n")) + )); + } else { + tail = None; + for (n, ty) in &outs { + inner.push(self.out_decl(n, ty)); + } + let mut call_args = exprs.clone(); + call_args.extend(outs.iter().map(|(n, _)| format!("&c_{n}_"))); + let call = format!("{c_name}({})", call_args.join(", ")); + match (&err, &ok) { + (Some(err), ok) => { + let ready = match ok { + Type::Unit => "return".to_string(), + _ => format!("return {}", self.lift_outs(&function.ok, ok)), + }; + inner.push(format!("if {call} {{\n\t{ready}\n}}")); + let lifted = self.lift(err, "c_err_"); + inner.push(format!( + "throw {}", + thrown_value(thrown.as_ref().expect("an error throws"), &lifted) + )); + } + (None, Type::Unit) => inner.push(call), + (None, ok) if !outs.is_empty() => { + inner.push(call); + inner.push(format!("return {}", self.lift_outs(&function.ok, ok))); + } + (None, ok) => { + inner.push(format!("let c_ret_ = {call}")); + inner.push(format!("return {}", self.lift(ok, "c_ret_"))); + } + } + } + // Wrap the call in each argument's closure, innermost last. + let mut code = inner.join("\n"); + for arg in args.iter().rev() { + if let Some((open, close)) = &arg.open { + let lines = code.lines().count(); + // A closure of one line returns its value; of more, its own `return` does. + let body = match lines > 1 || code.starts_with("return ") || code.starts_with("throw ") { + true => code.clone(), + false => format!("return {code}"), + }; + code = format!("return {open}\n{}\n{close}", indent(&body)); + } + } + let mut body = prelude; + match tail { + Some(tail) => { + let create = match code.starts_with("return ") { + true => code["return ".len()..].to_string(), + false => code, + }; + body.push(format!("let f_ = {create}")); + body.push(tail); + } + None => body.push(code), + } + + let mut text = function.docs.clone(); + for param in &function.params { + if let Type::Closure(lent, _) = ¶m.ty { + let name = kw(&camel(¶m.name)); + text.push_str(&format!( + "\n\nRust calls `{}` only during the call, from any thread.", + unquote(&name) + )); + if lent.iter().any(|p| matches!(p.base(), Type::Object(_))) { + text.push_str(&format!( + "\nAn object it lends lives only for that call of `{}`: keep none past it.", + unquote(&name) + )); + } + } + } + if function.future { + text.push_str("\n\nA cancelled task cancels it in Rust, losing nothing, and throws `CancellationError`."); + } + let effects = match (function.future, throws) { + (true, _) => " async throws", + (false, true) => " throws", + (false, false) => "", + }; + let ret = match ok { + Type::Unit => String::new(), + _ => format!(" -> {}", self.sw(&ok)), + }; + let is_static = receiver.is_none() && owner.is_some(); + let stat = match is_static { + true => "static ", + false => "", + }; + let body = body.join("\n"); + let head = match (function.access, receiver) { + (Access::Field { set: false, .. }, Some(_)) if function.params.is_empty() && !throws => { + format!("public var {member}{}", ret.replacen(" -> ", ": ", 1)) + } + (Access::Const, None) if function.params.is_empty() && !throws => { + member = member.trim_matches('`').to_uppercase(); + format!("public {stat}var {member}{}", ret.replacen(" -> ", ": ", 1)) + } + _ => format!("public {stat}func {member}({}){effects}{ret}", params.join(", ")), + }; + let code = format!("{}{head} {{\n{}\n}}\n", docs(text.trim(), ""), indent(&body)); + let labels = match head.contains(" var ") { + true => Vec::new(), + false => function.params.iter().map(|_| "_".to_string()).collect(), + }; + Some((code, full(is_static, &member, labels))) + } + + /// The declaration of the local the call writes the out value `name` of C type `ty` to. + fn out_decl(&self, name: &str, ty: &Ty) -> String { + let Ty::Out(inner) = ty else { + unreachable!("an out parameter is a pointer") + }; + format!("var c_{name}_: {} = {}", self.c_sw(inner), self.zero(inner)) + } + + /// The Swift value of a call's outs, which return `ok`, or each element of a tuple. + fn lift_outs(&mut self, declared: &Type, ok: &Type) -> String { + match declared { + Type::Tuple(elements) if elements.len() == 1 => self.lift(ok, "c_out0_"), + Type::Tuple(elements) => { + let lifted: Vec = (elements.iter().enumerate()) + .map(|(n, e)| self.lift(&strip_refs(e), &format!("c_out{n}_"))) + .collect(); + format!("({})", lifted.join(", ")) + } + _ => self.lift(ok, "c_out_"), + } + } + + /// How an error of type `err` is thrown: as itself if an exported type, which then + /// conforms to `Swift.Error`, or else in a struct of its own. + fn thrown(&mut self, err: &Type) -> Thrown { + match err.base() { + Type::Record(_) | Type::Enum(_) | Type::Object(_) => { + self.conforms.insert(self.sw(err)); + Thrown::Itself + } + _ => { + // Named by its key, as its C structs are, so two types never share one. + let name = format!("{}Error", pascal(&self.abi.key(err))); + let key = self.abi.key(err); + match self.thrown.get(&name) { + Some(seen) if self.abi.key(seen) == key => {} + // Another type whose key spells the same name in PascalCase: the claim fails. + _ => { + self.claim(&name, format!("the error type `{}`", self.sw(err))); + self.thrown.insert(name.clone(), err.clone()); + } + } + Thrown::Wrapped(name) + } + } + } +} + +/// How a function throws its error. +enum Thrown { + Itself, + Wrapped(String), +} + +/// The error value thrown for the lifted error `lifted`. +fn thrown_value(thrown: &Thrown, lifted: &str) -> String { + match thrown { + Thrown::Itself => lifted.to_string(), + Thrown::Wrapped(name) => format!("{name}(error: {lifted})"), + } +} + +/// Whether passing `ty` moves an object's value out: by value, boxed, or in an `Option`, +/// a list, or a tuple, unless `Copy`, and never through a reference. +fn moved(abi: &Abi<'_>, ty: &Type) -> bool { + match ty { + Type::Object(i) => !abi.api.objects[*i].copy, + Type::Option(inner) | Type::Box(inner) | Type::List(inner) => moved(abi, inner), + Type::Tuple(elements) => elements.iter().any(|ty| moved(abi, ty)), + _ => false, + } +} + +/// `ty` returned: a borrow is returned as a clone, so owned. +fn strip_refs(ty: &Type) -> Type { + match ty { + Type::Ref(_, inner) => strip_refs(inner), + Type::Option(inner) => Type::Option(Box::new(strip_refs(inner))), + Type::Box(inner) => Type::Box(Box::new(strip_refs(inner))), + Type::List(inner) => Type::List(Box::new(strip_refs(inner))), + Type::Tuple(elements) => Type::Tuple(elements.iter().map(strip_refs).collect()), + ty => ty.clone(), + } +} + +/// The Swift type of an integer. +fn sw_int(int: Int) -> &'static str { + match int { + Int::U8 => "Swift.UInt8", + Int::U16 => "Swift.UInt16", + Int::U32 => "Swift.UInt32", + Int::U64 => "Swift.UInt64", + Int::Usize => "Swift.UInt", + Int::I8 => "Swift.Int8", + Int::I16 => "Swift.Int16", + Int::I32 => "Swift.Int32", + Int::I64 => "Swift.Int64", + Int::Isize => "Swift.Int", + } +} + +/// The Swift type Swift imports a C integer as: `size_t` is `Int`. +fn c_int(int: Int) -> &'static str { + match int { + Int::Usize => "Swift.Int", + int => sw_int(int), + } +} + +/// The C integer of the Swift integer `e`. +fn lower_int(int: Int, e: &str) -> String { + match int { + Int::Usize => format!("Swift.Int(bitPattern: {e})"), + _ => e.to_string(), + } +} + +/// The Swift integer of the C integer `e`. +fn lift_int(int: Int, e: &str) -> String { + match int { + Int::Usize => format!("Swift.UInt(bitPattern: {e})"), + _ => e.to_string(), + } +} + +/// A snake-case name in Pascal case: `size_pair` is `SizePair`. +fn pascal(name: &str) -> String { + name.split('_') + .filter(|s| !s.is_empty()) + .map(|s| { + let mut chars = s.chars(); + match chars.next() { + Some(first) => first.to_uppercase().chain(chars).collect(), + None => String::new(), + } + }) + .collect() +} + +/// A snake-case name in camel case: `into_label` is `intoLabel`, and `0` is `_0`. +fn camel(name: &str) -> String { + if name.starts_with(|c: char| c.is_ascii_digit()) { + return format!("_{name}"); + } + let pascal = pascal(name); + let mut chars = pascal.chars(); + match chars.next() { + Some(first) => first.to_lowercase().chain(chars).collect(), + None => name.to_string(), + } +} + +/// A field's Swift name: `init` would name the initializer. +fn field_name(name: &str) -> String { + member(name, &["init"]) +} + +/// A member's Swift name: in camel case, quoted if a keyword, or with `_` appended if +/// Swift gives every such type a member of that name. +fn member(snake: &str, reserved: &[&str]) -> String { + let name = camel(snake); + match reserved.contains(&name.as_str()) { + true => format!("{name}_"), + false => kw(&name), + } +} + +/// A member's full Swift name, which two members of a type may share only if one is static +/// and the other not: its name, then its argument labels, so a property and a method of no +/// arguments are alike. +fn full(is_static: bool, name: &str, labels: impl IntoIterator) -> String { + let labels: String = labels.into_iter().map(|l| l + ":").collect(); + let stat = match is_static { + true => "static ", + false => "", + }; + match labels.is_empty() { + true => format!("{stat}{}", unquote(name)), + false => format!("{stat}{}({labels})", unquote(name)), + } +} + +/// A name without the backticks quoting a keyword. +fn unquote(name: &str) -> String { + name.trim_matches('`').to_string() +} + +/// A name Swift reads as a keyword, in backticks. +fn kw(name: &str) -> String { + const KEYWORDS: &[&str] = &[ + "Any", + "Protocol", + "Self", + "Type", + "as", + "associatedtype", + "await", + "break", + "case", + "catch", + "class", + "continue", + "default", + "defer", + "deinit", + "do", + "else", + "enum", + "extension", + "fallthrough", + "false", + "fileprivate", + "for", + "func", + "guard", + "if", + "import", + "in", + "init", + "inout", + "internal", + "is", + "let", + "nil", + "open", + "operator", + "precedencegroup", + "private", + "protocol", + "public", + "repeat", + "rethrows", + "return", + "self", + "static", + "struct", + "subscript", + "super", + "switch", + "throw", + "throws", + "true", + "try", + "typealias", + "var", + "where", + "while", + ]; + match KEYWORDS.contains(&name) { + true => format!("`{name}`"), + false => name.to_string(), + } +} + +/// Swift doc comments holding `docs`, each line indented by `indent`, or nothing for none. +fn docs(docs: &str, indent: &str) -> String { + docs.lines() + .map(|line| match line.is_empty() { + true => format!("{indent}///\n"), + false => format!("{indent}/// {line}\n"), + }) + .collect() +} + +/// Code indented by one tab more, but for blank lines. +fn indent(code: &str) -> String { + let mut out: String = code + .lines() + .map(|l| match l.is_empty() { + true => "\n".to_string(), + false => format!("\t{l}\n"), + }) + .collect(); + if !code.ends_with('\n') { + out.pop(); + } + out +} diff --git a/ffi/tests/api.swift b/ffi/tests/api.swift new file mode 100644 index 000000000..950e86719 --- /dev/null +++ b/ffi/tests/api.swift @@ -0,0 +1,726 @@ +// The api fixture's Swift test: the script fixtures/api/examples/transcript.rs runs +// natively, through the generated Swift. tests/fixtures.rs compares the transcripts. +// +// With an argument, it instead checks the futures' semantics, what releasing frees, or +// breaks a contract, which must fail. + +import Api +import Foundation + +// The Log callback: keeps even levels. +final class PrintLog: Log { + func log(_ level: UInt8, _ line: String) -> Bool { + print("log \(level) \(line)") + return level % 2 == 0 + } + + // Swift frees it once Rust drops the implementation. + deinit { + print("freed") + } +} + +func bound(_ b: BoundU64) -> String { + switch b { + case .included(let n): return "Included(\(n))" + case .excluded(let n): return "Excluded(\(n))" + case .unbounded: return "Unbounded" + } +} + +func debug(_ value: T) -> String { + String(reflecting: value) +} + +func text(_ data: Data) -> String { + String(decoding: data, as: UTF8.self) +} + +/// A path's UTF-8 bytes as Rust's `escape_ascii` prints them. +func escaped(_ path: String) -> String { + var out = "" + for b in path.utf8 { + switch b { + case 0x09: out += "\\t" + case 0x0d: out += "\\r" + case 0x0a: out += "\\n" + case 0x5c, 0x27, 0x22: out += "\\" + String(UnicodeScalar(b)) + case 0x20..<0x7f: out += String(UnicodeScalar(b)) + default: out += "\\x" + String(b, radix: 16).leftPadded(2) + } + } + return out +} + +extension String { + func leftPadded(_ width: Int) -> String { + String(repeating: "0", count: max(0, width - count)) + self + } +} + +func check(_ ok: Bool, _ what: String) { + if !ok { + FileHandle.standardError.write(Data("failed: \(what)\n".utf8)) + exit(1) + } +} + +/// A value a task sets and the test reads, under a lock. +final class Cell: @unchecked Sendable { + private let lock = NSLock() + private var stored: T + + init(_ value: T) { + stored = value + } + + var value: T { + get { + lock.lock() + defer { lock.unlock() } + return stored + } + set { + lock.lock() + defer { lock.unlock() } + stored = newValue + } + } +} + +/// A queue of jobs that a test runs when it chooses: the executor its tasks prefer, so a +/// call resumes on it. +final class Queue: TaskExecutor, @unchecked Sendable { + private let lock = NSLock() + private var jobs: [UnownedJob] = [] + + func enqueue(_ job: UnownedJob) { + lock.lock() + defer { lock.unlock() } + jobs.append(job) + } + + var size: Int { + lock.lock() + defer { lock.unlock() } + return jobs.count + } + + func drain() { + while true { + lock.lock() + let job = jobs.isEmpty ? nil : jobs.removeFirst() + lock.unlock() + guard let job else { return } + job.runSynchronously(on: asUnownedTaskExecutor()) + } + } + + /// Run `body` as a task that prefers this queue. + @discardableResult + func launch(_ body: @escaping @Sendable () async throws -> Void) -> Task { + Task(executorPreference: self, operation: body) + } +} + +/// `body`'s result, waiting on this thread for it to finish on the global executor. +func block(_ body: @escaping @Sendable () async throws -> T) -> T { + let done = DispatchSemaphore(value: 0) + let result = Cell?>(nil) + Task { + do { + result.value = .success(try await body()) + } catch { + result.value = .failure(error) + } + done.signal() + } + done.wait() + return try! result.value!.get() +} + +func script() { + print("limit \(LIMIT)") + + let p = Point(x: 1, y: 2).offset(2, 3) + print("offset \(p.x) \(p.y)") + print("debug \(debug(p))") + print("eq \(p == Point(x: 3, y: 5))") + let d = Point.default() + print("default \(d.x) \(d.y)") + + let c = Counter.new("c", 5) + print("get \(c.get())") + print("name \(c.name())") + print("add ok \(try! c.add(10))") + do { + _ = try c.add(2000) + } catch let e as Api.Error { + print("add err \(e) \(debug(e))") + } catch { + check(false, "add throws an Error") + } + do { + _ = try c.sub(20) + } catch let e as Api.Error { + print("sub err \(e) \(debug(e))") + } catch { + check(false, "sub throws an Error") + } + c.rename("d") + print("name \(c.name())") + print("is d \(c.`is`("d"))") + let copy = c.clone() + _ = try! copy.add(1) + print("clone \(copy.get()) original \(c.get())") + copy.close() + print("debug \(debug(c))") + let label = c.intoLabel(p) + print("label \(debug(label))") + print("note \(hasText(Note(text: ""))) \(hasText(Note(text: nil)))") + + let (below, above) = Counter.new("s", 7).split(5) + print("split \(below.name()) \(below.get()) \(above.name()) \(above.get())") + let one = only(Counter.new("o", 3)) + print("only \(one.name()) \(one.get())") + let (w8, w16, w32, w64) = widths(98305) + print("widths \(w8) \(w16) \(w32) \(w64)") + for b: UInt64 in [2, 0] { + do { + let (q, r) = try divide(7, b) + print("divide \(q) \(r)") + } catch { + print("divide err \(error)") + } + } + for n: UInt64 in [98305, 0] { + if let s = spread(n) { + print("spread \(s.0) \(s.1) \(s.2) \(s.3)") + } else { + print("spread none") + } + } + for needle in ["lo", "x"] { + if let found = find(Data("hello".utf8), Data(needle.utf8)) { + print("find \(found.0) \(found.1)") + } else { + print("find none") + } + } + let statRows = rows([Point(x: 1, y: 2), Point(x: 3, y: 3)]) + print("rows \(statRows.map { "\($0.0) \(debug($0.1)) \($0.2.x) \($0.2.y)" }.joined(separator: ", "))") + // Each handle in a tuple moves in, as one taken by value does. + let board = Board.new([("low", Counter.new("l", 1)), ("high", Counter.new("h", 9))]) + let ranked = board.ranked() + print("ranked \(ranked.map { "\($0.0) \($0.1.name()) \($0.1.get())" }.joined(separator: ", "))") + board.close() + let span = Span(bounds: (2, 10), named: ("s", .square)) + let (lowerHalf, upperHalf) = halves(span) + print("halves \(lowerHalf.0) \(lowerHalf.1) \(upperHalf.0) \(upperHalf.1) \(debug(span))") + let (swappedN, swappedText) = swapped(("t", 4)) + print("swapped \(swappedN) \(swappedText)") + // A tuple of one is its element, so `Option<(Option,)>` nests two optionals. + for value: UInt32?? in [.none, .some(nil), .some(3)] { + switch doubled(value) { + case .none: print("doubled None") + case .some(let inner): print("doubled Some((\(inner.map { "Some(\($0))" } ?? "None"),))") + } + } + print("lent back \(Array(lentBack(Data([1, 2])))) boxed \(Array(boxedTuple(Data([3]))))") + // A borrowed string and an owned one share a tuple struct. + print("title Some((\"\(title(true)!)\",)) \(title(false) == nil ? "None" : "Some") named Some((\"\(named("x")!)\",))") + + // Each lent counter is a new handle to a clone. + let registry = Registry.new(Counter.new("first", 1)) + print("last \(registry.last() != nil)") + registry.push(Counter.new("last", 2)) + let first = registry.first + _ = try! first.add(10) + print("first \(registry.first.get()) lent \(first.get())") + let last = registry.last()! + _ = try! last.add(10) + print("last \(registry.last()!.get()) lent \(last.get())") + // A closure lent for the call edits the first counter through a handle. + let by: UInt64 = 5 + let edited = registry.edit { counter in + _ = try! counter.add(by) + return counter.edited() && counter.get() == 6 + } + print("edit \(edited) \(registry.first.get())") + // A setter moves an object in, as assignment does, closing its handle. + registry.setFirst(Counter.new("second", 3)) + print("set first \(registry.first.get()) \(registry.first.name())") + print("walk \(walk(3) { cursor in cursor.at() * 2 })") + print("each \(each(2) { at in print("visit \(at)") })") + print("offset \(offset({ 7 }, 3))") + print("shift \(shift({ 7 }, 4))") + + let name = Name.owned("a") + let joined = name.join(Name.owned("b")) + print("joined \(joined.asStr()) \(debug(joined)) \(joined == name)") + joined.rename("c") + print("renamed \(joined.asStr())") + let long = longer(joined, Name.owned("dd")) + print("longer \(long.asStr())") + print("unnamed [\(unnamed().asStr())]") + + print("total \(total(RangeU64(start: 2, end: 5)))") + let wide = widen(RangeU64(start: 2, end: 5), 1) + print("widen \(wide.start) \(wide.end)") + let counts = [ + BoundsU64(start: .included(2), end: .excluded(5)), + BoundsU64(start: .included(2), end: .included(5)), + BoundsU64(start: .included(3), end: .unbounded), + BoundsU64(start: .unbounded, end: .unbounded), + BoundsU64(start: .excluded(2), end: .included(4)), + ].map { count($0) } + print("count \(counts.map(String.init).joined(separator: " "))") + print("end \(bound(end(3))) \(bound(end(0)))") + let (endsStart, endsEnd) = ends(3) + print("ends \(bound(endsStart)) \(bound(endsEnd))") + for n: UInt64 in [2, 5] { + do { + print("below \(try Api.below(n, 3))") + } catch let e as BoundsU64Error { + print("below err \(bound(e.error.start)) \(bound(e.error.end))") + } catch { + check(false, "below throws its bounds") + } + } + let window = Window(range: RangeU64(start: 2, end: 8), end: end(5)) + print("window \(window.contains(4)) \(window.contains(6)) \(debug(window))") + + print(greet("x", .square)) + print("len \(len(Data("abc".utf8)))") + let binary = Data([0xff, 0, 0x61]) + print("hex \(hex(binary))") + print("timed hex \(timedHex(binary))") + print("sample hex \(sampleHex(binary))") + print("queued hex \(queuedHex(binary))") + print("tick \(tickName(Label(name: "tick", at: nil, ttl: nil)))") + print("rooted \(rootedName(Label(name: "rooted", at: nil, ttl: nil)))") + print("stored \(storedText("stored"))") + print("layer \(layerName(Label(name: "layer", at: nil, ttl: nil)))") + for event: Event in [.moved(p), .named(name: "n"), .done] { + print(describe(event)) + } + check(advance(.init_(1)) == .init_(0) && advance(.init_(0)) == .done, "a variant named init crosses") + // Rust's seconds are a u64, past Int64's that Swift's components give. + let rustMax = Duration.seconds(Int64.max) * 2 + .seconds(1) + .nanoseconds(999_999_999) + let past = Duration.seconds(Int64.max) + .seconds(1) + check(extended(past, past - .nanoseconds(1)) == rustMax, "a duration past Int64's seconds crosses") + check(extended(rustMax - .nanoseconds(3), .nanoseconds(3)) == rustMax, "Rust's longest duration crosses") + check(extended(rustMax, .nanoseconds(1)) == nil, "Rust refuses a duration past its longest") + + // A `Copy` handle passed by value is copied, so it stays usable. + let stamp = Stamp.fromMillis(2500) + print("secs \(stamp.secs())") + print("later \(Stamp.fromMillis(3000).later(stamp))") + print("secs \(stamp.secs())") + let message = Message.new(stamp, Data("body".utf8)) + print("payload \(text(message.payload))") + print("stamp \(debug(message.stamp!))") + print("sent \(message.sent())") + // Setters assign in place: the bytes are copied in, and a `Copy` handle copied. + let reset = Stamp.fromMillis(4000) + message.setStamp(reset) + message.setPayload(Data("edited".utf8)) + print("payload \(text(message.payload))") + print("stamp \(debug(message.stamp!))") + print("secs \(reset.secs())") + message.setStamp(nil) + print("stamp \(message.stamp != nil)") + + // Bound first, so Swift's reference is gone before the print. + let kept = emit(PrintLog(), Data([1, 2, 3, 4])) + print("kept \(kept)") + let logger = Logger.new(PrintLog()) + print("write \(logger.write("hi"))") + logger.close() + print("dropped") + // Rust calls it on another thread, before the call returns. + let threshold: UInt8 = 2 + let filtered = filter(Data([1, 2, 3, 4])) { level, line in + print("filter \(level) \(line)") + return level > threshold + } + print("filtered \(filtered)") + + let channel = Channel.new() + let wakes = Cell(0) + // On an executor the script drains, so it sees the receive wait and its wake queue + // the resume. + let q = Queue() + let got = Cell(nil) + q.launch { got.value = try await channel.recv() } + q.drain() + print("recv pending \(got.value == nil)") + channel.send(Data("hi".utf8)) + wakes.value += q.size + print("woken \(wakes.value)") + q.drain() + print("recv \(text(got.value!))") + block { + channel.send(Data()) + do { + print("nonempty \(try await channel.recvNonempty().count)") + } catch { + print("nonempty err \(error)") + } + // The send happens at the call, before the future is first polled. + try await channel.sendLater(Data("eager".utf8)) + print("recv \(text(try await channel.recv()))") + channel.send(Data("sized".utf8)) + let (payload, size) = try await channel.recvSized() + print("sized \(text(payload)) \(size)") + } + // The waker moves into Rust, which keeps it until the send wakes it. + channel.notify { wakes.value += 1 } + channel.send(Data("rung".utf8)) + print("woken \(wakes.value)") + + let list = Node.new(1, nil) + list.push(2) + print("node \(list.sum())") + + let tally = Tally.new() + _ = tally.bump() + print("tally \(tally.bump()) \(tally == tally)") + print("both \(bumpBoth(tally, tally))") + print("peek \(peek(tally) { lent in lent.bump() })") + + let area = grow(Area.new(Size(width: 2, height: 3)), 1) + let size = area.size() + print("area \(size.width) \(size.height) \(area.pixels())") + + // Generic types, each at the instances it infers, named by their arguments. + let pair = PairU32(left: 2, right: 5) + let swappedPair = pair.swap() + print("pair \(swappedPair.left) \(swappedPair.right) sum \(pair.sum()) \(debug(pair)) \(pair == swappedPair)") + print("even \(pair.even()) \(PairU32(left: 7, right: 7).even())") + // An instance renamed alone, beside another of `Pair` that keeps its name. + let sized: SizeTag = tag(Size(width: 1, height: 2), 3) + let flipped: PairU8Size = sized.swap() + print("sized \(flipped.left) \(flipped.right.width) \(debug(flipped))") + let beside = PairU32(left: 258, right: 9).besideSize(sized.left) + print("beside \(beside.left) \(debug(beside))") + // At its default, named as the type itself. + let stack = Stack.new() + for (i, n) in ["a", "b"].enumerated() { + stack.push(Counter.new(n, UInt64(i) + 1)) + } + let top = stack.pop()!.get() + print("stack \(stack.depth()) Some(\(top)) \(stack.pop() != nil) \(stack.pop() == nil)") + // Generic functions and methods, at each implementation of the trait bounding them, by + // reference, which leaves Swift its handle. + let fromPoint = Counter.seededPoint("p", Point(x: -3, y: 4)) + let fromSize = Counter.fromSize("s", Size(width: 5, height: 6)) + let fromCounter = Counter.seededCounter("c", fromPoint) + let fromPolar = Counter.seededPolarPoint("q", PolarPoint(turns: 1, distance: 2)) + print( + "seeded \(fromPoint.get()) \(fromSize.get()) \(fromCounter.get()) \(fromPolar.get()) " + + "past \(stack.depthPastPoint(Point(x: 1, y: 2))) seeds \(seedsPointSize(Size(width: 2, height: 3)))") + + // A trait implemented for `Point` and `&Point` apart: one instance. + print("weigh \(weighPoint(Point(x: -1, y: 2)))") + // Instances that name each other, and one made only by an impl of it. + let crossed = cross(LeftRightU8(value: RightU8(value: 1)), RightLeftU8(value: LeftU8(value: 2))) + print("cross \(crossed) outer \(OuterInnerU8.new(4).get())") + // A bound that outlives the call takes a value, and an instance only a constant names. + print("kept \(keptPoint(Point(x: 1, y: -2))) boxed \(BOXED.value)") + print("keep \(keepPoint(Point(x: 1, y: -2)))") + // Inferred instances taking a lent closure and a foreign implementation, and one only + // its impl of a trait names. + let sides = PairU32(left: 2, right: 5) + print("apply \(applyU32(sides) { side in side * 3 })") + let report = reportU32(sides, PrintLog()) + print("report \(report)") + print("wrapped \(keptWrapU32(WrapU32(value: 4)))") + let keptLog = keeps(PrintLog()) + print("keeps \(keptLog.value)") + + // Its Drop runs when Swift closes the handle, not before. + let token = Token.new(7) + print("token \(token.id) dropped \(dropped())") + token.close() + print("dropped \(dropped())") + + // A field and the method of its name, each its own member. + let retry = Retry.new(5) + let limit = retry.limitSetting + print("retry \(limit != nil ? 1 : 0) \(limit ?? 0) \(retry.limit()) \(retry.delay())") + // The renamed field's setter, named as its getter. + retry.setLimitSetting(nil) + print("retry \(retry.limit())") + print("slot \(SlotU8.new(200).value())") + + // Foreign types: a URL parsed from text, socket addresses, OS paths, and `Vec`s. + let base = try! Url.parse("https://example.com/a/b") + print("url \(base) host \(host(base).map { "Some(\"\($0)\")" } ?? "None")") + let url = try! resolve(base.clone(), "c?d") + print("resolve \(url) \(url == base)") + url.close() + do { + try Url.parse("no scheme").close() + } catch let e as StringError { + print("parse err \(e.error)") + } catch { + check(false, "parse throws its message") + } + let v4 = SocketAddr(ip: [127, 0, 0, 1], port: 8080) + let loopback: [UInt8] = Array(repeating: 0, count: 15) + [1] + let v6 = SocketAddr(ip: loopback, port: 443, scopeId: 3) + print("ports \(ports([v4, v6]))") + let peer = Peer(addr: v6, names: ["a", "b"]).next("c") + print("peer \(debug(peer)) \(peer.aliases())") + let file = file("dir", "f\\'\"\t") + print("file \(escaped(file))") + print("parents \(parents([file, "/"]).map { $0.map(escaped) ?? "none" }.joined(separator: " "))") + let words = words(" one two three ") + print("words \(words.count) \(words)") + print("sentence \(sentence(words)) \(sentence(nil))") + print("sentence \(sentence([]))|") + print("chunks \(chunks(Data("abcde".utf8), 2).map { Array($0) })") + let counters = counters(["x", "y"]) + print("counters \(counters.map { "\($0.name()) \($0.get())" }.joined(separator: ", "))") + // Each counter moves in, so Swift closes it. + print("worth \(worth([counters[0]], [counters[1]])) \(worth(nil, []))") + print("sums \(sums([[1, 2], [], [3]]))") + let sum = summed([Point(x: 1, y: 2), Point(x: -4, y: 6)]) + let noneSum = summed([]) + print("summed \(sum.x) \(sum.y) \(noneSum.x) \(noneSum.y)") + print("longest \(longest(["a", "abc"])) \(longest([]))") + let doc = try! Json.parse(#"{"b": [1, 2.5, null], "a": "é"}"#) + print("json \(encode(doc))") + print("timed text \(timedText(doc))") + let tagged = tagged(doc, "t") + print("tagged \(tagged.toText())") + tagged.close() + do { + try Json.parse("{\"a\":").close() + } catch let e as StringError { + print("json parse err \(e.error)") + } catch { + check(false, "parse throws its message") + } + let site = Site.new(base) + print("site \(site.home) \(site.home)") + + // Slices, passed in and returned as clones, and the socket address of each family. + print("shapes \(Shape.ALL) greetings \(GREETINGS)") + let trail = Trail.new([Point(x: 1, y: 2), Point(x: 3, y: 4)], ["a", "b"]) + print("trail \(trail.points()) \(trail.labels())") + // Returned iterators, each collected at the call: drained labels are gone after. + print("iterated \(trail.iter()) \(trail.names()) \(Array(trail.bytes()))") + print("drained \(trail.drain()) then \(trail.labels())") + trail.close() + let numbered = numbered(["p", "q"]) + print("numbered \(numbered.map { "\($0.name()) \($0.get())" }.joined(separator: ", "))") + let (next4, next6) = nexts(SocketAddr(ip: [127, 0, 0, 1], port: 8080), v6) + print("nexts \(next4.ip.map(String.init).joined(separator: ".")) \(next4.port) \(next6.port) \(next6.scopeId)") +} + +// The futures' semantics beyond the transcript, each checked by an assertion. +func futures() { + let channel = Channel.new() + + // The send lands from another thread while the receive waits. + let payload = block { + Thread.detachNewThread { channel.send(Data("blocked".utf8)) } + return try await channel.recv() + } + check(text(payload) == "blocked", "recv returns the payload another thread sent") + + let q = Queue() + + // Resumed on its executor, never inside the wake. + do { + let got = Cell(nil) + q.launch { got.value = text(try await channel.recv()) } + q.drain() + check(got.value == nil, "recv waits for a send") + channel.send(Data("then".utf8)) + check(got.value == nil, "recv never resumes inside the wake") + q.drain() + check(got.value == "then", "recv resumes with the payload") + } + + // A wake that lands while the future polls, before it returns pending, is kept: the + // future polls again rather than waiting for a wake that already happened. + do { + let got = Cell(0) + q.launch { got.value = try await yielded(7) } + q.drain() + check(got.value == 7, "a wake during the poll polls again") + } + + // A future whose output is `None` is ready, not pending. + do { + let got = Cell(nil) + q.launch { got.value = .some(try await halved(3)) } + q.drain() + check(got.value == .some(nil), "a future ready with None returns nil") + check(block { try await halved(4) } == 2, "a future ready with Some returns its value") + } + + // Cancelled between its wake and its poll, a receive loses nothing: the payload stays + // for the next. + do { + let called = Cell(false) + let threw = Cell(false) + let task = q.launch { + do { + _ = try await channel.recv() + called.value = true + } catch is CancellationError { + threw.value = true + } + } + q.drain() + channel.send(Data("kept".utf8)) + task.cancel() + q.drain() + check(!called.value && threw.value, "a cancelled receive throws CancellationError") + let payload = block { try await channel.recv() } + check(text(payload) == "kept", "a cancelled receive loses nothing") + } + + // A task cancelled before its call takes nothing, even a payload ready at the first poll. + do { + let threw = Cell(false) + channel.send(Data("ready".utf8)) + let task = q.launch { + do { + _ = try await channel.recv() + } catch is CancellationError { + threw.value = true + } + } + task.cancel() + q.drain() + check(threw.value, "a cancelled task's receive throws") + let payload = block { try await channel.recv() } + check(text(payload) == "ready", "a cancelled task's receive takes nothing") + } + + // Cancelling a task that waits for no wake resumes it. + do { + let threw = Cell(false) + let task = q.launch { + do { + _ = try await channel.recv() + } catch is CancellationError { + threw.value = true + } + } + q.drain() + check(!threw.value, "recv waits for a send") + task.cancel() + q.drain() + check(threw.value, "cancelling a waiting receive resumes it, throwing") + } + + // A handle closed while a call awaits on it stays alive until the call ends. + do { + let other = Channel.new() + let got = Cell(nil) + q.launch { got.value = text(try await other.recv()) } + q.drain() + let sender = other.clone() + other.close() + sender.send(Data("held".utf8)) + q.drain() + check(got.value == "held", "a closed handle stays alive in the call awaiting on it") + sender.close() + } + print("futures ok") +} + +// What frees an object or bytes nobody closed: Swift's release of the last reference, at +// once. Returned bytes lend Rust's buffer until released. +func owned() { + for n in 1023...1025 { + let payload = Data((0.. u64 { u64::from_str_radix(hex, 16).unwrap() } +/// The generated Swift, built as the module `Api` with warnings as errors in Swift 6's +/// language mode and type-checked in Swift 5's, which moq's package builds in, runs +/// `tests/api.swift`'s script, its futures, and what releasing frees, then fails loud on +/// each broken contract and on a library of another contract. +#[test] +#[cfg_attr( + all(target_os = "linux", target_arch = "aarch64"), + ignore = "nixpkgs builds no Swift for aarch64-linux: quest/m2/uniffi-swift-arm.md" +)] +fn api_swift() { + let api = api(); + let dir = scratch().join("api").join("swift"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let module = api.out.join("swift"); + let source = module.join("api.swift"); + run(swiftc(&module, 13) + .args(["-swift-version", "5", "-typecheck"]) + .arg(&source)); + run(swiftc(&module, 13) + .args(["-swift-version", "6", "-parse-as-library", "-module-name", "Api"]) + .args(["-emit-module", "-emit-module-path"]) + .arg(dir.join("Api.swiftmodule")) + .args(["-emit-library", "-static", "-o"]) + .arg(dir.join("libApi.a")) + .arg(&source)); + // The test's executor preference is macOS 15's. + let exe = dir.join("api-test"); + run(swiftc(&module, 15) + .args(["-parse-as-library", "-I"]) + .arg(&dir) + .arg(manifest_dir().join("tests").join("api.swift")) + .arg(dir.join("libApi.a")) + .arg(api.staticlib()) + .args(&api.native) + .arg("-o") + .arg(&exe)); + let swift = run(&mut Command::new(&exe)); + assert_eq!( + String::from_utf8_lossy(&swift.stdout), + api.transcript, + "the Swift transcript differs from the native one" + ); + run(Command::new(&exe).arg("futures")); + run(Command::new(&exe).arg("owned")); + for (case, message) in [ + ("moved", "Counter is closed"), + ("boxed moved", "Node is closed"), + ("lent kept", "Counter is closed"), + ( + "lent moved", + "api_counter_into_label: a lent handle's value cannot move out", + ), + ("family", "api_nexts: an IPv6 socket address where IPv4 is taken"), + ("stray", "a path from Rust is not UTF-8"), + ("lent stray", "a path from Rust is not UTF-8"), + ] { + let output = Command::new(&exe).arg(case).output().unwrap(); + assert!(!output.status.success(), "Swift {case} did not fail"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains(message), "Swift {case} printed {stderr}"); + } + + // The same Swift, with another contract than the library's in place of its own, refuses + // it at the first call. + let generated = std::fs::read_to_string(&source).unwrap(); + let at = generated + .find("api_contract() == 0x") + .expect("the Swift checks its contract") + + "api_contract() == 0x".len(); + let mut mismatched = generated.clone(); + let digit = if &generated[at..at + 1] == "0" { "1" } else { "0" }; + mismatched.replace_range(at..at + 1, digit); + let contract = dir.join("contract"); + std::fs::create_dir_all(&contract).unwrap(); + std::fs::write(contract.join("api.swift"), mismatched).unwrap(); + std::fs::write(contract.join("main.swift"), "_ = dropped()\n").unwrap(); + let exe = contract.join("contract"); + run(swiftc(&module, 13) + .args(["-module-name", "Api"]) + .arg(contract.join("api.swift")) + .arg(contract.join("main.swift")) + .arg(api.staticlib()) + .args(&api.native) + .arg("-o") + .arg(&exe)); + let output = Command::new(&exe).output().unwrap(); + assert!(!output.status.success(), "a mismatched contract did not fail"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("api.swift and the library are from different builds"), + "a mismatched contract printed {stderr}" + ); +} + +/// swiftc, with warnings as errors, finding the C header's module in `module`, at this +/// macOS deployment target on macOS: 13 is the oldest the generated `Swift.Duration` allows. +fn swiftc(module: &Path, macos: u8) -> Command { + let mut cmd = Command::new("swiftc"); + if cfg!(target_os = "macos") { + let arch = match std::env::consts::ARCH { + "aarch64" => "arm64", + arch => arch, + }; + cmd.arg("-target").arg(format!("{arch}-apple-macos{macos}")); + } + cmd.args(["-warnings-as-errors", "-I"]).arg(module); + cmd +} + /// A relative `CARGO_TARGET_DIR` is the caller's, wherever cargo runs. #[test] fn relative_target() { diff --git a/flake.nix b/flake.nix index 558dfb7a9..7cb349691 100644 --- a/flake.nix +++ b/flake.nix @@ -300,6 +300,8 @@ # rust2-ffi's tests compile its generated Kotlin and run it on the JVM. kotlin ]; + # ... and its generated Swift, where nixpkgs builds Swift: not on aarch64-linux. + nativeBuildInputs = pkgs.lib.optional (!pkgs.swift.meta.broken) pkgs.swift; RUST2_TS_TOOLCHAIN = tsToolchain; RUST2_TS_SYSROOT = rust2-ts-std; KOTLINX_COROUTINES = "${kotlinx-coroutines}"; diff --git a/quest/m1/ffi-export/README.md b/quest/m1/ffi-export/README.md index 89163a335..ebe0f1ebd 100644 --- a/quest/m1/ffi-export/README.md +++ b/quest/m1/ffi-export/README.md @@ -57,7 +57,6 @@ than `ctypes`; the others call C natively. moq's packages switch to them in moq' - [Sessions on sans-IO QUIC](/quest/m1/ffi-export/session-quic.md) - bindings move UDP datagrams and drive the session's poll themselves, over moq's runtime-free QUIC driver once it lands - [C++ bindings on MSVC](/quest/m1/ffi-export/cpp.md) - the nightly's MSVC job passes the C++ checks - [Waiting: moq-tokio within the recursion limit](/quest/m1/ffi-export/recursion-limit.md) - the exporter's moq pin moves past moq-dev/moq#7032 once moq-tokio's `Send` check fits the limit -- [Swift bindings](/quest/m1/ffi-export/swift.md) - Swift over the C ABI as a clang module, tested on Linux and Apple - [Python bindings](/quest/m1/ffi-export/python.md) - a generated CPython extension, skipping the C ABI as Kotlin's JNI does - [Go bindings](/quest/m1/ffi-export/go.md) - Go over the C ABI through cgo - [Dart bindings](/quest/m1/ffi-export/dart.md) - Dart over the C ABI through `dart:ffi` diff --git a/quest/m1/ffi-export/swift.md b/quest/m1/ffi-export/swift.md deleted file mode 100644 index d6fc41ebc..000000000 --- a/quest/m1/ffi-export/swift.md +++ /dev/null @@ -1,22 +0,0 @@ -# [L] Swift bindings - -## Goal - -The exporter generates Swift over its C ABI, imported as a clang module, meeting the -[epic's](/quest/m1/ffi-export/README.md) rules for every generator, native futures included. -`tests/api.swift` runs the shared script on Linux in the merge gate and on Apple's toolchain -in the macOS nightly, and `moq/moq.swift` runs a moq session. - -## Plan - -Decided with the maintainer on 2026-11-01: - -- The generated API is the crate's `pub` API in Swift's idiom, as Kotlin's is, not UniFFI's. - moq's `swift/` package adapts in moq's [switch](https://github.com/moq-dev/moq/blob/main/quest/m2/uniffi-fork.md). -- An object frees its Rust value in `deinit`, and an explicit close frees it sooner. -- Futures are Swift concurrency's `async`, with a cancelled task cancelling its C ABI future. -- Tests run with the Swift from nixpkgs in the merge gate, and on macOS in the nightly, as - C++ does with Apple Clang. Swift's aarch64-linux gap is - [its own quest](/quest/m2/uniffi-swift-arm.md). - -Public API: a new generator. diff --git a/quest/m2/uniffi-swift-arm.md b/quest/m2/uniffi-swift-arm.md index 968487a0e..9a05d529c 100644 --- a/quest/m2/uniffi-swift-arm.md +++ b/quest/m2/uniffi-swift-arm.md @@ -15,6 +15,8 @@ Decided with the maintainer on 2026-10-26, from [#93](https://github.com/moq-dev/rust2/pull/93): swiftc 6.3.3 is marked broken on aarch64-linux in the pinned nixpkgs, where its gold link fails, so the job moved to x86_64. Then bump the `nixpkgs` input in its own change, move the job back, and -drop the workflow comment. Never allow the broken package. +drop the workflow comment. The default shell then has Swift on aarch64-linux too, so +drop rust2-ffi's skips there as well: `api_swift`'s `ignore`, and the Swift's in +`just ffi moq` and `just ffi bench`. Never allow the broken package. Public API: none.