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:
57
PROGRESS.md
57
PROGRESS.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user