Conversation
Add cmd/vzbridgegen to inspect Virtualization.framework Swift declarations and selected private Objective-C runtime methods. The tool generates checked Go and purego bindings alongside precompiled arm64 and amd64 static Swift archives. It also emits a minimal linker stub with verified weak imports to support linking against older SDK versions.
Link precompiled Swift archives through a small cgo adapter and call them with purego. Preserve public Go signatures and tie native lifetimes to Go owners, with releases on the bridge queue and request-ID callbacks. Keep window UI in Objective-C on the main thread and cover native cleanup and callbacks with integration tests.
Explicitly enable CGO in CI configurations to support static linkage. Document consumer Clang and SDK requirements, deployment targets, and bridge regeneration procedures. Add documentation covering public and private API generation rules along with native ownership semantics.
Generate all supported host runtime bindings without a selection list. Register each Go binding on first use and keep native entry points in separate archive members. Preserve ownership and ABI checks.
|
@AkihiroSuda @cfergeau I’ve changed the internal implementation to reduce build times. The public API signatures remain unchanged. Could you try this with your usual workloads and check that everything still works as expected? |
There was a problem hiding this comment.
This binary blob dependency makes the library harder to use.
Unlikely to be acceptable in package managers such as Homebrew.
There was a problem hiding this comment.
I’d also have to rebuild it before releasing/signing vfkit
|
I tried this branch, first I wanted to build Updating to xcode 27 fixed this failure. I don’t know if this is expected to fail with older xcode versions. Then I tried to use the |
|
Thanks @AkihiroSuda and @cfergeau for your feedback and testing. Could you review #241 and try it with your usual workloads? |
Fixes #239
Consumer builds currently recompile the Objective-C cgo bridge from source. This change links precompiled Swift archives directly into consumer executables and invokes them through purego. Public Go API signatures remain unchanged, and window UI code stays in Objective-C running on the main thread.
Application builds still require
CGO_ENABLED=1, Clang, and a macOS SDK, but they do not compile Swift sources. A small cgo adapter provides native symbol addresses, and standard binary signing covers the bridge code. macOS continues to supply system Apple frameworks and the Swift runtime.Why Swift
Apple Virtualization now includes Swift-only types such as
VZVirtualMachineViewAdaptor. A Swift bridge allows writing adapters for these APIs and compiling them with Swift 6 concurrency checks.The generator does not yet support
VZVirtualMachineViewAdaptor, async methods, actor-isolated declarations, or arbitrary value types. Those interfaces still require handwritten adapters.Generation and linkage
The generator tool (
cmd/vzbridgegen) reads SDK Swift declarations and Clang enum widths to generate all supported public bindings. It also inspects the host Objective-C runtime to discover and generate all supported private methods, removing the need for a manually maintained private selection list.SwiftSyntax splits native entry points into individual archive members, and each Go binding registers itself on first call. Normal Go builds link only reachable entry points and their dependencies. The compiler checks bridge types and C ABI compatibility. A small generated linker stub records verified weak Virtualization imports so older consumer SDKs can link the archives without altering runtime availability checks.
flowchart TB subgraph generation["Maintainer workflow"] sdk["SDK Swift declarations and Clang enum widths"] --> gen["cmd/vzbridgegen"] private["Host Objective-C runtime methods"] --> gen gen --> wrappers["Generated Swift wrappers"] wrappers --> compiler["Swift and Clang"] manual["Handwritten Swift adapters and Objective-C UI"] --> compiler compiler --> split["One entry point per archive member"] split --> native["libBridge.a and Virtualization.tbd"] gen --> bindings["Generated Go bindings"] end subgraph build["Application build, CGO_ENABLED=1"] native --> linker["go build and cgo linker"] bindings --> reachable["Reachable Go bindings"] reachable --> linker linker --> executable["Executable with reachable bridge functions"] end executable -->|"Runtime calls"| framework["System Virtualization.framework and Swift runtime"]Ownership and asynchronous calls
Managed bridge objects share one Go owner across aliases.
runtime.AddCleanupregisters a cleanup that runs after that owner becomes unreachable. Cleanup timing is not deterministic, and releasing the Go-owned reference destroys the native object only if no other native references remain.flowchart TD native["Swift returns one retained native reference"] --> owner["Go aliases share one owner<br/>NewManagedPointer and runtime.AddCleanup"] owner --> gc["Owner becomes unreachable<br/>Go runtime schedules cleanup later"] gc --> close["managedReference.close via sync.Once<br/>ReleaseObject via purego"] close --> queue["bridgeQueue.async<br/>Release the native reference"] queue --> last{"Last native reference?"} last -->|"Yes"| destroy["Native object is destroyed"] last -->|"No"| retained["Other native owners keep it alive"]The following sequence shows a one-shot request such as
VirtualMachine.Start.runtime.KeepAliveprotects Go arguments through the synchronous bridge call. The Swift completion closure keeps the native receiver alive until completion. The Go closure stays in the callback registry, while Swift stores the numeric request ID and a shared callback function address.sequenceDiagram participant G as Go API participant R as Go callback registry participant S as Swift bridge participant Q as Bridge queue participant F as Virtualization.framework G->>R: Register completion closure R-->>G: uint64 request ID G->>S: purego call with native receiver and request ID S->>Q: bridgeSync Q->>F: Start asynchronous operation Note over Q,F: Swift closure retains the native receiver Q-->>S: Operation scheduled S-->>G: Bridge call returns G->>G: runtime.KeepAlive(arguments) Note over G: Wait on the buffered completion channel F-->>S: Completion callback S->>Q: emit through bridgeSync Q->>R: purego callback with request ID and result R->>R: Look up and remove the one-shot entry R-->>G: Invoke closure and send the result G->>G: Return the result to the callerThe completion channel is buffered, so completion can arrive even before the Go caller starts waiting. Repeated event callbacks keep their registry entries until removal or a terminal event.
Maintenance and generator details:
Build time
The median package build fell from 47.99 s to 6.37 s, about 7.5x faster and 86.7% less time.
Measured against
c03d09aon an Apple M2, macOS 27.0.1, Xcode 27.0.0, and Go 1.25.0. Both versions usedCGO_ENABLED=1. Each rango build -modcacherw .three times in alternating order with fresh Go build, dependency, Clang, and Swift module caches. Dependency downloads are included; source checkouts were already present, and OS file caches were retained. This measures package builds, not final application linking.Executable size
A test program that creates and prints a MAC address shrank from 6,300,226 to 5,559,426 bytes (an 11.8% reduction compared to the previous static bridge). Native bridge export symbols dropped from 424 to 7. This build includes all generated runtime bindings in the archive, including private methods.
A separate linker check invokes one SDK binding and then adds one runtime binding. The resulting executables contain only those specific entry points plus the ABI check, and both run successfully.
The archives themselves are 24.6 MB for arm64 and 23.5 MB for amd64, containing 1,708 runtime bindings (including the allocation helper). The linker does not pull unreferenced archive members into the executable. Generation reported 352 unsupported runtime methods on macOS 27.0.1 (arm64).