feat(apple): VoiceCatCore Swift package + XCFramework build for macOS/iOS clients

Lays the groundwork for the macOS (AppKit) and iOS (SwiftUI) clients with a shared
Swift core wrapping the C ABI, mirroring the proven Windows VoiceCat.Interop layer.

Architecture decision: macOS UI = AppKit (not SwiftUI) for the most mature VoiceOver
accessibility story — same rationale as the Windows client's WinForms-over-WinUI-3
decision. iOS stays SwiftUI. Recorded in docs/roadmap.md §2.

Build infrastructure (Phase 0):
- clients/apple/scripts/build-xcframework.sh: runs cmake --preset apple-dev, merges
  libvoicecat.a + 107 vcpkg static deps into a single ~30 MB fat static library
  (libvoicecat-fat.a) via libtool -static (SPM binary targets link one .a per slice),
  stages voicecat.h + a generated module.modulemap (module VoiceCatC) into the headers,
  runs xcodebuild -create-xcframework -> clients/apple/VoiceCatCore.xcframework.

VoiceCatCore Swift Package (Phase 1):
- Package.swift: binary target (VoiceCatCoreXCF) + library (VoiceCatCore) + test target.
- Sources/VoiceCatCore/: 7 files mirroring the C# VoiceCat.Interop patterns adapted to
  Swift native C interop — Enums (9 Swift mirrors of C enums, UInt32-backed), Config,
  Event (copies ev.text to String inside the callback — the #1 lifetime rule), Models
  (10 Swift value types), Marshaling (C arrays -> Swift + immediate vc_free_*),
  Callbacks (@convention(c) + Unmanaged.passUnretained, the Swift analog of C#'s
  [UnmanagedCallersOnly] + GCHandle), VoiceCatClient (owns vc_client* as OpaquePointer,
  all 38 C ABI functions, deinit -> vc_client_destroy then frees config CStrings, event
  delivery on main queue via coalesced DispatchQueue.main drain).

Tests — 6/6 green (swift test against a real voicecat-server):
- testConnectTofuAuthListChannelsRoundTrips, testAdminChannelCrudAccountCrudRoundTrips,
  testScreenAudioStreamStartsAndStops, testPerStreamRecvControlsRoundTrip, plus two
  static smoke tests. Catches Swift-specific interop bugs (@convention(c) callback
  lifetime, Unmanaged pointer resolution, CString memory management, enum raw-value
  bridging, struct layout) that C++ ctest cannot. C++ suite still 21/21 green.

Docs updated (house rule): tech-stack.md §2, architecture.md §4, roadmap.md M4 + §2,
clients/apple/README.md (full rewrite), PROGRESS.md, .gitignore.
This commit is contained in:
2026-06-18 14:20:38 +02:00
parent b2af1a3001
commit b4766d2f24
16 changed files with 2019 additions and 61 deletions

View File

@@ -10,6 +10,63 @@ up instantly. Newest status at the top.
## ▶ Where we left off / next action
- **Done:** **macOS/iOS Swift core — `VoiceCatCore` package + tests** (2026-06-18). The
shared Swift core for the macOS (AppKit) and iOS (SwiftUI) clients is built and verified.
This is the foundation that both platform UIs will build on — mirrors the Windows client's
proven `VoiceCat.Interop` layer using Swift-native C interop.
- **Architecture decision:** macOS UI = **AppKit** (not SwiftUI), for the most mature
VoiceOver accessibility story — same rationale as the Windows client's WinForms-over-
WinUI-3 decision. iOS stays SwiftUI. Recorded in `docs/roadmap.md` §2 and
`docs/tech-stack.md` §2.
- **`VoiceCatCore` Swift Package** (`clients/apple/Package.swift`): binary target
(`VoiceCatCoreXCF``VoiceCatCore.xcframework`) + library target (`VoiceCatCore` — the
Swift wrapper) + test target (`VoiceCatCoreTests`). 7 source files:
`Enums.swift` (Swift mirrors of the 9 C enums), `Config.swift`, `Event.swift`
(copies `ev.text` to `String` inside the callback — the #1 lifetime rule),
`Models.swift` (Channel/User/Stream/Device/Permissions/Account/AudioConfig/…),
`Marshaling.swift` (C arrays → Swift arrays + immediate `vc_free_*`),
`Callbacks.swift` (`@convention(c)` + `Unmanaged.passUnretained` — the Swift analog of
C#'s `[UnmanagedCallersOnly]` + `GCHandle`), `VoiceCatClient.swift` (owns `vc_client*`,
all 38 C functions, `deinit``vc_client_destroy` then frees config CStrings, event
delivery on `@MainActor` via coalesced `DispatchQueue.main` drain).
- **`build-xcframework.sh`** (`clients/apple/scripts/`): runs `cmake --preset apple-dev`,
merges `libvoicecat.a` + 107 vcpkg static deps into a single ~30 MB fat static library
(`libvoicecat-fat.a`) via `libtool -static`, stages `voicecat.h` + a generated
`module.modulemap` (`module VoiceCatC { header "voicecat.h" }`) into the XCFramework
headers, runs `xcodebuild -create-xcframework``clients/apple/VoiceCatCore.xcframework`.
The fat library is needed because SPM binary targets link ONE `.a` per slice — without
it, the final executable gets undefined-symbol errors for protobuf/mbedtls/sodium/opus/…
(the Apple equivalent of how the Windows client ships a single `voicecat.dll` with all
deps statically linked).
- **Tests — 6/6 green:** `swift test` against a real `voicecat-server` (built by
`cmake --preset dev`). Tests mirror the C# `VoiceCat.Interop.Tests`:
`testVersionStringIsNonEmpty`, `testResultStringRoundTrips`,
`testConnectTofuAuthListChannelsRoundTrips` (connect → TOFU → confirm → guest auth →
channels → permissions → guest ListAccounts rejected),
`testAdminChannelCrudAccountCrudRoundTrips` (admin auth → channel CRUD → account CRUD),
`testScreenAudioStreamStartsAndStops` (screen-audio stream lifecycle through Swift),
`testPerStreamRecvControlsRoundTrip` (two clients, per-stream gain/mute/NR round-trip).
Catches Swift-specific interop bugs (`@convention(c)` callback lifetime, `Unmanaged`
pointer resolution, CString memory management, enum raw-value bridging, struct layout)
that C++ ctest can't.
- **Key C interop discoveries:** (1) Swift imports `vc_client*` (incomplete C struct) as
`OpaquePointer?`, not a named type — `private var handle: OpaquePointer?`. (2) C enums
are imported as `UInt32`-backed (not `Int32`) — all Swift enum mirrors use `UInt32`;
`vc_event.result` is `int32_t` (signed), bridged via `UInt32(bitPattern:)`. (3) Swift
auto-marshals `String` to `const char*` for function params, but NOT for C struct fields
`vc_stream_desc`/`vc_channel_info` string fields need `strdup` + `defer { free }`.
(4) `selfPointer` must be a computed property (not stored) to break the circular init
dependency (`Unmanaged.passUnretained(self)` needs `self` fully initialized, but stored
properties must be set first).
- **Docs updated:** `docs/tech-stack.md` §2 (AppKit macOS / SwiftUI iOS / shared
`VoiceCatCore` package / fat static lib), `docs/architecture.md` §4 (Swift binding
notes), `docs/roadmap.md` M4 + §2 (AppKit decision recorded), `clients/apple/README.md`
(full rewrite), `PROGRESS.md` (this entry), `.gitignore` (XCFramework + SPM artifacts).
- **Next:** macOS AppKit app (`clients/apple/macOS/`) — connect dialog, saved-server list
(Keychain), TOFU identity dialog, main window (NSOutlineView + NSTableView + NSTextView
+ activity log), voice controls, per-user tuning, full VoiceOver accessibility. Mirrors
the Windows `VoiceCat.App` feature set.
- **Done:** **macOS port — `dev` + `apple-dev` presets validated** (2026-06-18). The core,
server, tools, and tests now build and run on macOS (Apple Silicon, macOS 26.5, Apple clang
21). This lays the groundwork for the macOS/iOS Swift client. Three real bugs found and