diff --git a/.clang-format b/.clang-format index 451b578f..f0507abf 100644 --- a/.clang-format +++ b/.clang-format @@ -1,4 +1,10 @@ +--- BasedOnStyle: WebKit Language: ObjC TabWidth: 4 PointerAlignment: Right +--- +BasedOnStyle: WebKit +Language: Cpp +TabWidth: 4 +PointerAlignment: Right diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index febedd54..c32eb0e8 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -54,7 +54,7 @@ jobs: test: needs: formatting-check runs-on: ${{ matrix.os }} - timeout-minutes: 3 + timeout-minutes: 5 strategy: fail-fast: false # Can't expand the matrix due to the flakiness of the CI infra diff --git a/Makefile b/Makefile index d0094fdb..3a82dc10 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ fmt: .PHONY: test test: - go test -p 1 -exec "go run $(PWD)/cmd/codesign" ./... -timeout 2m -v + go test -p 1 -exec "go run $(PWD)/cmd/codesign" ./... -timeout 4m -v .PHONY: test/run test/run: diff --git a/cgoutil.go b/cgoutil.go index 7799fc10..3acb8b6c 100644 --- a/cgoutil.go +++ b/cgoutil.go @@ -1,7 +1,7 @@ package vz /* -#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc #cgo darwin LDFLAGS: -lobjc -framework Foundation #import diff --git a/example/macOS/go.mod b/example/macOS/go.mod index cf289732..73498afe 100644 --- a/example/macOS/go.mod +++ b/example/macOS/go.mod @@ -9,4 +9,5 @@ require github.com/Code-Hex/vz/v3 v3.0.0-00010101000000-000000000000 require ( github.com/Code-Hex/go-infinity-channel v1.0.0 // indirect golang.org/x/mod v0.22.0 // indirect + golang.org/x/sys v0.39.0 // indirect ) diff --git a/go.mod b/go.mod index 565f5f44..df802509 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,4 @@ require ( golang.org/x/mod v0.22.0 ) -require golang.org/x/sys v0.39.0 // indirect +require golang.org/x/sys v0.39.0 diff --git a/internal/cgohandler/cgohandler.go b/internal/cgohandler/cgohandler.go new file mode 100644 index 00000000..495cd79c --- /dev/null +++ b/internal/cgohandler/cgohandler.go @@ -0,0 +1,40 @@ +package cgohandler + +import ( + "runtime" + "runtime/cgo" +) + +// Handler holds a cgo.Handle for an Object. +// It provides methods to hold and release the handle. +// handle will released when Handler is cleaned up. +type Handler struct { + handle cgo.Handle +} + +// releaseOnCleanup registers a cleanup function to delete the cgo.Handle when cleaned up. +func (h *Handler) releaseOnCleanup() { + runtime.AddCleanup(h, func(h cgo.Handle) { + h.Delete() + }, h.handle) +} + +// New creates a new [Handler] and holds the given value. +func New(v any) (*Handler, uintptr) { + if v == nil { + return nil, 0 + } + h := &Handler{cgo.NewHandle(v)} + h.releaseOnCleanup() + return h, uintptr(h.handle) +} + +// Unwrap unwraps the cgo.Handle from the given uintptr and returns the associated value. +// It does NOT delete the handle; it expects the handle to be managed by [Handler] or caller. +func Unwrap[T any](handle uintptr) T { + if handle == 0 { + var zero T + return zero + } + return cgo.Handle(handle).Value().(T) +} diff --git a/osversion.go b/internal/osversion/osversion.go similarity index 73% rename from osversion.go rename to internal/osversion/osversion.go index 02a8a373..6867051c 100644 --- a/osversion.go +++ b/internal/osversion/osversion.go @@ -1,4 +1,4 @@ -package vz +package osversion /* #cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc @@ -27,24 +27,24 @@ var ( ErrBuildTargetOSVersion = errors.New("unsupported build target macOS version") ) -func macOSAvailable(version float64) error { +func MacOSAvailable(version float64) error { if macOSMajorMinorVersion() < version { return ErrUnsupportedOSVersion } - return macOSBuildTargetAvailable(version) + return MacOSBuildTargetAvailable(version) } var ( - majorMinorVersion float64 - majorMinorVersionOnce interface{ Do(func()) } = &sync.Once{} + MajorMinorVersion float64 + MajorMinorVersionOnce interface{ Do(func()) } = &sync.Once{} // This can be replaced in the test code to enable mock. // It will not be changed in production. - sysctl = syscall.Sysctl + Sysctl = syscall.Sysctl ) -func fetchMajorMinorVersion() (float64, error) { - osver, err := sysctl("kern.osproductversion") +func FetchMajorMinorVersion() (float64, error) { + osver, err := Sysctl("kern.osproductversion") if err != nil { return 0, err } @@ -58,19 +58,19 @@ func fetchMajorMinorVersion() (float64, error) { } func macOSMajorMinorVersion() float64 { - majorMinorVersionOnce.Do(func() { - version, err := fetchMajorMinorVersion() + MajorMinorVersionOnce.Do(func() { + version, err := FetchMajorMinorVersion() if err != nil { panic(err) } - majorMinorVersion = version + MajorMinorVersion = version }) - return majorMinorVersion + return MajorMinorVersion } var ( - maxAllowedVersion int - maxAllowedVersionOnce interface{ Do(func()) } = &sync.Once{} + MaxAllowedVersion int + MaxAllowedVersionOnce interface{ Do(func()) } = &sync.Once{} getMaxAllowedVersion = func() int { return int(C.mac_os_x_version_max_allowed()) @@ -78,14 +78,14 @@ var ( ) func fetchMaxAllowedVersion() int { - maxAllowedVersionOnce.Do(func() { - maxAllowedVersion = getMaxAllowedVersion() + MaxAllowedVersionOnce.Do(func() { + MaxAllowedVersion = getMaxAllowedVersion() }) - return maxAllowedVersion + return MaxAllowedVersion } -// macOSBuildTargetAvailable checks whether the API available in a given version has been compiled. -func macOSBuildTargetAvailable(version float64) error { +// MacOSBuildTargetAvailable checks whether the API available in a given version has been compiled. +func MacOSBuildTargetAvailable(version float64) error { allowedVersion := fetchMaxAllowedVersion() if allowedVersion == 0 { return fmt.Errorf("undefined __MAC_OS_X_VERSION_MAX_ALLOWED: %w", ErrBuildTargetOSVersion) diff --git a/virtualization_helper.h b/internal/osversion/virtualization_helper.h similarity index 92% rename from virtualization_helper.h rename to internal/osversion/virtualization_helper.h index 7914efdd..2f254668 100644 --- a/virtualization_helper.h +++ b/internal/osversion/virtualization_helper.h @@ -46,6 +46,13 @@ NSDictionary *dumpProcessinfo(); #pragma message("macOS 15 API has been disabled") #endif +// for macOS 26 API +#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 260000 +#define INCLUDE_TARGET_OSX_26 1 +#else +#pragma message("macOS 26 API has been disabled") +#endif + static inline int mac_os_x_version_max_allowed() { #ifdef __MAC_OS_X_VERSION_MAX_ALLOWED diff --git a/virtualization_helper.m b/internal/osversion/virtualization_helper.m similarity index 100% rename from virtualization_helper.m rename to internal/osversion/virtualization_helper.m diff --git a/network.go b/network.go index c3ecddc8..5dc6fb8f 100644 --- a/network.go +++ b/network.go @@ -2,9 +2,10 @@ package vz /* #cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc -#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework Virtualization +#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework Virtualization -framework vmnet # include "virtualization_11.h" # include "virtualization_13.h" +# include "virtualization_26.h" */ import "C" import ( @@ -14,6 +15,7 @@ import ( "syscall" "github.com/Code-Hex/vz/v3/internal/objc" + "github.com/Code-Hex/vz/v3/vmnet" ) // BridgedNetwork defines a network interface that bridges a physical interface with a virtual machine. @@ -260,6 +262,51 @@ func (f *FileHandleNetworkDeviceAttachment) MaximumTransmissionUnit() int { return f.mtu } +// VmnetNetworkDeviceAttachment represents a vmnet network device attachment. +// +// This attachment is used to connect a virtual machine to a vmnet network. +// The attachment is created with a VmnetNetwork and can be used with a VirtioNetworkDeviceConfiguration. +// see: https://developer.apple.com/documentation/virtualization/vzvmnetnetworkdeviceattachment?language=objc +// +// This is only supported on macOS 26 and newer, error will +// be returned on older versions. +type VmnetNetworkDeviceAttachment struct { + *pointer + + *baseNetworkDeviceAttachment +} + +func (*VmnetNetworkDeviceAttachment) String() string { + return "VmnetNetworkDeviceAttachment" +} + +func (v *VmnetNetworkDeviceAttachment) Network() *vmnet.Network { + ptr := C.VZVmnetNetworkDeviceAttachment_network(objc.Ptr(v)) + return vmnet.NewNetworkFromPointer(objc.NewPointer(ptr)) +} + +var _ NetworkDeviceAttachment = (*VmnetNetworkDeviceAttachment)(nil) + +// NewVmnetNetworkDeviceAttachment creates a new VmnetNetworkDeviceAttachment with network. +// +// This is only supported on macOS 26 and newer, error will +// be returned on older versions. +func NewVmnetNetworkDeviceAttachment(network *vmnet.Network) (*VmnetNetworkDeviceAttachment, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + attachment := &VmnetNetworkDeviceAttachment{ + pointer: objc.NewPointer( + C.newVZVmnetNetworkDeviceAttachment(objc.Ptr(network)), + ), + } + objc.SetFinalizer(attachment, func(self *VmnetNetworkDeviceAttachment) { + objc.Release(self) + }) + return attachment, nil +} + // NetworkDeviceAttachment for a network device attachment. // see: https://developer.apple.com/documentation/virtualization/vznetworkdeviceattachment?language=objc type NetworkDeviceAttachment interface { diff --git a/osversion_alias.go b/osversion_alias.go new file mode 100644 index 00000000..01e0afc5 --- /dev/null +++ b/osversion_alias.go @@ -0,0 +1,28 @@ +package vz + +import "github.com/Code-Hex/vz/v3/internal/osversion" + +var ( + // ErrUnsupportedOSVersion is returned when calling a method which is only + // available in newer macOS versions. + ErrUnsupportedOSVersion = osversion.ErrUnsupportedOSVersion + + // ErrBuildTargetOSVersion indicates that the API is available but the + // running program has disabled it. + ErrBuildTargetOSVersion = osversion.ErrBuildTargetOSVersion + + macOSAvailable = osversion.MacOSAvailable + + // MacOSBuildTargetAvailable checks whether the API available in a given version has been compiled. + macOSBuildTargetAvailable = osversion.MacOSBuildTargetAvailable +) + +// for Testing +var ( + fetchMajorMinorVersion = osversion.FetchMajorMinorVersion + majorMinorVersion = &osversion.MajorMinorVersion + majorMinorVersionOnce = &osversion.MajorMinorVersionOnce + maxAllowedVersion = &osversion.MaxAllowedVersion + maxAllowedVersionOnce = &osversion.MaxAllowedVersionOnce + sysctl = &osversion.Sysctl +) diff --git a/osversion_arm64_test.go b/osversion_arm64_test.go index d5a03d1d..101ca7fd 100644 --- a/osversion_arm64_test.go +++ b/osversion_arm64_test.go @@ -13,13 +13,13 @@ import ( ) func TestAvailableVersionArm64(t *testing.T) { - majorMinorVersionOnce = &nopDoer{} + *majorMinorVersionOnce = &nopDoer{} defer func() { - majorMinorVersion = 0 - majorMinorVersionOnce = &sync.Once{} + *majorMinorVersion = 0 + *majorMinorVersionOnce = &sync.Once{} }() t.Run("macOS 12", func(t *testing.T) { - majorMinorVersion = 11 + *majorMinorVersion = 11 cases := map[string]func() error{ "NewMacOSBootLoader": func() error { _, err := NewMacOSBootLoader() @@ -79,7 +79,7 @@ func TestAvailableVersionArm64(t *testing.T) { t.Skip("disabled build target for macOS 13") } - majorMinorVersion = 12.3 + *majorMinorVersion = 12.3 cases := map[string]func() error{ "WithStartUpFromMacOSRecovery": func() error { return (*VirtualMachine)(nil).Start(WithStartUpFromMacOSRecovery(true)) @@ -110,7 +110,7 @@ func TestAvailableVersionArm64(t *testing.T) { } defer f.Close() - majorMinorVersion = 13 + *majorMinorVersion = 13 cases := map[string]func() error{ "NewLinuxRosettaUnixSocketCachingOptions": func() error { _, err := NewLinuxRosettaUnixSocketCachingOptions(filename) diff --git a/osversion_test.go b/osversion_test.go index 348fa077..67a547ce 100644 --- a/osversion_test.go +++ b/osversion_test.go @@ -16,14 +16,14 @@ type nopDoer struct{} func (*nopDoer) Do(func()) {} func TestAvailableVersion(t *testing.T) { - majorMinorVersionOnce = &nopDoer{} + *majorMinorVersionOnce = &nopDoer{} defer func() { - majorMinorVersion = 0 - majorMinorVersionOnce = &sync.Once{} + *majorMinorVersion = 0 + *majorMinorVersionOnce = &sync.Once{} }() t.Run("macOS 11", func(t *testing.T) { - majorMinorVersion = 10 + *majorMinorVersion = 10 cases := map[string]func() error{ "NewLinuxBootLoader": func() error { _, err := NewLinuxBootLoader("") @@ -109,7 +109,7 @@ func TestAvailableVersion(t *testing.T) { }) t.Run("macOS 12", func(t *testing.T) { - majorMinorVersion = 11 + *majorMinorVersion = 11 cases := map[string]func() error{ "NewVirtioSoundDeviceConfiguration": func() error { _, err := NewVirtioSoundDeviceConfiguration() @@ -177,7 +177,7 @@ func TestAvailableVersion(t *testing.T) { t.Skip("disabled build target for macOS 12.3") } - majorMinorVersion = 12 + *majorMinorVersion = 12 cases := map[string]func() error{ "BlockDeviceIdentifier": func() error { _, err := (*VirtioBlockDeviceConfiguration)(nil).BlockDeviceIdentifier() @@ -202,7 +202,7 @@ func TestAvailableVersion(t *testing.T) { t.Skip("disabled build target for macOS 13") } - majorMinorVersion = 12.3 + *majorMinorVersion = 12.3 cases := map[string]func() error{ "NewEFIBootLoader": func() error { _, err := NewEFIBootLoader() @@ -309,7 +309,7 @@ func TestAvailableVersion(t *testing.T) { } defer f.Close() - majorMinorVersion = 13 + *majorMinorVersion = 13 cases := map[string]func() error{ "NewNVMExpressControllerDeviceConfiguration": func() error { _, err := NewNVMExpressControllerDeviceConfiguration(nil) @@ -378,9 +378,9 @@ func Test_fetchMajorMinorVersion(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sysctl = tt.sysctl + *sysctl = tt.sysctl defer func() { - sysctl = syscall.Sysctl + *sysctl = syscall.Sysctl }() version, err := fetchMajorMinorVersion() @@ -395,9 +395,9 @@ func Test_fetchMajorMinorVersion(t *testing.T) { } func Test_macOSBuildTargetAvailable(t *testing.T) { - maxAllowedVersionOnce = &nopDoer{} + *maxAllowedVersionOnce = &nopDoer{} defer func() { - maxAllowedVersionOnce = &sync.Once{} + *maxAllowedVersionOnce = &sync.Once{} }() wantErrMsgFor := func(version float64, maxAllowedVersion int) string { @@ -492,7 +492,7 @@ func Test_macOSBuildTargetAvailable(t *testing.T) { t.Run(name, func(t *testing.T) { tmp := maxAllowedVersion defer func() { maxAllowedVersion = tmp }() - maxAllowedVersion = tc.maxAllowedVersion + *maxAllowedVersion = tc.maxAllowedVersion err := macOSBuildTargetAvailable(tc.version) if (err != nil) != tc.wantErr { diff --git a/shared_directory_arm64_test.go b/shared_directory_arm64_test.go index 9cc2fcf6..dffee603 100644 --- a/shared_directory_arm64_test.go +++ b/shared_directory_arm64_test.go @@ -156,25 +156,6 @@ func rosettaConfiguration(t *testing.T, o vz.LinuxRosettaCachingOptions) func(*v } } -func (c *Container) exec(t *testing.T, cmds ...string) { - t.Helper() - for _, cmd := range cmds { - session := c.NewSession(t) - defer session.Close() - output, err := session.CombinedOutput(cmd) - if err != nil { - if len(output) > 0 { - t.Fatalf("failed to run command %q: %v, outputs:\n%s", cmd, err, string(output)) - } else { - t.Fatalf("failed to run command %q: %v", cmd, err) - } - } - if len(output) > 0 { - t.Logf("command %q outputs:\n%s", cmd, string(output)) - } - } -} - // rosettad's default unix socket const rosettadDefaultUnixSocket = "~/.cache/rosettad/uds/rosetta.sock" diff --git a/virtualization_11.h b/virtualization_11.h index f9f9fdc6..7fc1a9f0 100644 --- a/virtualization_11.h +++ b/virtualization_11.h @@ -6,7 +6,7 @@ #pragma once -#import "virtualization_helper.h" +#import "internal/osversion/virtualization_helper.h" #import /* exported from cgo */ diff --git a/virtualization_12.h b/virtualization_12.h index dd679a7f..d446e7c7 100644 --- a/virtualization_12.h +++ b/virtualization_12.h @@ -4,7 +4,7 @@ // Created by codehex. // -#import "virtualization_helper.h" +#import "internal/osversion/virtualization_helper.h" #import "virtualization_view.h" // FIXME(codehex): this is dirty hack to avoid clang-format error like below diff --git a/virtualization_12_3.h b/virtualization_12_3.h index 5d210cd0..a6ccc93f 100644 --- a/virtualization_12_3.h +++ b/virtualization_12_3.h @@ -6,7 +6,7 @@ #pragma once -#import "virtualization_helper.h" +#import "internal/osversion/virtualization_helper.h" #import // FIXME(codehex): this is dirty hack to avoid clang-format error like below diff --git a/virtualization_12_arm64.h b/virtualization_12_arm64.h index 4a40a579..22629de2 100644 --- a/virtualization_12_arm64.h +++ b/virtualization_12_arm64.h @@ -6,7 +6,7 @@ #pragma once -#import "virtualization_helper.h" +#import "internal/osversion/virtualization_helper.h" #import #import #import diff --git a/virtualization_13.h b/virtualization_13.h index cf2fb722..0478e31e 100644 --- a/virtualization_13.h +++ b/virtualization_13.h @@ -6,7 +6,7 @@ #pragma once -#import "virtualization_helper.h" +#import "internal/osversion/virtualization_helper.h" #import /* macOS 13 API */ diff --git a/virtualization_13_arm64.h b/virtualization_13_arm64.h index 99701ce2..65f70b28 100644 --- a/virtualization_13_arm64.h +++ b/virtualization_13_arm64.h @@ -12,7 +12,7 @@ // "Configuration file(s) do(es) not support C++: /github.com/Code-Hex/vz/.clang-format" #define NSURLComponents NSURLComponents -#import "virtualization_helper.h" +#import "internal/osversion/virtualization_helper.h" #import /* exported from cgo */ diff --git a/virtualization_14.h b/virtualization_14.h index 97d06c12..f92db9af 100644 --- a/virtualization_14.h +++ b/virtualization_14.h @@ -10,7 +10,7 @@ // "Configuration file(s) do(es) not support C++: /github.com/Code-Hex/vz/.clang-format" #define NSURLComponents NSURLComponents -#import "virtualization_helper.h" +#import "internal/osversion/virtualization_helper.h" #import /* exported from cgo */ diff --git a/virtualization_14_arm64.h b/virtualization_14_arm64.h index 23bd60d8..7962e6fc 100644 --- a/virtualization_14_arm64.h +++ b/virtualization_14_arm64.h @@ -11,7 +11,7 @@ // "Configuration file(s) do(es) not support C++: /github.com/Code-Hex/vz/.clang-format" #define NSURLComponents NSURLComponents -#import "virtualization_helper.h" +#import "internal/osversion/virtualization_helper.h" #import void saveMachineStateToURLWithCompletionHandler(void *machine, void *queue, uintptr_t cgoHandle, const char *saveFilePath); diff --git a/virtualization_15.h b/virtualization_15.h index f056c671..a02736ba 100644 --- a/virtualization_15.h +++ b/virtualization_15.h @@ -10,7 +10,7 @@ // "Configuration file(s) do(es) not support C++: /github.com/Code-Hex/vz/.clang-format" #define NSURLComponents NSURLComponents -#import "virtualization_helper.h" +#import "internal/osversion/virtualization_helper.h" #import /* exported from cgo */ diff --git a/virtualization_26.h b/virtualization_26.h new file mode 100644 index 00000000..9bb0481e --- /dev/null +++ b/virtualization_26.h @@ -0,0 +1,20 @@ +// +// virtualization_26.h +// +// Created by codehex. +// + +#pragma once + +// FIXME(codehex): this is dirty hack to avoid clang-format error like below +// "Configuration file(s) do(es) not support C++: /github.com/Code-Hex/vz/.clang-format" +#define NSURLComponents NSURLComponents + +#import "internal/osversion/virtualization_helper.h" +#import +#import + +/* macOS 26 API */ +// VZVmnetNetworkDeviceAttachment +void *newVZVmnetNetworkDeviceAttachment(void *network); +void *VZVmnetNetworkDeviceAttachment_network(void *attachment); diff --git a/virtualization_26.m b/virtualization_26.m new file mode 100644 index 00000000..8114e645 --- /dev/null +++ b/virtualization_26.m @@ -0,0 +1,30 @@ +// +// virtualization_26.m +// +// Created by codehex. +// + +#import "virtualization_26.h" + +// VZVmnetNetworkDeviceAttachment +// see: https://developer.apple.com/documentation/virtualization/vzvmnetnetworkdeviceattachment/init(network:)?language=objc +void *newVZVmnetNetworkDeviceAttachment(void *network) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return [[VZVmnetNetworkDeviceAttachment alloc] initWithNetwork:(vmnet_network_ref)network]; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/virtualization/vzvmnetnetworkdeviceattachment/network?language=objc +void *VZVmnetNetworkDeviceAttachment_network(void *attachment) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return [(VZVmnetNetworkDeviceAttachment *)attachment network]; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} diff --git a/virtualization_debug.m b/virtualization_debug.m index 67fe356a..8c5089fc 100644 --- a/virtualization_debug.m +++ b/virtualization_debug.m @@ -5,7 +5,7 @@ // #import "virtualization_debug.h" -#import "virtualization_helper.h" +#import "internal/osversion/virtualization_helper.h" /*! @abstract Create a VZGDBDebugStubConfiguration with debug port for GDB server. diff --git a/virtualization_test.go b/virtualization_test.go index 9be7357d..1d78b33d 100644 --- a/virtualization_test.go +++ b/virtualization_test.go @@ -7,6 +7,7 @@ import ( "math" "net" "os" + "regexp" "runtime" "syscall" "testing" @@ -110,6 +111,46 @@ func (c *Container) NewSession(t *testing.T) *ssh.Session { return sshSession } +func (c *Container) DetectIPv4(t *testing.T, ifname string) string { + sshSession, err := c.Client.NewSession() + if err != nil { + t.Fatal(err) + } + defer sshSession.Close() + + output, err := sshSession.Output(fmt.Sprintf("ip address show dev %s scope global", ifname)) + if err != nil { + t.Fatal(err) + } + re := regexp.MustCompile(`(?ms)^\s+inet\s+([0-9.]+)/`) + matches := re.FindStringSubmatch(string(output)) + if len(matches) == 2 { + return matches[1] + } + t.Fatalf("failed to parse IP address from output: %s", output) + + return "" +} + +func (c *Container) exec(t *testing.T, cmds ...string) { + t.Helper() + for _, cmd := range cmds { + session := c.NewSession(t) + defer session.Close() + output, err := session.CombinedOutput(cmd) + if err != nil { + if len(output) > 0 { + t.Fatalf("failed to run command %q: %v, outputs:\n%s", cmd, err, string(output)) + } else { + t.Fatalf("failed to run command %q: %v", cmd, err) + } + } + if len(output) > 0 { + t.Logf("command %q outputs:\n%s", cmd, string(output)) + } + } +} + func (c *Container) Shutdown() error { defer func() { log.Println("shutdown done") diff --git a/virtualization_view.h b/virtualization_view.h index 3b81b283..15aa786f 100644 --- a/virtualization_view.h +++ b/virtualization_view.h @@ -6,7 +6,7 @@ #pragma once -#import "virtualization_helper.h" +#import "internal/osversion/virtualization_helper.h" #import #import #import diff --git a/vmnet/datagram_darwin.go b/vmnet/datagram_darwin.go new file mode 100644 index 00000000..80f2a63a --- /dev/null +++ b/vmnet/datagram_darwin.go @@ -0,0 +1,246 @@ +package vmnet + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework vmnet +# include "vmnet_darwin.h" +*/ +import "C" +import ( + "errors" + "fmt" + "net" + "os" + "syscall" +) + +// MARK: - DatagramFileAdaptorForInterface + +// DatagramFileAdaptorForInterface returns a file for the given [Network]. +// - It starts goroutines to handle packet transfer between the vmnet interface and the file. +// - The context can be used to stop the goroutines and the interface. +// - The returned error channel can be used to receive errors from the goroutines. +// +// The returned file can be used as a file descriptor for QEMU's netdev datagram backend or VZ's [NewFileHandleNetworkDeviceAttachment] +// QEMU: +// +// -netdev datagram,id=net0,addr.type=fd,addr.str= +// +// VZ: +// +// file, errCh, err := DatagramFileAdaptorForInterface(ctx, iface) +// attachment := NewFileHandleNetworkDeviceAttachment(file) +var DatagramFileAdaptorForInterface = FileAdaptorToInterface[*DatagramPacketForwarder, net.PacketConn] + +// MARK: - DatagramPacketForwarder for datagram file adaptor + +// DatagramPacketForwarder implements PacketForwarder for datagram file descriptor. +type DatagramPacketForwarder struct { + readPktDescsManager *pktDescsManager + writePktDescsManager *pktDescsManager +} + +var _ PacketForwarder[net.PacketConn] = (*DatagramPacketForwarder)(nil) + +// New creates a new DatagramPacketForwarder. +func (f *DatagramPacketForwarder) New() PacketForwarder[net.PacketConn] { + return &DatagramPacketForwarder{} +} + +// Sockopts returns socket options for the given Interface and user desired options. +// Default values are based on the following references: +// - https://developer.apple.com/documentation/virtualization/vzfilehandlenetworkdeviceattachment/maximumtransmissionunit?language=objc +func (*DatagramPacketForwarder) Sockopts(iface *Interface, userOpts Sockopts) Sockopts { + return sockoptsForPacketConn(iface, userOpts) +} + +// ConnAndFile creates a [net.PacketConn] and *[os.File] pair using [syscall.Socketpair]. +func (f *DatagramPacketForwarder) ConnAndFile(opts Sockopts) (net.PacketConn, *os.File, error) { + return packetConnAndFile(opts) +} + +// AllocateBuffers allocates packet descriptor buffers for reading and writing packets. +func (f *DatagramPacketForwarder) AllocateBuffers(iface *Interface) error { + f.readPktDescsManager = newPktDescsManager(iface.MaxReadPacketCount, iface.MaxPacketSize) + f.writePktDescsManager = newPktDescsManager(iface.MaxWritePacketCount, iface.MaxPacketSize) + return nil +} + +// ReadPacketsFromInterface reads packets from the vmnet Interface. +func (f *DatagramPacketForwarder) ReadPacketsFromInterface(iface *Interface, estimatedCount int) (int, error) { + f.readPktDescsManager.reset() + return iface.ReadPackets(f.readPktDescsManager.pktDescs, estimatedCount) +} + +// WritePacketsToConn writes packets to the connection. +func (f *DatagramPacketForwarder) WritePacketsToConn(conn net.PacketConn, packetCount int) (int, error) { + _, err := f.readPktDescsManager.writePacketsToPacketConn(conn, packetCount) + if err != nil { + return 0, err + } + return packetCount, nil +} + +// ReadPacketsFromConn reads packets from the connection. +func (f *DatagramPacketForwarder) ReadPacketsFromConn(conn net.PacketConn) (int, error) { + return f.writePktDescsManager.readPacketsFromPacketConn(conn) +} + +// WritePacketsToInterface writes packets to the vmnet Interface. +func (f *DatagramPacketForwarder) WritePacketsToInterface(iface *Interface, packetCount int) (int, error) { + return iface.WritePackets(f.writePktDescsManager.pktDescs, packetCount) +} + +// sockoptsForPacketConn returns socket options for the given [Interface] and user desired options for [net.PacketConn]. +// Default values are based on the following references: +// - https://developer.apple.com/documentation/virtualization/vzfilehandlenetworkdeviceattachment/maximumtransmissionunit?language=objc +func sockoptsForPacketConn(iface *Interface, userOpts Sockopts) Sockopts { + // Calculate minimum buffer sizes based on interface configuration + packetSize := int(iface.MaxPacketSize) + minPacketCount := max(iface.MaxReadPacketCount, iface.MaxWritePacketCount) + minSendBufSize := packetSize + minRecvBufSize := minSendBufSize * minPacketCount + + // Default socket options + sockopts := Sockopts{ + ReceiveBufferSize: minRecvBufSize * 4, + SendBufferSize: packetSize, + } + // If user specified options, override with minimums as needed + if userOpts.ReceiveBufferSize > 0 { + sockopts.ReceiveBufferSize = max(userOpts.ReceiveBufferSize, minRecvBufSize) + } + if userOpts.SendBufferSize > 0 { + sockopts.SendBufferSize = max(userOpts.SendBufferSize, minSendBufSize) + } + return sockopts +} + +// packetConnAndFile creates a [net.PacketConn] and *[os.File] pair using [syscall.Socketpair]. +func packetConnAndFile(opts Sockopts) (net.PacketConn, *os.File, error) { + sendBufSize, recvBufSize := opts.SendBufferSize, opts.ReceiveBufferSize + connFile, file, err := filePair(syscall.SOCK_DGRAM, sendBufSize, recvBufSize) + if err != nil { + return nil, nil, fmt.Errorf("ConnAndFile failed: %w", err) + } + conn, err := net.FilePacketConn(connFile) + if err != nil { + connFile.Close() + file.Close() + return nil, nil, fmt.Errorf("net.FilePacketConn failed: %w", err) + } + if err = connFile.Close(); err != nil { + conn.Close() + file.Close() + return nil, nil, fmt.Errorf("failed to close connFile: %w", err) + } + return conn, file, nil +} + +// MARK: - pktDescsManager methods for datagram file adaptor + +// buffersForWritingToPacketConn returns [net.Buffers] to write to the [net.PacketConn] +// adjusted their buffer sizes based vm_pkt_size in [VMPktDesc]s read from [Interface]. +// The 4-byte header is excluded. +func (v *pktDescsManager) buffersForWritingToPacketConn(packetCount int) (net.Buffers, error) { + for i, vmPktDesc := range v.iter(packetCount) { + if uint64(vmPktDesc.vm_pkt_size) > v.maxPacketSize { + return nil, fmt.Errorf("vm_pkt_size %d exceeds maxPacketSize %d", vmPktDesc.vm_pkt_size, v.maxPacketSize) + } + // Resize buffer to exclude the 4-byte header + v.writingBuffers[i] = v.backingBuffers[i][headerSize : headerSize+uintptr(vmPktDesc.vm_pkt_size)] + } + return v.writingBuffers[:packetCount], nil +} + +// writePacketsToPacketConn writes packets from VMPktDescs to the net.PacketConn. +// - It returns the number of packets written. +func (v *pktDescsManager) writePacketsToPacketConn(conn net.PacketConn, packetCount int) (int64, error) { + buffers, err := v.buffersForWritingToPacketConn(packetCount) + if err != nil { + return 0, fmt.Errorf("buffersForWritingToPacketConn failed: %w", err) + } + // Get rawConn for syscall.Sendto + rawConn, _ := conn.(syscall.Conn).SyscallConn() + + written := 0 + for written < packetCount { + var sendtoErr error + rawConnWriteErr := rawConn.Write(func(fd uintptr) (done bool) { + for written < packetCount { + // send packet from buffer + if err := syscall.Sendmsg(int(fd), buffers[written], nil, nil, 0); err != nil { + if errors.Is(err, syscall.EAGAIN) { + return false // try again later + } + sendtoErr = fmt.Errorf("syscall.Sendmsg failed: %w", err) + return true + } + written++ + } + return true + }) + if rawConnWriteErr != nil { + return int64(written), fmt.Errorf("rawConn.Write failed: %w", rawConnWriteErr) + } + if sendtoErr != nil { + return int64(written), sendtoErr + } + } + return int64(packetCount), nil +} + +// readPacketsFromPacketConn reads packets from the [net.PacketConn] into [VMPktDesc]s. +// - It returns the number of packets read. +// - The packets are expected to come one by one. +// - It receives all available packets until no more packets are available, packetCount reaches maxPacketCount, or an error occurs. +// - It waits for the connection to be ready for initial packet. +func (v *pktDescsManager) readPacketsFromPacketConn(conn net.PacketConn) (int, error) { + var packetCount int + // Wait until 4-byte header is read + n, _, err := conn.ReadFrom(v.backingBuffers[packetCount][headerSize:]) + if n == 0 { + // normal closure + return 0, errors.New("conn.ReadFrom: use of closed network connection") + } + if err != nil { + return 0, fmt.Errorf("conn.ReadFrom failed: %w", err) + } + v.at(packetCount).SetPacketSize(n) + packetCount++ + // Get rawConn for syscall.Recvfrom + rawConn, _ := conn.(syscall.Conn).SyscallConn() + // Read available packets + for packetCount < v.maxPacketCount { + // Read packet from the connection + var bytesHasBeenRead int + var err error + rawConnReadErr := rawConn.Read(func(fd uintptr) (done bool) { + for packetCount < v.maxPacketCount { + // receive packet into buffer + bytesHasBeenRead, _, err = syscall.Recvfrom(int(fd), v.backingBuffers[packetCount][headerSize:], 0) + if err != nil { + return true + } + v.at(packetCount).SetPacketSize(bytesHasBeenRead) + packetCount++ + } + return true + }) + if rawConnReadErr != nil { + return 0, fmt.Errorf("rawConn.Read failed: %w", rawConnReadErr) + } + if err != nil { + if errors.Is(err, syscall.EAGAIN) { + // no more packets available, normal closure + break + } + return 0, fmt.Errorf("closure in rawConn.Read failed: %w", err) + } + if bytesHasBeenRead == 0 { + // no more packets available, normal closure + break + } + } + return packetCount, nil +} diff --git a/vmnet/fileadapter_darwin.go b/vmnet/fileadapter_darwin.go new file mode 100644 index 00000000..3378c06a --- /dev/null +++ b/vmnet/fileadapter_darwin.go @@ -0,0 +1,175 @@ +package vmnet + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "runtime" + "strings" +) + +// Sockopts holds socket options for the connection. +type Sockopts struct { + ReceiveBufferSize int // [syscall.SO_RCVBUF] + SendBufferSize int // [syscall.SO_SNDBUF] +} + +// Sockopt defines a function type to set socket options. +type Sockopt func(*Sockopts) + +// WithReceiveBufferSize sets the receive buffer size (SO_RCVBUF) for the socket. +// Specified size may be overridden to meet minimum requirements. +func WithReceiveBufferSize(size int) Sockopt { + return func(o *Sockopts) { + o.ReceiveBufferSize = size + } +} + +// WithSendBufferSize sets the send buffer size (SO_SNDBUF) for the socket. +// Specified size may be overridden to meet minimum requirements. +func WithSendBufferSize(size int) Sockopt { + return func(o *Sockopts) { + o.SendBufferSize = size + } +} + +// PacketForwarder defines methods to forward packets between a vmnet [Interface] and a connection [T io.Closer]. +type PacketForwarder[T io.Closer] interface { + // New creates a new PacketForwarder instance. + New() PacketForwarder[T] + + // Preparetion + + // Sockopts returns socket options for the given [Interface] and user desired options. + // Optimal options are calculated based on the Interface configuration and merged with userOpts. + // If userOpts specify buffer sizes smaller than optimal, optimal sizes are used instead. + Sockopts(iface *Interface, userOpts Sockopts) Sockopts + + // ConnAndFile creates a connection [T] and its corresponding *[os.File] pair. + // The connection is used for packet forwarding from/to the [Interface], which actual type is [net.Conn] or [net.PacketConn]. + // The file is used as a file descriptor for QEMU or Virtualization frameworks. + // The socket options are applied to both ends of the pair. + ConnAndFile(sockopts Sockopts) (T, *os.File, error) + // AllocateBuffers allocates packet descriptor buffers based on the [Interface] configuration. + AllocateBuffers(iface *Interface) error + + // Interface -> Conn + + // ReadPacketsFromInterface reads packets from the vmnet [Interface]. + ReadPacketsFromInterface(iface *Interface, estimatedCount int) (int, error) + // WritePacketsToConn writes packets to the connection. + WritePacketsToConn(conn T, packetCount int) (int, error) + + // Conn -> Interface + + // ReadPacketsFromConn reads packets from the connection. + ReadPacketsFromConn(conn T) (int, error) + // WritePacketsToInterface writes packets to the vmnet [Interface]. + WritePacketsToInterface(iface *Interface, packetCount int) (int, error) +} + +// FileAdaptorToInterface is a generic function that returns a file for the given [Network]. +// The returned file is used as a file descriptor for network devices in QEMU or Virtualization frameworks. +// - It starts goroutines to handle packet transfer between the vmnet interface and the file. +// - The context can be used to stop the goroutines and the interface. +// - The returned error channel can be used to receive errors from the goroutines. +func FileAdaptorToInterface[T PacketForwarder[U], U io.Closer](ctx context.Context, iface *Interface, opts ...Sockopt) (*os.File, <-chan error, error) { + var factory T + forwarder := factory.New() + + var userSockopts Sockopts + for _, opt := range opts { + opt(&userSockopts) + } + + // Get socket options from the forwarder + sockopts := forwarder.Sockopts(iface, userSockopts) + + // Create socketpair connection as conn and file + conn, file, err := forwarder.ConnAndFile(sockopts) + if err != nil { + if err2 := iface.Stop(); err2 != nil { + return nil, nil, errors.Join(err, fmt.Errorf("failed to stop iface: %w", err2)) + } + return nil, nil, fmt.Errorf("forwarder.ConnAndFile failed: %w", err) + } + + // Allocate buffers based on interface configuration + if err := forwarder.AllocateBuffers(iface); err != nil { + return nil, nil, fmt.Errorf("failed to allocate buffers: %w", err) + } + + // Channel to report errors from goroutine + errCh := make(chan error, 10) + reportError := func(err error, message string) { + if err != nil { + errCh <- fmt.Errorf("%s: %w", message, err) + } + } + evaluateAndReportError := func(f func() error, message string) { + reportError(f(), message) + } + + go func() { + defer evaluateAndReportError(iface.Stop, "failed to stop iface") + defer evaluateAndReportError(conn.Close, "failed to close conn") + + // Set packets available event packetAvailableEventCallback to read packets from vmnet interface + packetAvailableEventCallback := func(estimatedCount int) { + for estimatedCount > 0 { + var packetCount int + // Read packets from vmnet interface + if packetCount, err = forwarder.ReadPacketsFromInterface(iface, estimatedCount); err != nil { + reportError(err, "forwarder.ReadPacketsFromInterface failed") + return + } + // Write packets to the connection + if _, err := forwarder.WritePacketsToConn(conn, packetCount); err != nil { + reportError(err, "forwarder.WritePacketsToConn failed") + return + } + estimatedCount -= packetCount + } + } + if err := iface.SetPacketsAvailableEventCallback(packetAvailableEventCallback); err != nil { + reportError(err, "SetPacketsAvailableEventCallback failed") + return + } + // Start reading packet from the connection (VM) and writing to vmnet interface. + // Packets comes one by one with 4-byte big-endian header indicating the packet size. + // Read all available packets in a loop. + for { + // Read packets from the connection to writeDescs + packetCount, err := forwarder.ReadPacketsFromConn(conn) + if err != nil { + if strings.Contains(err.Error(), "use of closed network connection") { + // Normal closure + break + } + reportError(err, "forwarder.ReadPacketsFromConn failed") + break + } + // Write packets to vmnet interface + if writtenCount, err := forwarder.WritePacketsToInterface(iface, packetCount); err != nil { + reportError(err, fmt.Sprintf("forwarder.WritePacketsToInterface failed with packetCount=%d, writtenCount=%d", packetCount, writtenCount)) + break + } + } + // Keep readBuffers and writeBuffers alive until the goroutine ends + runtime.KeepAlive(forwarder) + runtime.KeepAlive(iface) + }() + + go func() { + <-ctx.Done() + if err := conn.Close(); err != nil { + if !strings.Contains(err.Error(), "use of closed network connection") { + reportError(err, "failed to close conn on context done") + } + } + }() + + return file, errCh, nil +} diff --git a/vmnet/filepair_darwin.go b/vmnet/filepair_darwin.go new file mode 100644 index 00000000..ef0cbc15 --- /dev/null +++ b/vmnet/filepair_darwin.go @@ -0,0 +1,49 @@ +package vmnet + +import ( + "fmt" + "os" + "syscall" +) + +// filePair creates a pair of connected *[os.File] using [syscall.Socketpair]. +func filePair(typ, sendBufSize, recvBufSize int) (connFile, passingFile *os.File, err error) { + fds, err := syscall.Socketpair(syscall.AF_UNIX, typ, 0) + if err != nil { + return nil, nil, fmt.Errorf("failed to create socketpair: %w", err) + } + connFd, passingFd := fds[0], fds[1] + // Set up connFd. + if err := setupFd(connFd, sendBufSize, recvBufSize); err != nil { + syscall.Close(connFd) + syscall.Close(passingFd) + return nil, nil, err + } + // Set up passingFd. + if err := setupFd(passingFd, sendBufSize, recvBufSize); err != nil { + syscall.Close(connFd) + syscall.Close(passingFd) + return nil, nil, err + } + connFile = os.NewFile(uintptr(connFd), "vmnet-conn-file") + passingFile = os.NewFile(uintptr(passingFd), "vmnet-passing-file") + return connFile, passingFile, nil +} + +// setupFd sets non-blocking and buffer sizes on the given fd. +func setupFd(fd, sendBufSize, recvBufSize int) error { + if err := syscall.SetNonblock(fd, true); err != nil { + return fmt.Errorf("failed to set nonblock on fd: %w", err) + } + if recvBufSize > 0 { + if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, recvBufSize); err != nil { + return fmt.Errorf("failed to set SO_RCVBUF on fd: %w", err) + } + } + if sendBufSize > 0 { + if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_SNDBUF, sendBufSize); err != nil { + return fmt.Errorf("failed to set SO_SNDBUF on fd: %w", err) + } + } + return nil +} diff --git a/vmnet/msg_x_darwin.go b/vmnet/msg_x_darwin.go new file mode 100644 index 00000000..b91bd4f7 --- /dev/null +++ b/vmnet/msg_x_darwin.go @@ -0,0 +1,228 @@ +package vmnet + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework vmnet +# include "msg_x_darwin.h" +*/ +import "C" +import ( + "errors" + "fmt" + "iter" + "net" + "os" + "runtime" + "syscall" + "unsafe" +) + +// MARK: - DatagramNextFileAdaptorForInterface + +// DatagramNextFileAdaptorForInterface returns a file for the given [Network]. +// - It uses [recvmsg_x] and [sendmsg_x] for packet transfer. +// - It starts goroutines to handle packet transfer between the vmnet interface and the file. +// - The context can be used to stop the goroutines and the interface. +// - The returned error channel can be used to receive errors from the goroutines. +// +// The returned file can be used as a file descriptor for QEMU's netdev datagram backend or VZ's [NewFileHandleNetworkDeviceAttachment] +// QEMU: +// +// -netdev datagram,id=net0,addr.type=fd,addr.str= +// +// VZ: +// +// file, errCh, err := DatagramNextFileAdaptorForInterface(ctx, iface) +// attachment := NewFileHandleNetworkDeviceAttachment(file) +// +// [recvmsg_x]: https://github.com/apple-oss-distributions/xnu/blob/94d3b452840153a99b38a3a9659680b2a006908e/bsd/sys/socket.h#L1425-L1455 +// [sendmsg_x]: https://github.com/apple-oss-distributions/xnu/blob/94d3b452840153a99b38a3a9659680b2a006908e/bsd/sys/socket.h#L1457-L1487 +var DatagramNextFileAdaptorForInterface = FileAdaptorToInterface[*DatagramNextPacketForwarder, net.PacketConn] + +// MARK: - DatagramNextPacketForwarder for DatagramNext file adaptor + +// DatagramNextPacketForwarder implements PacketForwarder for DatagramNext file descriptor. +type DatagramNextPacketForwarder struct { + readMsgHdrsArray *msgHdrXArray + writeMsgHdrsArray *msgHdrXArray + localAddr net.Addr +} + +var _ PacketForwarder[net.PacketConn] = (*DatagramNextPacketForwarder)(nil) + +// New creates a new DatagramNextPacketForwarder. +func (*DatagramNextPacketForwarder) New() PacketForwarder[net.PacketConn] { + return &DatagramNextPacketForwarder{} +} + +// Sockopts returns socket options for the given Interface and user desired options. +// Default values are based on the following references: +// - https://developer.apple.com/documentation/virtualization/vzfilehandlenetworkdeviceattachment/maximumtransmissionunit?language=objc +func (*DatagramNextPacketForwarder) Sockopts(iface *Interface, userOpts Sockopts) Sockopts { + // Same as DatagramPacketForwarder + return sockoptsForPacketConn(iface, userOpts) +} + +// ConnAndFile creates a [net.PacketConn] and *[os.File] pair using [syscall.Socketpair]. +func (f *DatagramNextPacketForwarder) ConnAndFile(opts Sockopts) (net.PacketConn, *os.File, error) { + conn, file, err := packetConnAndFile(opts) + if err != nil { + return nil, nil, err + } + // Save local address for initialize msgHdrX + f.localAddr = conn.LocalAddr() + return conn, file, nil +} + +// AllocateBuffers allocates message header buffers for reading and writing packets. +func (f *DatagramNextPacketForwarder) AllocateBuffers(iface *Interface) error { + f.readMsgHdrsArray = newMsgHdrXArray(iface.MaxReadPacketCount, iface.MaxPacketSize, f.localAddr) + f.writeMsgHdrsArray = newMsgHdrXArray(iface.MaxWritePacketCount, iface.MaxPacketSize, f.localAddr) + return nil +} + +// ReadPacketsFromInterface reads packets from the vmnet Interface. +func (f *DatagramNextPacketForwarder) ReadPacketsFromInterface(iface *Interface, estimatedCount int) (int, error) { + f.readMsgHdrsArray.reset() + return iface.ReadPackets(f.readMsgHdrsArray.pkgDescsMgr.pktDescs, estimatedCount) +} + +// WritePacketsToConn writes packets to the connection. +func (f *DatagramNextPacketForwarder) WritePacketsToConn(conn net.PacketConn, packetCount int) (int, error) { + _, err := f.readMsgHdrsArray.writePacketsToPacketConn(conn, packetCount) + if err != nil { + return 0, err + } + return packetCount, nil +} + +// ReadPacketsFromConn reads packets from the connection. +func (f *DatagramNextPacketForwarder) ReadPacketsFromConn(conn net.PacketConn) (int, error) { + return f.writeMsgHdrsArray.readPacketsFromPacketConn(conn) +} + +// WritePacketsToInterface writes packets to the vmnet Interface. +func (f *DatagramNextPacketForwarder) WritePacketsToInterface(iface *Interface, packetCount int) (int, error) { + return iface.WritePackets(f.writeMsgHdrsArray.pkgDescsMgr.pktDescs, packetCount) +} + +// MARK: - msgHdrXArray and its methods + +// msgHdrX is a Go representation of C.struct_msghdr_x. +type msgHdrX C.struct_msghdr_x + +// msgHdrXArray manages an array of msgHdrX and its pktDescsManager. +type msgHdrXArray struct { + msgHdrs *msgHdrX + pkgDescsMgr *pktDescsManager +} + +func newMsgHdrXArray(count int, maxPacketSize uint64, _ net.Addr) *msgHdrXArray { + m := &msgHdrXArray{ + msgHdrs: (*msgHdrX)(C.allocateMsgHdrXArray(C.int(count))), + pkgDescsMgr: newPktDescsManager(count, maxPacketSize), + } + // sa, len, err := addrToSockaddr(addr) + // if err != nil { + // panic(fmt.Sprintf("addrToSockaddr failed: %v", err)) + // } + runtime.AddCleanup(m, func(self *C.struct_msghdr_x) { C.deallocateMsgHdrXArray(self) }, (*C.struct_msghdr_x)(m.msgHdrs)) + // Initialize msgHdrX's iov to point to pktDescs' iov + for msgHdrX, pktDesc := range m.iter(count) { + msgHdrX.msg_name = nil + msgHdrX.msg_namelen = 0 + msgHdrX.msg_iov = pktDesc.vm_pkt_iov + msgHdrX.msg_iovlen = 1 + } + return m +} + +// at returns the msgHdrX at index i. +func (m *msgHdrXArray) at(i int) *msgHdrX { + return (*msgHdrX)(unsafe.Pointer(uintptr(unsafe.Pointer(m.msgHdrs)) + uintptr(i)*unsafe.Sizeof(msgHdrX{}))) +} + +// iter iterates over the msgHdrXArray. +func (m *msgHdrXArray) iter(packetCount int) iter.Seq2[*msgHdrX, *VMPktDesc] { + return func(yield func(*msgHdrX, *VMPktDesc) bool) { + for i := range packetCount { + if !yield(m.at(i), m.pkgDescsMgr.at(i)) { + return + } + } + } +} + +// reset resets the pktDescsManager and updates msg_datalen for each msgHdrX. +func (m *msgHdrXArray) reset() { + m.pkgDescsMgr.reset() + m.clearDataLenAndFlags() +} + +// clearDataLenAndFlags updates msg_datalen from msg_iov.iov_len for each msgHdrX. +func (m *msgHdrXArray) clearDataLenAndFlags() { + for msgHdrX := range m.iter(m.pkgDescsMgr.maxPacketCount) { + msgHdrX.msg_datalen = 0 + msgHdrX.msg_flags = 0 + } +} + +func (m *msgHdrXArray) writePacketsToPacketConn(conn net.PacketConn, packetCount int) (int64, error) { + m.clearDataLenAndFlags() + // Get rawConn for C.sendmsg_x + rawConn, _ := conn.(syscall.Conn).SyscallConn() + var sentCount int + var sendmsgErr error + for sentCount < packetCount { + rawConnWriteErr := rawConn.Write(func(fd uintptr) (done bool) { + n, err := C.sendmsg_x(C.int(fd), (*C.struct_msghdr_x)(m.at(sentCount)), C.u_int(packetCount-sentCount), 0) + if n < 0 { + if errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.ENOBUFS) { + return false // try again later + } + sendmsgErr = fmt.Errorf("sendmsg_x failed: %w", err) + return true + } + sentCount += int(n) + return true + }) + if rawConnWriteErr != nil { + return 0, rawConnWriteErr + } + if sendmsgErr != nil { + return 0, sendmsgErr + } + } + return int64(sentCount), nil +} + +func (m *msgHdrXArray) readPacketsFromPacketConn(conn net.PacketConn) (int, error) { + m.reset() + // Get rawConn for C.recvmsg_x + rawConn, _ := conn.(syscall.Conn).SyscallConn() + var packetCount int + var recvmsgErr error + rawConnReadErr := rawConn.Read(func(fd uintptr) (done bool) { + n, err := C.recvmsg_x(C.int(fd), (*C.struct_msghdr_x)(m.msgHdrs), C.u_int(m.pkgDescsMgr.maxPacketCount), 0) + if n < 0 { + if errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.ENOBUFS) { + return false // try again later + } + recvmsgErr = fmt.Errorf("recvmsg_x failed: %w", err) + return true + } + packetCount = int(n) + return true + }) + if rawConnReadErr != nil { + return 0, rawConnReadErr + } + if recvmsgErr != nil { + return 0, recvmsgErr + } + for msgHdrX, pktDesc := range m.iter(packetCount) { + // Update pktDesc's packet size from msg_iov.iov_len + pktDesc.SetPacketSize(int(msgHdrX.msg_datalen)) + } + return packetCount, nil +} diff --git a/vmnet/msg_x_darwin.h b/vmnet/msg_x_darwin.h new file mode 100644 index 00000000..89176bcc --- /dev/null +++ b/vmnet/msg_x_darwin.h @@ -0,0 +1,103 @@ +#pragma once + +#import +#import +#import + +// MARK: - msghdr_x + +// https://github.com/apple-oss-distributions/xnu/blob/94d3b452840153a99b38a3a9659680b2a006908e/bsd/sys/socket.h#L878-L896 +/* + * Extended version for sendmsg_x() and recvmsg_x() calls + * + * For recvmsg_x(), the size of the data received is given by the field + * msg_datalen. + * + * For sendmsg_x(), the size of the data to send is given by the length of + * the iovec array -- like sendmsg(). The field msg_datalen is ignored. + */ +struct msghdr_x { + void *msg_name; /* optional address */ + socklen_t msg_namelen; /* size of address */ + struct iovec *msg_iov; /* scatter/gather array */ + int msg_iovlen; /* # elements in msg_iov */ + void *msg_control; /* ancillary data, see below */ + socklen_t msg_controllen; /* ancillary data buffer len */ + int msg_flags; /* flags on received message */ + size_t msg_datalen; /* byte length of buffer in msg_iov */ +}; + +// MARK: - recvmsg_x + +// https://github.com/apple-oss-distributions/xnu/blob/94d3b452840153a99b38a3a9659680b2a006908e/bsd/sys/socket.h#L1425-L1455 +/* + * recvmsg_x() is a system call similar to recvmsg(2) to receive + * several datagrams at once in the array of message headers "msgp". + * + * recvmsg_x() can be used only with protocols handlers that have been specially + * modified to support sending and receiving several datagrams at once. + * + * The size of the array "msgp" is given by the argument "cnt". + * + * The "flags" arguments supports only the value MSG_DONTWAIT. + * + * Each member of "msgp" array is of type "struct msghdr_x". + * + * The "msg_iov" and "msg_iovlen" are input parameters that describe where to + * store a datagram in a scatter gather locations of buffers -- see recvmsg(2). + * On output the field "msg_datalen" gives the length of the received datagram. + * + * The field "msg_flags" must be set to zero on input. On output, "msg_flags" + * may have MSG_TRUNC set to indicate the trailing portion of the datagram was + * discarded because the datagram was larger than the buffer supplied. + * recvmsg_x() returns as soon as a datagram is truncated. + * + * recvmsg_x() may return with less than "cnt" datagrams received based on + * the low water mark and the amount of data pending in the socket buffer. + * + * recvmsg_x() returns the number of datagrams that have been received, + * or -1 if an error occurred. + * + * NOTE: This a private system call, the API is subject to change. + */ +ssize_t recvmsg_x(int s, const struct msghdr_x *msgp, u_int cnt, int flags); + +// MARK: - sendmsg_x + +// https://github.com/apple-oss-distributions/xnu/blob/94d3b452840153a99b38a3a9659680b2a006908e/bsd/sys/socket.h#L1457-L1487 +/* + * sendmsg_x() is a system call similar to send(2) to send + * several datagrams at once in the array of message headers "msgp". + * + * sendmsg_x() can be used only with protocols handlers that have been specially + * modified to support sending and receiving several datagrams at once. + * + * The size of the array "msgp" is given by the argument "cnt". + * + * The "flags" arguments supports only the value MSG_DONTWAIT. + * + * Each member of "msgp" array is of type "struct msghdr_x". + * + * The "msg_iov" and "msg_iovlen" are input parameters that specify the + * data to be sent in a scatter gather locations of buffers -- see sendmsg(2). + * + * sendmsg_x() fails with EMSGSIZE if the sum of the length of the datagrams + * is greater than the high water mark. + * + * Address and ancillary data are not supported so the following fields + * must be set to zero on input: + * "msg_name", "msg_namelen", "msg_control" and "msg_controllen". + * + * The field "msg_flags" and "msg_datalen" must be set to zero on input. + * + * sendmsg_x() returns the number of datagrams that have been sent, + * or -1 if an error occurred. + * + * NOTE: This a private system call, the API is subject to change. + */ +ssize_t sendmsg_x(int s, const struct msghdr_x *msgp, u_int cnt, int flags); + +// MARK: - helpers + +struct msghdr_x *allocateMsgHdrXArray(int count); +void deallocateMsgHdrXArray(struct msghdr_x *msgHdrs); diff --git a/vmnet/msg_x_darwin.m b/vmnet/msg_x_darwin.m new file mode 100644 index 00000000..b4b2fd44 --- /dev/null +++ b/vmnet/msg_x_darwin.m @@ -0,0 +1,16 @@ +#import "msg_x_darwin.h" + +// MARK: - helpers + +struct msghdr_x *allocateMsgHdrXArray(int count) +{ + size_t totalSize = sizeof(struct msghdr_x) * count; + struct msghdr_x *msgHdrs = (struct msghdr_x *)malloc(totalSize); + memset(msgHdrs, 0, totalSize); + return msgHdrs; +} + +void deallocateMsgHdrXArray(struct msghdr_x *msgHdrs) +{ + free(msgHdrs); +} diff --git a/vmnet/pktdesc_darwin.go b/vmnet/pktdesc_darwin.go new file mode 100644 index 00000000..29c1dd0c --- /dev/null +++ b/vmnet/pktdesc_darwin.go @@ -0,0 +1,81 @@ +package vmnet + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework vmnet +# include "vmnet_darwin.h" +*/ +import "C" +import ( + "iter" + "net" + "runtime" + "slices" + "unsafe" +) + +const headerSize = unsafe.Sizeof(C.uint32_t(0)) + +// VMPktDesc is a Go representation of C.struct_vmpktdesc. +type VMPktDesc C.struct_vmpktdesc + +// SetPacketSize sets the packet size in VMPktDesc. +func (v *VMPktDesc) SetPacketSize(size int) { + v.vm_pkt_size = C.size_t(size) + v.vm_pkt_iov.iov_len = C.size_t(size) +} + +// pktDescsManager manages pktDescs and their backing buffers. +type pktDescsManager struct { + pktDescs *VMPktDesc + backingBuffers net.Buffers + writingBuffers net.Buffers + maxPacketCount int + maxPacketSize uint64 +} + +// newPktDescsManager allocates pktDesc array and backing buffers. +// pktDesc's iov_base points to the buffer after 4-byte header. +// The 4-byte header is reserved for packet size to the connection. +func newPktDescsManager(count int, maxPacketSize uint64) *pktDescsManager { + v := &pktDescsManager{ + pktDescs: (*VMPktDesc)(C.allocateVMPktDescArray(C.int(count), C.uint64_t(maxPacketSize))), + backingBuffers: make(net.Buffers, 0, count), + maxPacketCount: count, + maxPacketSize: maxPacketSize, + } + runtime.AddCleanup(v, func(self *C.struct_vmpktdesc) { C.deallocateVMPktDescArray(self) }, (*C.struct_vmpktdesc)(v.pktDescs)) + bufLen := maxPacketSize + uint64(headerSize) + for i := range count { + // Allocate buffer with extra 4 bytes for header + buf := make([]byte, bufLen) + vmPktDesc := v.at(i) + // point after the 4-byte header + vmPktDesc.vm_pkt_iov.iov_base = unsafe.Add(unsafe.Pointer(unsafe.SliceData(buf)), headerSize) + vmPktDesc.vm_pkt_iov.iov_len = C.size_t(maxPacketSize) + v.backingBuffers = append(v.backingBuffers, buf) + } + v.writingBuffers = slices.Clone(v.backingBuffers) + return v +} + +// at returns the pointer to the pktDesc at the given index. +func (v *pktDescsManager) at(index int) *VMPktDesc { + return (*VMPktDesc)(unsafe.Add(unsafe.Pointer(v.pktDescs), index*int(unsafe.Sizeof(VMPktDesc{})))) +} + +// iter iterates over pktDescs and their corresponding buffers. +func (v *pktDescsManager) iter(packetCount int) iter.Seq2[int, *VMPktDesc] { + return func(yield func(int, *VMPktDesc) bool) { + for i := range packetCount { + if !yield(i, v.at(i)) { + return + } + } + } +} + +// reset resets pktDescs to initial state. +func (v *pktDescsManager) reset() { + C.resetVMPktDescArray((*C.struct_vmpktdesc)(v.pktDescs), C.int(v.maxPacketCount), C.uint64_t(v.maxPacketSize)) +} diff --git a/vmnet/stream_darwin.go b/vmnet/stream_darwin.go new file mode 100644 index 00000000..88d86da2 --- /dev/null +++ b/vmnet/stream_darwin.go @@ -0,0 +1,227 @@ +package vmnet + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework vmnet +# include "vmnet_darwin.h" +*/ +import "C" +import ( + "encoding/binary" + "errors" + "fmt" + "net" + "os" + "syscall" + + "golang.org/x/sys/unix" +) + +// MARK: - StreamFileAdaptorForInterface + +// StreamFileAdaptorForInterface returns a file for the given [Network]. +// - It starts goroutines to handle packet transfer between the vmnet interface and the file. +// - The context can be used to stop the goroutines and the interface. +// - The returned error channel can be used to receive errors from the goroutines. +// +// The returned file can be used as a file descriptor for QEMU's netdev stream or socket backend. +// +// -netdev socket,id=net0,fd= +// -netdev stream,id=net0,addr.type=fd,addr.str= +var StreamFileAdaptorForInterface = FileAdaptorToInterface[*StreamPacketForwarder, net.Conn] + +// MARK: - StreamPacketForwarder for stream + +// StreamPacketForwarder implements PacketForwarder for stream file descriptor. +type StreamPacketForwarder struct { + readPktDescsManager *pktDescsManager + writePktDescsManager *pktDescsManager +} + +var _ PacketForwarder[net.Conn] = (*StreamPacketForwarder)(nil) + +// New creates a new StreamPacketForwarder. +func (*StreamPacketForwarder) New() PacketForwarder[net.Conn] { + return &StreamPacketForwarder{} +} + +// Sockopts returns socket options for the given Interface and user desired options. +// Default values are based on the following references: +// - https://developer.apple.com/documentation/virtualization/vzfilehandlenetworkdeviceattachment/maximumtransmissionunit?language=objc +func (*StreamPacketForwarder) Sockopts(iface *Interface, userOpts Sockopts) Sockopts { + // Calculate minimum buffer sizes based on interface configuration + packetSize := int(iface.MaxPacketSize) + int(headerSize) + minPacketCount := max(iface.MaxReadPacketCount, iface.MaxWritePacketCount) + minSendBufSize := packetSize * minPacketCount + minRecvBufSize := minSendBufSize + + // Default socket options + sockopts := Sockopts{ + ReceiveBufferSize: minRecvBufSize * 4, + SendBufferSize: minSendBufSize * 1, + } + // If user specified options, override with minimums as needed + if userOpts.ReceiveBufferSize > 0 { + sockopts.ReceiveBufferSize = max(userOpts.ReceiveBufferSize, minRecvBufSize) + } + if userOpts.SendBufferSize > 0 { + sockopts.SendBufferSize = max(userOpts.SendBufferSize, minSendBufSize) + } + return sockopts +} + +// connAndFile creates a [net.Conn] and *[os.File] pair using [syscall.Socketpair]. +func (*StreamPacketForwarder) ConnAndFile(opts Sockopts) (net.Conn, *os.File, error) { + sendBufSize, recvBufSize := opts.SendBufferSize, opts.ReceiveBufferSize + connFile, file, err := filePair(syscall.SOCK_STREAM, sendBufSize, recvBufSize) + if err != nil { + return nil, nil, fmt.Errorf("ConnAndFile failed: %w", err) + } + conn, err := net.FileConn(connFile) + if err != nil { + connFile.Close() + file.Close() + return nil, nil, fmt.Errorf("net.FileConn failed: %w", err) + } + if err = connFile.Close(); err != nil { + conn.Close() + file.Close() + return nil, nil, fmt.Errorf("failed to close connFile: %w", err) + } + return conn, file, nil +} + +// AllocateBuffers allocates packet descriptor buffers for reading and writing packets. +func (f *StreamPacketForwarder) AllocateBuffers(iface *Interface) error { + f.readPktDescsManager = newPktDescsManager(iface.MaxReadPacketCount, iface.MaxPacketSize) + f.writePktDescsManager = newPktDescsManager(iface.MaxWritePacketCount, iface.MaxPacketSize) + return nil +} + +// ReadPacketsFromInterface reads packets from the vmnet Interface. +func (f *StreamPacketForwarder) ReadPacketsFromInterface(iface *Interface, estimatedCount int) (int, error) { + f.readPktDescsManager.reset() + return iface.ReadPackets(f.readPktDescsManager.pktDescs, estimatedCount) +} + +// WritePacketsToConn writes packets to the connection. +func (f *StreamPacketForwarder) WritePacketsToConn(conn net.Conn, packetCount int) (int, error) { + _, err := f.readPktDescsManager.writePacketsToConn(conn, packetCount) + if err != nil { + return 0, err + } + return packetCount, nil +} + +// ReadPacketsFromConn reads packets from the connection. +func (f *StreamPacketForwarder) ReadPacketsFromConn(conn net.Conn) (int, error) { + return f.writePktDescsManager.readPacketsFromConn(conn) +} + +// WritePacketsToInterface writes packets to the vmnet Interface. +func (f *StreamPacketForwarder) WritePacketsToInterface(iface *Interface, packetCount int) (int, error) { + return iface.WritePackets(f.writePktDescsManager.pktDescs, packetCount) +} + +// MARK: - pktDescsManager methods for stream file adaptor + +// buffersForWritingToConn returns [net.Buffers] to write to the [net.Conn] +// adjusted their buffer sizes based vm_pkt_size in [VMPktDesc]s read from [Interface]. +func (v *pktDescsManager) buffersForWritingToConn(packetCount int) (net.Buffers, error) { + for i, vmPktDesc := range v.iter(packetCount) { + if uint64(vmPktDesc.vm_pkt_size) > v.maxPacketSize { + return nil, fmt.Errorf("vm_pkt_size %d exceeds maxPacketSize %d", vmPktDesc.vm_pkt_size, v.maxPacketSize) + } + // Write packet size to the 4-byte header + binary.BigEndian.PutUint32(v.backingBuffers[i][:headerSize], uint32(vmPktDesc.vm_pkt_size)) + // Resize buffer to include header and packet size + v.writingBuffers[i] = v.backingBuffers[i][:headerSize+uintptr(vmPktDesc.vm_pkt_size)] + } + return v.writingBuffers[:packetCount], nil +} + +// writePacketsToConn writes packets from [VMPktDesc]s to the [net.Conn]. +// - It returns the number of bytes written. +func (v *pktDescsManager) writePacketsToConn(conn net.Conn, packetCount int) (int64, error) { + // To use built-in Writev implementation in net package (internal/poll.FD.Writev), + // we use net.Buffers and its WriteTo method. + buffers, err := v.buffersForWritingToConn(packetCount) + if err != nil { + return 0, fmt.Errorf("buffersForWritingToConn failed: %w", err) + } + n, err := buffers.WriteTo(conn) + if err != nil { + return n, fmt.Errorf("buffers.WriteTo failed: %w", err) + } + return n, nil +} + +// readPacketsFromConn reads packets from the [net.Conn] into [VMPktDesc]s. +// - It returns the number of packets read. +// - The packets are expected to come one by one with 4-byte big-endian header indicating the packet size. +// - It reads all available packets until no more packets are available, packetCount reaches maxPacketCount, or an error occurs. +// - It waits for the connection to be ready for initial read of 4-byte header. +func (v *pktDescsManager) readPacketsFromConn(conn net.Conn) (int, error) { + var packetCount int + // Wait until 4-byte header is read + if _, err := conn.Read(v.backingBuffers[packetCount][:headerSize]); err != nil { + return 0, fmt.Errorf("conn.Read failed: %w", err) + } + // Get rawConn for Readv + rawConn, _ := conn.(syscall.Conn).SyscallConn() + // Read available packets + var packetLen uint32 + var bufs net.Buffers + for { + packetLen = binary.BigEndian.Uint32(v.backingBuffers[packetCount][:headerSize]) + if packetLen == 0 || uint64(packetLen) > v.maxPacketSize { + return 0, fmt.Errorf("invalid packetLen: %d (max %d)", packetLen, v.maxPacketSize) + } + + // prepare buffers for reading packet and next header if any + if packetCount+1 < v.maxPacketCount { + // prepare next header read as well + bufs = net.Buffers{ + v.backingBuffers[packetCount][headerSize : headerSize+uintptr(packetLen)], + v.backingBuffers[packetCount+1][:headerSize], + } + } else { + // prepare only packet read to avoid exceeding maxPacketCount + bufs = net.Buffers{ + v.backingBuffers[packetCount][headerSize : headerSize+uintptr(packetLen)], + } + } + + // Read packet from the connection + var bytesHasBeenRead int + var err error + rawConnReadErr := rawConn.Read(func(fd uintptr) (done bool) { + // read packet into buffers + bytesHasBeenRead, err = unix.Readv(int(fd), bufs) + if bytesHasBeenRead <= 0 { + if errors.Is(err, syscall.EAGAIN) { + return false // try again later + } + err = fmt.Errorf("unix.Readv failed: %w", err) + return true + } + // assumes partial read of a packet does not happen since packet len is already known + return true + }) + if rawConnReadErr != nil { + return 0, fmt.Errorf("rawConn.Read failed: %w", rawConnReadErr) + } + if err != nil { + return 0, fmt.Errorf("closure in rawConn.Read failed: %w", err) + } + v.at(packetCount).SetPacketSize(int(packetLen)) + packetCount++ + if bytesHasBeenRead == int(packetLen) { + // next packet seems not available now, or reached maxPacketCount + break + } else if bytesHasBeenRead != int(packetLen)+int(headerSize) { + return 0, fmt.Errorf("unexpected bytesHasBeenRead: %d (expected %d or %d)", bytesHasBeenRead, packetLen, packetLen+uint32(headerSize)) + } + } + return packetCount, nil +} diff --git a/vmnet/vmnet_darwin.go b/vmnet/vmnet_darwin.go new file mode 100644 index 00000000..8799ed24 --- /dev/null +++ b/vmnet/vmnet_darwin.go @@ -0,0 +1,603 @@ +package vmnet + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation -framework vmnet +# include "vmnet_darwin.h" +*/ +import "C" +import ( + "errors" + "fmt" + "net" + "net/netip" + "runtime" + "runtime/cgo" + "unsafe" + + "github.com/Code-Hex/vz/v3/internal/cgohandler" + "github.com/Code-Hex/vz/v3/internal/objc" + "github.com/Code-Hex/vz/v3/internal/osversion" + "github.com/Code-Hex/vz/v3/xpc" + "golang.org/x/sys/unix" +) + +var macOSAvailable = osversion.MacOSAvailable + +// MARK: - Return + +// The status code returning the result of vmnet operations. +// - https://developer.apple.com/documentation/vmnet/vmnet_return_t?language=objc +type Return C.uint32_t + +const ( + ErrSuccess Return = C.VMNET_SUCCESS // VMNET_SUCCESS Successfully completed. + ErrFailure Return = C.VMNET_FAILURE // VMNET_FAILURE General failure. + ErrMemFailure Return = C.VMNET_MEM_FAILURE // VMNET_MEM_FAILURE Memory allocation failure. + ErrInvalidArgument Return = C.VMNET_INVALID_ARGUMENT // VMNET_INVALID_ARGUMENT Invalid argument specified. + ErrSetupIncomplete Return = C.VMNET_SETUP_INCOMPLETE // VMNET_SETUP_INCOMPLETE Interface setup is not complete. + ErrInvalidAccess Return = C.VMNET_INVALID_ACCESS // VMNET_INVALID_ACCESS Permission denied. + ErrPacketTooBig Return = C.VMNET_PACKET_TOO_BIG // VMNET_PACKET_TOO_BIG Packet size larger than MTU. + ErrBufferExhausted Return = C.VMNET_BUFFER_EXHAUSTED // VMNET_BUFFER_EXHAUSTED Buffers exhausted in kernel. + ErrTooManyPackets Return = C.VMNET_TOO_MANY_PACKETS // VMNET_TOO_MANY_PACKETS Packet count exceeds limit. + ErrSharingServiceBusy Return = C.VMNET_SHARING_SERVICE_BUSY // VMNET_SHARING_SERVICE_BUSY Vmnet Interface cannot be started as conflicting sharing service is in use. + ErrNotAuthorized Return = C.VMNET_NOT_AUTHORIZED // VMNET_NOT_AUTHORIZED The operation could not be completed due to missing authorization. +) + +var _ error = Return(0) + +func (e Return) Error() string { + switch e { + case ErrSuccess: + return "Vmnet: Successfully completed" + case ErrFailure: + return "Vmnet: Failure" + case ErrMemFailure: + return "Vmnet: Memory allocation failure" + case ErrInvalidArgument: + return "Vmnet: Invalid argument specified" + case ErrSetupIncomplete: + return "Vmnet: Interface setup is not complete" + case ErrInvalidAccess: + return "Vmnet: Permission denied" + case ErrPacketTooBig: + return "Vmnet: Packet size larger than MTU" + case ErrBufferExhausted: + return "Vmnet: Buffers exhausted in kernel" + case ErrTooManyPackets: + return "Vmnet: Packet count exceeds limit" + case ErrSharingServiceBusy: + return "Vmnet: Vmnet Interface cannot be started as conflicting sharing service is in use" + case ErrNotAuthorized: + return "Vmnet: The operation could not be completed due to missing authorization" + default: + return fmt.Sprintf("Vmnet: Unknown error %d", uint32(e)) + } +} + +// MARK: - Mode + +// Mode defines the mode of a [Network]. (See [operating_modes_t]) +// - [HostMode] and [SharedMode] are supported by [NewNetworkConfiguration]. +// - VMNET_BRIDGED_MODE is not supported by underlying API [vmnet_network_configuration_create]. +// +// [operating_modes_t]: https://developer.apple.com/documentation/vmnet/operating_modes_t?language=objc +// [vmnet_network_configuration_create]: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_create(_:_:)?language=objc +type Mode uint32 + +const ( + // https://developer.apple.com/documentation/vmnet/operating_modes_t/vmnet_host_mode?language=objc + HostMode Mode = C.VMNET_HOST_MODE + // https://developer.apple.com/documentation/vmnet/operating_modes_t/vmnet_shared_mode?language=objc + SharedMode Mode = C.VMNET_SHARED_MODE +) + +// MARK: - object + +type pointer = objc.Pointer + +// object +type object struct { + *pointer +} + +// retain calls CFRetain on the underlying object. +func (o *object) retain() { + C.vmnetRetain(objc.Ptr(o)) +} + +// releaseOnCleanup registers a cleanup function to release the object when cleaned up. +func (o *object) releaseOnCleanup() { + runtime.AddCleanup(o, func(p unsafe.Pointer) { + C.vmnetRelease(p) + }, objc.Ptr(o)) +} + +// Retain calls retain method on the given object and returns it. +func Retain[O interface{ retain() }](o O) O { + o.retain() + return o +} + +// ReleaseOnCleanup calls releaseOnCleanup method on the given object and returns it. +func ReleaseOnCleanup[O interface{ releaseOnCleanup() }](o O) O { + o.releaseOnCleanup() + return o +} + +// MARK: - NetworkConfiguration + +// NetworkConfiguration represents a [vmnet_network_configuration_ref]. +// +// [vmnet_network_configuration_ref]: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_ref?language=objc +type NetworkConfiguration struct { + *object +} + +// NewNetworkConfiguration creates a new [NetworkConfiguration] with [Mode]. +// This is only supported on macOS 26 and newer, error will be returned on older versions. +// [BridgedMode] is not supported by this function. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_create(_:_:)?language=objc +func NewNetworkConfiguration(mode Mode) (*NetworkConfiguration, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + var status Return + ptr := C.VmnetNetworkConfigurationCreate( + C.uint32_t(mode), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrSuccess) { + return nil, fmt.Errorf("failed to create VmnetNetworkConfiguration: %w", status) + } + config := &NetworkConfiguration{object: &object{objc.NewPointer(ptr)}} + ReleaseOnCleanup(config) + return config, nil +} + +// AddDhcpReservation configures a new DHCP reservation for the [Network]. +// client is the MAC address for which the DHCP address is reserved. +// reservation is the DHCP IPv4 address to be reserved. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_add_dhcp_reservation(_:_:_:)?language=objc +func (c *NetworkConfiguration) AddDhcpReservation(client net.HardwareAddr, reservation netip.Addr) error { + if !reservation.Is4() { + return fmt.Errorf("reservation is not ipv4") + } + ip := reservation.As4() + var cReservation C.struct_in_addr + + cClient, err := netHardwareAddrToEtherAddr(client) + if err != nil { + return err + } + copy((*[4]byte)(unsafe.Pointer(&cReservation))[:], ip[:]) + + status := C.VmnetNetworkConfiguration_addDhcpReservation( + objc.Ptr(c), + &cClient, + &cReservation, + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to add dhcp reservation: %w", Return(status)) + } + return nil +} + +// AddPortForwardingRule configures a port forwarding rule for the [Network]. +// These rules will not be able to be removed or queried until network has been started. +// To do that, use `vmnet_interface_remove_ip_forwarding_rule` or +// `vmnet_interface_get_ip_port_forwarding_rules` C API directly. +// (`vmnet_interface` related functionality not implemented in this package yet) +// +// protocol must be either IPPROTO_TCP or IPPROTO_UDP +// addressFamily must be either AF_INET or AF_INET6 +// internalPort is the TCP or UDP port that forwarded traffic should be redirected to. +// externalPort is the TCP or UDP port on the outside network that should be redirected from. +// internalAddress is the IPv4 or IPv6 address of the machine on the internal network that should receive the forwarded traffic. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_add_port_forwarding_rule(_:_:_:_:_:_:)?language=objc +func (c *NetworkConfiguration) AddPortForwardingRule(protocol uint8, addressFamily uint8, internalPort uint16, externalPort uint16, internalAddress netip.Addr) error { + var address unsafe.Pointer + switch addressFamily { + case unix.AF_INET: + if !internalAddress.Is4() { + return fmt.Errorf("internal address is not ipv4") + } + var inAddr C.struct_in_addr + ip := internalAddress.As4() + copy((*[4]byte)(unsafe.Pointer(&inAddr))[:], ip[:]) + address = unsafe.Pointer(&inAddr) + case unix.AF_INET6: + if !internalAddress.Is6() { + return fmt.Errorf("internal address is not ipv6") + } + var in6Addr C.struct_in6_addr + ip := internalAddress.As16() + copy((*[16]byte)(unsafe.Pointer(&in6Addr))[:], ip[:]) + address = unsafe.Pointer(&in6Addr) + default: + return fmt.Errorf("unsupported address family: %d", addressFamily) + } + status := C.VmnetNetworkConfiguration_addPortForwardingRule( + objc.Ptr(c), + C.uint8_t(protocol), + C.sa_family_t(addressFamily), + C.uint16_t(internalPort), + C.uint16_t(externalPort), + address, + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to add port forwarding rule: %w", Return(status)) + } + return nil +} + +// DisableDhcp disables DHCP server on the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_dhcp(_:)?language=objc +func (c *NetworkConfiguration) DisableDhcp() { + C.VmnetNetworkConfiguration_disableDhcp(objc.Ptr(c)) +} + +// DisableDnsProxy disables DNS proxy on the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_dns_proxy(_:)?language=objc +func (c *NetworkConfiguration) DisableDnsProxy() { + C.VmnetNetworkConfiguration_disableDnsProxy(objc.Ptr(c)) +} + +// DisableNat44 disables NAT44 on the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_nat44(_:)?language=objc +func (c *NetworkConfiguration) DisableNat44() { + C.VmnetNetworkConfiguration_disableNat44(objc.Ptr(c)) +} + +// DisableNat66 disables NAT66 on the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_nat66(_:)?language=objc +func (c *NetworkConfiguration) DisableNat66() { + C.VmnetNetworkConfiguration_disableNat66(objc.Ptr(c)) +} + +// DisableRouterAdvertisement disables router advertisement on the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_router_advertisement(_:)?language=objc +func (c *NetworkConfiguration) DisableRouterAdvertisement() { + C.VmnetNetworkConfiguration_disableRouterAdvertisement(objc.Ptr(c)) +} + +// SetExternalInterface sets the external interface of the [Network]. +// This is only available to networks of [SharedMode]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_external_interface(_:_:)?language=objc +func (c *NetworkConfiguration) SetExternalInterface(ifname string) error { + cIfname := C.CString(ifname) + defer C.free(unsafe.Pointer(cIfname)) + + status := C.VmnetNetworkConfiguration_setExternalInterface( + objc.Ptr(c), + cIfname, + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to set external interface: %w", Return(status)) + } + return nil +} + +// SetIPv4Subnet configures the IPv4 address for the [Network]. +// Note that the first, second, and last addresses of the range are reserved. +// The second address is reserved for the host, the first and last are not assignable to any node. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_ipv4_subnet(_:_:_:)?language=objc +func (c *NetworkConfiguration) SetIPv4Subnet(subnet netip.Prefix) error { + if !subnet.Addr().Is4() { + return fmt.Errorf("subnet is not ipv4") + } + if !netip.MustParsePrefix("192.168.0.0/16").Overlaps(subnet) { + return fmt.Errorf("subnet %s is out of range", subnet.String()) + } + // Use the first assignable address as the subnet address to avoid + // Virtualization fails with error "Internal Virtualization error. Internal Network Error.". + ip := subnet.Masked().Addr().Next().As4() + mask := net.CIDRMask(subnet.Bits(), 32) + var cSubnet C.struct_in_addr + var cMask C.struct_in_addr + + copy((*[4]byte)(unsafe.Pointer(&cSubnet))[:], ip[:]) + copy((*[4]byte)(unsafe.Pointer(&cMask))[:], mask[:]) + + status := C.VmnetNetworkConfiguration_setIPv4Subnet( + objc.Ptr(c), + &cSubnet, + &cMask, + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to set ipv4 subnet: %d", Return(status)) + } + return nil +} + +// SetIPv6Prefix configures the IPv6 prefix for the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_ipv6_prefix(_:_:_:)?language=objc +func (c *NetworkConfiguration) SetIPv6Prefix(prefix netip.Prefix) error { + if !prefix.Addr().Is6() { + return fmt.Errorf("prefix is not ipv6") + } + ip := prefix.Addr().As16() + var cPrefix C.struct_in6_addr + + copy((*[16]byte)(unsafe.Pointer(&cPrefix))[:], ip[:]) + + status := C.VmnetNetworkConfiguration_setIPv6Prefix( + objc.Ptr(c), + &cPrefix, + C.uint8_t(prefix.Bits()), + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to set ipv6 prefix: %w", Return(status)) + } + return nil +} + +func netHardwareAddrToEtherAddr(hw net.HardwareAddr) (C.ether_addr_t, error) { + if len(hw) != 6 { + return C.ether_addr_t{}, fmt.Errorf("invalid MAC address length: %d", len(hw)) + } + var addr C.ether_addr_t + copy((*[6]byte)(unsafe.Pointer(&addr))[:], hw[:6]) + return addr, nil +} + +// SetMtu configures the maximum transmission unit (MTU) for the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_mtu(_:_:)?language=objc +func (c *NetworkConfiguration) SetMtu(mtu uint32) error { + status := C.VmnetNetworkConfiguration_setMtu( + objc.Ptr(c), + C.uint32_t(mtu), + ) + if !errors.Is(Return(status), ErrSuccess) { + return fmt.Errorf("failed to set mtu: %w", Return(status)) + } + return nil +} + +// MARK: - Network + +// Network represents a [vmnet_network_ref]. +// +// [vmnet_network_ref]: https://developer.apple.com/documentation/vmnet/vmnet_network_ref?language=objc +type Network struct { + *object +} + +// NewNetwork creates a new [Network] with [NetworkConfiguration]. +// This is only supported on macOS 26 and newer, error will be returned on older versions. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_create(_:_:)?language=objc +func NewNetwork(config *NetworkConfiguration) (*Network, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + var status Return + ptr := C.VmnetNetworkCreate( + objc.Ptr(config), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrSuccess) { + return nil, fmt.Errorf("failed to create VmnetNetwork: %w", status) + } + network := &Network{object: &object{objc.NewPointer(ptr)}} + ReleaseOnCleanup(network) + return network, nil +} + +// NewNetworkWithSerialization creates a new [Network] from a serialized representation. +// This is only supported on macOS 26 and newer, error will be returned on older versions. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_create_with_serialization(_:_:)?language=objc +func NewNetworkWithSerialization(serialization xpc.Object) (*Network, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + var status Return + ptr := C.VmnetNetworkCreateWithSerialization( + objc.Ptr(serialization), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrSuccess) { + return nil, fmt.Errorf("failed to create VmnetNetwork with serialization: %w", status) + } + network := &Network{object: &object{objc.NewPointer(ptr)}} + ReleaseOnCleanup(network) + return network, nil +} + +// NewNetworkFromPointer creates a new [Network] from an existing [objc.Pointer]. +func NewNetworkFromPointer(p *objc.Pointer) *Network { + return &Network{object: &object{p}} +} + +// CopySerialization returns a serialized copy of [Network] in [xpc_object_t] as [xpc.Object]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_copy_serialization(_:_:)?language=objc +// +// [xpc_object_t]: https://developer.apple.com/documentation/xpc/xpc_object_t?language=objc +func (n *Network) CopySerialization() (xpc.Object, error) { + var status Return + ptr := C.VmnetNetwork_copySerialization( + objc.Ptr(n), + (*C.uint32_t)(unsafe.Pointer(&status)), + ) + if !errors.Is(status, ErrSuccess) { + return nil, fmt.Errorf("failed to copy serialization: %w", status) + } + return xpc.NewObject(ptr), nil +} + +// IPv4Subnet returns the IPv4 subnet of the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_get_ipv4_subnet(_:_:_:)?language=objc +func (n *Network) IPv4Subnet() (subnet netip.Prefix, err error) { + var cSubnet C.struct_in_addr + var cMask C.struct_in_addr + + C.VmnetNetwork_getIPv4Subnet(objc.Ptr(n), &cSubnet, &cMask) + + sIP := inAddrToNetipAddr(cSubnet) + mIP := inAddrToIP(cMask) + + // netmask → prefix length + ones, bits := net.IPMask(mIP.To4()).Size() + if bits != 32 { + return netip.Prefix{}, fmt.Errorf("unexpected mask size") + } + + return netip.PrefixFrom(sIP, ones), nil +} + +func inAddrToNetipAddr(a C.struct_in_addr) netip.Addr { + p := (*[4]byte)(unsafe.Pointer(&a)) + return netip.AddrFrom4(*p) +} + +func inAddrToIP(a C.struct_in_addr) net.IP { + p := (*[4]byte)(unsafe.Pointer(&a)) + return net.IPv4(p[0], p[1], p[2], p[3]) +} + +// IPv6Prefix returns the IPv6 prefix of the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_get_ipv6_prefix(_:_:_:)?language=objc +func (n *Network) IPv6Prefix() (netip.Prefix, error) { + var prefix C.struct_in6_addr + var prefixLen C.uint8_t + + C.VmnetNetwork_getIPv6Prefix(objc.Ptr(n), &prefix, &prefixLen) + + addr := in6AddrToNetipAddr(prefix) + pfx := netip.PrefixFrom(addr, int(prefixLen)) + + if !pfx.IsValid() { + return netip.Prefix{}, fmt.Errorf("invalid ipv6 prefix") + } + return pfx, nil +} + +func in6AddrToNetipAddr(a C.struct_in6_addr) netip.Addr { + p := (*[16]byte)(unsafe.Pointer(&a)) + return netip.AddrFrom16(*p) +} + +// MARK: - Interface + +// Interface represents a [interface_ref] in vmnet. +// +// [interface_ref]: https://developer.apple.com/documentation/vmnet/interface_ref?language=objc +type Interface struct { + *object + Param *xpc.Dictionary + MaxPacketSize uint64 + MaxReadPacketCount int + MaxWritePacketCount int + packetsAvailableEventCallbackHandler *cgohandler.Handler +} + +// Keys for interface describing parameters dictionary. +var ( + // AllocateMacAddressKey represents [vmnet_allocate_mac_address_key]. + // - Can be used in the interface describing dictionary passed to [StartInterfaceWithNetwork] to request automatic MAC address allocation. + // - See for details. + // + // [vmnet_allocate_mac_address_key]: https://developer.apple.com/documentation/vmnet/vmnet_allocate_mac_address_key?language=objc + AllocateMacAddressKey = C.GoString(C.vmnet_allocate_mac_address_key) + + // EnableChecksumOffloadKey represents [vmnet_enable_checksum_offload_key]. + // - Can be used in the interface describing dictionary passed to [StartInterfaceWithNetwork] to enable checksum offloading. + // - See for details. + // + // [vmnet_enable_checksum_offload_key]: https://developer.apple.com/documentation/vmnet/vmnet_enable_checksum_offload_key?language=objc + EnableChecksumOffloadKey = C.GoString(C.vmnet_enable_checksum_offload_key) + + // EnableIsolationKey represents [vmnet_enable_isolation_key]. + // - Can be used in the interface describing dictionary passed to [StartInterfaceWithNetwork] to enable isolation. + // - See for details. + // + // [vmnet_enable_isolation_key]: https://developer.apple.com/documentation/vmnet/vmnet_enable_isolation_key?language=objc + EnableIsolationKey = C.GoString(C.vmnet_enable_isolation_key) + + // EnableTSOKey represents [vmnet_enable_tso_key]. + // - Can be used in the interface describing dictionary passed to [StartInterfaceWithNetwork] to enable TCP Segmentation Offloading (TSO). + // - See for details. + // + // [vmnet_enable_tso_key]: https://developer.apple.com/documentation/vmnet/vmnet_enable_tso_key?language=objc + EnableTSOKey = C.GoString(C.vmnet_enable_tso_key) +) + +// StartInterfaceWithNetwork starts an [Interface] with the given [Network] and interface description. +// - https://developer.apple.com/documentation/vmnet/vmnet_interface_start_with_network(_:_:_:_:)?language=objc +func StartInterfaceWithNetwork(network *Network, interfaceDesc *xpc.Dictionary) (*Interface, error) { + result := C.VmnetInterfaceStartWithNetwork(objc.Ptr(network), objc.Ptr(interfaceDesc)) + if vzvmnetResult := Return(result.vmnetReturn); vzvmnetResult != ErrSuccess { + return nil, fmt.Errorf("VmnetInterfaceStartWithNetwork failed: %w", vzvmnetResult) + } + i := &Interface{ + object: ReleaseOnCleanup(&object{objc.NewPointer(result.iface)}), + Param: xpc.ReleaseOnCleanup(xpc.NewObject(result.ifaceParam).(*xpc.Dictionary)), + MaxPacketSize: uint64(result.maxPacketSize), + MaxReadPacketCount: int(result.maxReadPacketCount), + MaxWritePacketCount: int(result.maxWritePacketCount), + } + return i, nil +} + +// PacketsAvailableEventCallback is a callback function type for packets available event. +// - https://developer.apple.com/documentation/vmnet/vmnet_interface_set_event_callback(_:_:_:_:)?language=objc +type PacketsAvailableEventCallback func(estimatedCount int) + +//export callPacketsAvailableEventCallback +func callPacketsAvailableEventCallback(cgoHandle uintptr, estimatedCount C.int) { + if cgoHandle != 0 { + callback := cgo.Handle(cgoHandle).Value().(PacketsAvailableEventCallback) + callback(int(estimatedCount)) + } +} + +// SetPacketsAvailableEventCallback sets the packets available event callback for the [Interface]. +// - https://developer.apple.com/documentation/vmnet/vmnet_interface_set_event_callback(_:_:_:_:)?language=objc +// - https://developer.apple.com/documentation/vmnet/vmnet_interface_event_callback_t?language=objc +// - https://developer.apple.com/documentation/vmnet/interface_event_t/vmnet_interface_packets_available?language=objc +// - https://developer.apple.com/documentation/vmnet/vmnet_estimated_packets_available_key?language=objc +func (i *Interface) SetPacketsAvailableEventCallback(callback PacketsAvailableEventCallback) error { + cgoHandle, p := cgohandler.New(callback) + if result := Return( + C.VmnetInterfaceSetPacketsAvailableEventCallback(objc.Ptr(i), C.uintptr_t(p)), + ); result != ErrSuccess { + return fmt.Errorf("VmnetInterfaceSetPacketsAvailableEventCallback failed: %w", result) + } + i.packetsAvailableEventCallbackHandler = cgoHandle + return nil +} + +// Stop stops the [Interface]. +// - https://developer.apple.com/documentation/vmnet/vmnet_stop_interface(_:_:_:)?language=objc +func (i *Interface) Stop() error { + result := Return(C.VmnetStopInterface(objc.Ptr(i))) + if result != ErrSuccess { + return fmt.Errorf("VmnetStopInterface failed: %w", result) + } + return nil +} + +// ReadPackets reads packets from the [Interface] into [VMPktDesc] array. +// It returns the number of packets read. +// - https://developer.apple.com/documentation/vmnet/vmnet_read(_:_:_:)?language=objc +func (i *Interface) ReadPackets(v *VMPktDesc, packetCount int) (int, error) { + // Limit packetCount to maxReadPacketCount + count := C.int(min(packetCount, i.MaxReadPacketCount)) + if result := Return(C.VmnetRead(objc.Ptr(i), (*C.struct_vmpktdesc)(v), &count)); result != ErrSuccess { + return 0, fmt.Errorf("VmnetRead failed: %w", result) + } + return int(count), nil +} + +// WritePackets writes packets to the [Interface] from [VMPktDesc] array. +// It returns the number of packets written. +// - https://developer.apple.com/documentation/vmnet/vmnet_write(_:_:_:)?language=objc +func (i *Interface) WritePackets(v *VMPktDesc, packetCount int) (int, error) { + count := C.int(min(packetCount, i.MaxWritePacketCount)) + if result := Return(C.VmnetWrite(objc.Ptr(i), (*C.struct_vmpktdesc)(v), &count)); result != ErrSuccess { + // Will partial write happen here? + return 0, fmt.Errorf("VmnetWrite failed: %w", result) + } + return int(count), nil +} diff --git a/vmnet/vmnet_darwin.h b/vmnet/vmnet_darwin.h new file mode 100644 index 00000000..68ee8870 --- /dev/null +++ b/vmnet/vmnet_darwin.h @@ -0,0 +1,59 @@ +#pragma once + +#import +#import +#import +// In older SDKs, vmnet.h does not include above headers, so we include them here. +#import "../internal/osversion/virtualization_helper.h" +#import + +// MARK: - CFRetain/Release Wrapper + +void vmnetRetain(void *obj); +void vmnetRelease(void *obj); + +// MARK: - vmnet_network_configuration_t (macOS 26+) + +uint32_t VmnetNetworkConfiguration_addDhcpReservation(void *config, ether_addr_t const *client, struct in_addr const *reservation); +uint32_t VmnetNetworkConfiguration_addPortForwardingRule(void *config, uint8_t protocol, sa_family_t address_family, uint16_t internal_port, uint16_t external_port, void const *internal_address); +void *VmnetNetworkConfigurationCreate(uint32_t mode, uint32_t *status); +void VmnetNetworkConfiguration_disableDhcp(void *config); +void VmnetNetworkConfiguration_disableDnsProxy(void *config); +void VmnetNetworkConfiguration_disableNat44(void *config); +void VmnetNetworkConfiguration_disableNat66(void *config); +void VmnetNetworkConfiguration_disableRouterAdvertisement(void *config); +uint32_t VmnetNetworkConfiguration_setExternalInterface(void *config, const char *ifname); +uint32_t VmnetNetworkConfiguration_setIPv4Subnet(void *config, struct in_addr const *subnet_addr, struct in_addr const *subnet_mask); +uint32_t VmnetNetworkConfiguration_setIPv6Prefix(void *config, struct in6_addr const *prefix, uint8_t prefix_len); +uint32_t VmnetNetworkConfiguration_setMtu(void *config, uint32_t mtu); + +// MARK: - vmnet_network_ref (macOS 26+) + +void *VmnetNetwork_copySerialization(void *network, uint32_t *status); +void *VmnetNetworkCreate(void *config, uint32_t *status); +void *VmnetNetworkCreateWithSerialization(void *serialization, uint32_t *status); +void VmnetNetwork_getIPv4Subnet(void *network, struct in_addr *subnet, struct in_addr *mask); +void VmnetNetwork_getIPv6Prefix(void *network, struct in6_addr *prefix, uint8_t *prefix_len); + +// MARK: - interface_ref (macOS 26+) + +uint32_t VmnetInterfaceSetPacketsAvailableEventCallback(void *interface, uintptr_t callback); +uint32_t VmnetStopInterface(void *interface); +uint32_t VmnetRead(void *interface, struct vmpktdesc *packets, int *pktcnt); +uint32_t VmnetWrite(void *interface, struct vmpktdesc *packets, int *pktcnt); + +struct vmnetInterfaceStartResult { + void *iface; // interface_ref + void *ifaceParam; // xpc_object_t + uint64_t maxPacketSize; + int maxReadPacketCount; + int maxWritePacketCount; + uint32_t vmnetReturn; +}; + +struct vmnetInterfaceStartResult VmnetInterfaceStartWithNetwork(void *network, void *interfaceDesc); + +// MARK: - vmpktdesc helper functions +struct vmpktdesc *allocateVMPktDescArray(int count, uint64_t maxPacketSize); +struct vmpktdesc *resetVMPktDescArray(struct vmpktdesc *pktDescs, int count, uint64_t maxPacketSize); +void deallocateVMPktDescArray(struct vmpktdesc *pktDescs); diff --git a/vmnet/vmnet_darwin.m b/vmnet/vmnet_darwin.m new file mode 100644 index 00000000..d9b69d64 --- /dev/null +++ b/vmnet/vmnet_darwin.m @@ -0,0 +1,337 @@ +#import "vmnet_darwin.h" + +// MARK: - CFRetain/Release Wrapper +void vmnetRetain(void *obj) +{ + if (obj != NULL) { + CFRetain((CFTypeRef)obj); + } +} + +void vmnetRelease(void *obj) +{ + if (obj != NULL) { + CFRelease((CFTypeRef)obj); + } +} + +// MARK: - vmnet_network_configuration_t (macOS 26+) + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_add_dhcp_reservation(_:_:_:)?language=objc +uint32_t VmnetNetworkConfiguration_addDhcpReservation(void *config, ether_addr_t const *client, struct in_addr const *reservation) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_add_dhcp_reservation((vmnet_network_configuration_ref)config, client, reservation); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_add_port_forwarding_rule(_:_:_:_:_:_:)?language=objc +uint32_t VmnetNetworkConfiguration_addPortForwardingRule(void *config, uint8_t protocol, sa_family_t address_family, uint16_t internal_port, uint16_t external_port, void const *internal_address) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_add_port_forwarding_rule((vmnet_network_configuration_ref)config, protocol, address_family, internal_port, external_port, internal_address); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_create(_:_:)?language=objc +void *VmnetNetworkConfigurationCreate(uint32_t mode, uint32_t *status) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_create(mode, status); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_dhcp(_:)?language=objc +void VmnetNetworkConfiguration_disableDhcp(void *config) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_configuration_disable_dhcp((vmnet_network_configuration_ref)config); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_dns_proxy(_:)?language=objc +void VmnetNetworkConfiguration_disableDnsProxy(void *config) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_configuration_disable_dns_proxy((vmnet_network_configuration_ref)config); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_nat44(_:)?language=objc +void VmnetNetworkConfiguration_disableNat44(void *config) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_configuration_disable_nat44((vmnet_network_configuration_ref)config); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_nat66(_:)?language=objc +void VmnetNetworkConfiguration_disableNat66(void *config) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_configuration_disable_nat66((vmnet_network_configuration_ref)config); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_disable_router_advertisement(_:)?language=objc +void VmnetNetworkConfiguration_disableRouterAdvertisement(void *config) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_configuration_disable_router_advertisement((vmnet_network_configuration_ref)config); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_external_interface(_:_:)?language=objc +uint32_t VmnetNetworkConfiguration_setExternalInterface(void *config, const char *ifname) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_set_external_interface((vmnet_network_configuration_ref)config, ifname); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_ipv4_subnet(_:_:_:)?language=objc +uint32_t VmnetNetworkConfiguration_setIPv4Subnet(void *config, struct in_addr const *subnet_addr, struct in_addr const *subnet_mask) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_set_ipv4_subnet((vmnet_network_configuration_ref)config, subnet_addr, subnet_mask); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_ipv6_prefix(_:_:_:)?language=objc +uint32_t VmnetNetworkConfiguration_setIPv6Prefix(void *config, struct in6_addr const *prefix, uint8_t prefix_len) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_set_ipv6_prefix((vmnet_network_configuration_ref)config, prefix, prefix_len); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_set_mtu(_:_:)?language=objc +uint32_t VmnetNetworkConfiguration_setMtu(void *config, uint32_t mtu) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_configuration_set_mtu((vmnet_network_configuration_ref)config, mtu); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// MARK: - vmnet_network_ref (macOS 26+) + +// https://developer.apple.com/documentation/vmnet/vmnet_network_copy_serialization(_:_:)?language=objc +void *VmnetNetwork_copySerialization(void *network, uint32_t *status) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_copy_serialization((vmnet_network_ref)network, status); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// vmnet_network +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_create(_:_:)?language=objc +void *VmnetNetworkCreate(void *config, uint32_t *status) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_create((vmnet_network_configuration_ref)config, status); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_create_with_serialization(_:_:)?language=objc +void *VmnetNetworkCreateWithSerialization(void *serialization, uint32_t *status) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_network_create_with_serialization((xpc_object_t)serialization, status); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_get_ipv4_subnet(_:_:_:)?language=objc +void VmnetNetwork_getIPv4Subnet(void *network, struct in_addr *subnet, struct in_addr *mask) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_get_ipv4_subnet((vmnet_network_ref)network, subnet, mask); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// see: https://developer.apple.com/documentation/vmnet/vmnet_network_get_ipv6_prefix(_:_:_:)?language=objc +void VmnetNetwork_getIPv6Prefix(void *network, struct in6_addr *prefix, uint8_t *prefix_len) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + vmnet_network_get_ipv6_prefix((vmnet_network_ref)network, prefix, prefix_len); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// MARK: - interface_ref (macOS 26+) + +extern void callPacketsAvailableEventCallback(uintptr_t cgoHandle, int estimatedCount); + +uint32_t VmnetInterfaceSetPacketsAvailableEventCallback(void *iface, uintptr_t callback) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + dispatch_queue_t queue = dispatch_queue_create("vmnet.interface.eventcallback", DISPATCH_QUEUE_SERIAL); + vmnet_return_t result = vmnet_interface_set_event_callback((interface_ref)iface, VMNET_INTERFACE_PACKETS_AVAILABLE, queue, ^(interface_event_t eventMask, xpc_object_t event) { + if ((eventMask & VMNET_INTERFACE_PACKETS_AVAILABLE) != 0) { + int estimated = (int)xpc_dictionary_get_uint64(event, vmnet_estimated_packets_available_key); + callPacketsAvailableEventCallback(callback, estimated); + } + }); + dispatch_release(queue); + return result; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +uint32_t VmnetStopInterface(void *interface) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + dispatch_queue_t queue = dispatch_queue_create("vmnet.interface.stop", DISPATCH_QUEUE_SERIAL); + __block vmnet_return_t status; + vmnet_return_t scheduleStatus = vmnet_stop_interface((interface_ref)interface, queue, ^(vmnet_return_t stopStatus) { + status = stopStatus; + dispatch_semaphore_signal(sem); + }); + dispatch_release(queue); + if (scheduleStatus != VMNET_SUCCESS) { + dispatch_release(sem); + return scheduleStatus; + } + dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); + dispatch_release(sem); + return status; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +uint32_t VmnetRead(void *interface, struct vmpktdesc *packets, int *pktcnt) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_read((interface_ref)interface, packets, pktcnt); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +uint32_t VmnetWrite(void *interface, struct vmpktdesc *packets, int *pktcnt) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return vmnet_write((interface_ref)interface, packets, pktcnt); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +struct vmnetInterfaceStartResult VmnetInterfaceStartWithNetwork(void *network, void *interfaceDesc) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + dispatch_queue_t queue = dispatch_queue_create("vmnet.interface.start", DISPATCH_QUEUE_SERIAL); + __block struct vmnetInterfaceStartResult result; + vmnet_start_interface_completion_handler_t handler = ^(vmnet_return_t vmnetReturn, xpc_object_t ifaceParam) { + result.ifaceParam = xpc_retain(ifaceParam); + result.maxPacketSize = xpc_dictionary_get_uint64(ifaceParam, vmnet_max_packet_size_key); + result.maxReadPacketCount = xpc_dictionary_get_uint64(ifaceParam, vmnet_read_max_packets_key); + result.maxWritePacketCount = xpc_dictionary_get_uint64(ifaceParam, vmnet_write_max_packets_key); + result.vmnetReturn = vmnetReturn; + dispatch_semaphore_signal(sem); + }; + interface_ref iface = vmnet_interface_start_with_network((vmnet_network_ref)network, (xpc_object_t)interfaceDesc, queue, handler); + result.iface = iface; + dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); + dispatch_release(queue); + dispatch_release(sem); + return result; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// MARK: - vmpktdesc helper functions + +struct vmpktdesc *allocateVMPktDescArray(int count, uint64_t maxPacketSize) +{ + // Calculate total size needed for pktdesc array and iovec array + size_t totalSize = (sizeof(struct vmpktdesc) + sizeof(struct iovec)) * count; + struct vmpktdesc *pktDescs = (struct vmpktdesc *)malloc(totalSize); + return resetVMPktDescArray(pktDescs, count, maxPacketSize); +} + +struct vmpktdesc *resetVMPktDescArray(struct vmpktdesc *pktDescs, int count, uint64_t maxPacketSize) +{ + struct iovec *iovecArray = (struct iovec *)(pktDescs + count); + for (int i = 0; i < count; i++) { + pktDescs[i].vm_pkt_size = maxPacketSize; + pktDescs[i].vm_pkt_iov = &iovecArray[i]; + pktDescs[i].vm_pkt_iovcnt = 1; + pktDescs[i].vm_flags = 0; + iovecArray[i].iov_len = maxPacketSize; + } + return pktDescs; +} + +void deallocateVMPktDescArray(struct vmpktdesc *pktDescs) +{ + if (pktDescs != NULL) { + free(pktDescs); + } +} diff --git a/vmnet_test.go b/vmnet_test.go new file mode 100644 index 00000000..a65d10aa --- /dev/null +++ b/vmnet_test.go @@ -0,0 +1,460 @@ +package vz_test + +import ( + "bytes" + "context" + _ "embed" + "encoding/hex" + "flag" + "fmt" + "log" + "net" + "net/netip" + "os" + "os/exec" + "os/signal" + "path" + "runtime" + "slices" + "strings" + "syscall" + "testing" + "text/template" + + "github.com/Code-Hex/vz/v3" + "github.com/Code-Hex/vz/v3/vmnet" + "github.com/Code-Hex/vz/v3/xpc" +) + +// TestVmnetSharedModeAllowsCommunicationBetweenMultipleVMs tests VmnetNetwork in SharedMode +// allows communication between multiple VMs connected to the same VmnetNetwork instance. +// This test creates two VmnetNetwork instances by serializing and deserializing the first instance, +// then boots a VM using each VmnetNetwork instance and tests communication between the two VMs. +func TestVmnetSharedModeAllowsCommunicationBetweenMultipleVMs(t *testing.T) { + if vz.Available(26.0) { + t.Skip("VmnetSharedMode is supported from macOS 26") + } + + // Create VmnetNetwork instance from configuration + config, err := vmnet.NewNetworkConfiguration(vmnet.SharedMode) + if err != nil { + t.Fatal(err) + } + network1, err := vmnet.NewNetwork(config) + if err != nil { + t.Fatal(err) + } + macaddress1 := randomMACAddress(t) + + // Create another VmnetNetwork instance from serialization of the first one + serialization, err := network1.CopySerialization() + if err != nil { + t.Fatal(err) + } + network2, err := vmnet.NewNetworkWithSerialization(serialization) + if err != nil { + t.Fatal(err) + } + macaddress2 := randomMACAddress(t) + + container1 := newVirtualizationMachine(t, configureNetworkDevice(network1, macaddress1)) + container2 := newVirtualizationMachine(t, configureNetworkDevice(network2, macaddress2)) + t.Cleanup(func() { + if err := container1.Shutdown(); err != nil { + log.Println(err) + } + if err := container2.Shutdown(); err != nil { + log.Println(err) + } + }) + + // Log network information + ipv4Subnet, err := network1.IPv4Subnet() + if err != nil { + t.Fatal(err) + } + t.Logf("Vmnet network IPv4 subnet: %s", ipv4Subnet.String()) + prefix, err := network1.IPv6Prefix() + if err != nil { + t.Fatal(err) + } + t.Logf("Vmnet network IPv6 prefix: %s", prefix.String()) + + // Detect IP addresses and test communication between VMs + container1IPv4 := container1.DetectIPv4(t, "eth0") + t.Logf("Container 1 IPv4: %s", container1IPv4) + container2IPv4 := container2.DetectIPv4(t, "eth0") + t.Logf("Container 2 IPv4: %s", container2IPv4) + container1.exec(t, "ping "+container2IPv4) + container2.exec(t, "ping "+container1IPv4) +} + +// TestVmnetSharedModeWithConfiguringIPv4 tests VmnetNetwork in SharedMode +// with custom IPv4 subnet and DHCP reservation. +// This test creates a VmnetNetwork instance with a specified IPv4 subnet and DHCP reservation, +// then boots a VM using the VmnetNetwork and verifies the VM receives the expected IP address. +func TestVmnetSharedModeWithConfiguringIPv4(t *testing.T) { + if vz.Available(26.0) { + t.Skip("VmnetSharedMode is supported from macOS 26") + } + // Create VmnetNetwork instance from configuration + config, err := vmnet.NewNetworkConfiguration(vmnet.SharedMode) + if err != nil { + t.Fatal(err) + } + // Configure IPv4 subnet + ipv4Subnet := detectFreeIPv4Subnet(t, netip.MustParsePrefix("192.168.5.0/24")) + if err := config.SetIPv4Subnet(ipv4Subnet); err != nil { + t.Fatal(err) + } + // Configure DHCP reservation + macaddress := randomMACAddress(t) + ipv4 := netip.MustParseAddr("192.168.5.15") + if err := config.AddDhcpReservation(macaddress.HardwareAddr(), ipv4); err != nil { + t.Fatal(err) + } + + // Create VmnetNetwork instance + network, err := vmnet.NewNetwork(config) + if err != nil { + t.Fatal(err) + } + + // Create VirtualizationMachine instance + container := newVirtualizationMachine(t, configureNetworkDevice(network, macaddress)) + t.Cleanup(func() { + if err := container.Shutdown(); err != nil { + log.Println(err) + } + }) + + // Log network information + ipv4SubnetConfigured, err := network.IPv4Subnet() + if err != nil { + t.Fatal(err) + } + t.Logf("Vmnet network IPv4 subnet: %s", ipv4SubnetConfigured.String()) + + // Verify the configured subnet + // Compare with masked value to ignore host bits since Vmnet prefers to use first address as network address. + if ipv4Subnet != ipv4SubnetConfigured.Masked() { + t.Fatalf("expected IPv4 subnet %s, but got %s", ipv4Subnet.String(), ipv4SubnetConfigured.Masked().String()) + } + + // Log IPv6 prefix + prefix, err := network.IPv6Prefix() + if err != nil { + t.Fatal(err) + } + t.Logf("Vmnet network IPv6 prefix: %s", prefix.String()) + + // Detect IP address and verify DHCP reservation + containerIPv4 := container.DetectIPv4(t, "eth0") + t.Logf("Container IPv4: %s", containerIPv4) + if ipv4.String() != containerIPv4 { + t.Fatalf("expected IPv4 %s, but got %s", ipv4, containerIPv4) + } +} + +var server bool + +func init() { + // Determine if running as server or client based on command-line arguments + flag.BoolVar(&server, "server", false, "run as mach service server") +} + +// TestVmnetNetworkShareModeSharingOverXpc tests sharing VmnetNetwork in SharedMode over XPC communication. +// This test registers test executable as an Mach service and launches it using launchctl. +// The launched Mach service provides VmnetNetwork serialization to clients upon request, after booting +// a VM using the provided VmnetNetwork to ensure the network is functional on the server side. +// The client boots VM using the provided VmnetNetwork serialization. +// +// This test uses pkg/xpc package to implement XPC communication. +func TestVmnetNetworkShareModeSharingOverXpc(t *testing.T) { + if vz.Available(26.0) { + t.Skip("VmnetSharedMode is supported from macOS 26") + } + + label := "dev.code-hex.vz.test.vmnetsharedmode" + machServiceName := label + ".subnet" + + if server { + t.Log("running as mach service server") + listener, err := xpcServerProvidingSubnet(t, machServiceName) + if err != nil { + log.Printf("failed to create mach service server: %v", err) + t.Fatal(err) + } + if err := listener.Activate(); err != nil { + log.Printf("failed to activate mach service server: %v", err) + t.Fatal(err) + } + ctx, stop := signal.NotifyContext(t.Context(), os.Interrupt, syscall.SIGTERM) + defer stop() + <-ctx.Done() + if err := listener.Close(); err != nil { + log.Printf("failed to close mach service server: %v", err) + } + } else { + t.Log("running as mach service client") + xpcRegisterMachService(t, label, machServiceName) + ipv4Subnet := detectFreeIPv4Subnet(t, netip.MustParsePrefix("192.168.6.0/24")) + network, err := xpcClientRequestingSubnet(t, machServiceName, ipv4Subnet) + if err != nil { + t.Fatal(err) + } + container := newVirtualizationMachine(t, configureNetworkDevice(network, randomMACAddress(t))) + t.Cleanup(func() { + if err := container.Shutdown(); err != nil { + log.Println(err) + } + }) + containerIPv4 := container.DetectIPv4(t, "eth0") + t.Logf("Container IPv4: %s", containerIPv4) + if !ipv4Subnet.Contains(netip.MustParseAddr(containerIPv4)) { + t.Fatalf("expected container IPv4 %s to be within subnet %s", containerIPv4, ipv4Subnet) + } + } +} + +// configureNetworkDevice returns a function that configures a network device +// with the given VmnetNetwork and MAC address. +func configureNetworkDevice(network *vmnet.Network, macAddress *vz.MACAddress) func(cfg *vz.VirtualMachineConfiguration) error { + return func(cfg *vz.VirtualMachineConfiguration) error { + var configurations []*vz.VirtioNetworkDeviceConfiguration + attachment, err := vz.NewVmnetNetworkDeviceAttachment(network) + if err != nil { + return err + } + config, err := vz.NewVirtioNetworkDeviceConfiguration(attachment) + if err != nil { + return err + } + config.SetMACAddress(macAddress) + configurations = append(configurations, config) + cfg.SetNetworkDevicesVirtualMachineConfiguration(configurations) + return nil + } +} + +// detectFreeIPv4Subnet detects a free IPv4 subnet on the host machine. +func detectFreeIPv4Subnet(t *testing.T, prefer netip.Prefix) netip.Prefix { + targetPrefix := netip.MustParsePrefix("192.168.0.0/16") + hostNetIfs, err := net.Interfaces() + if err != nil { + t.Fatal(err) + } + candidates := make([]netip.Prefix, len(hostNetIfs)) + for _, hostNetIf := range hostNetIfs { + hostNetAddrs, err := hostNetIf.Addrs() + if err != nil { + continue + } + for _, hostNetAddr := range hostNetAddrs { + netIPNet, ok := hostNetAddr.(*net.IPNet) + if !ok { + continue + } + hostPrefix := netip.MustParsePrefix(netIPNet.String()) + if targetPrefix.Overlaps(hostPrefix) { + candidates = append(candidates, hostPrefix) + } + } + } + slices.SortFunc(candidates, func(l, r netip.Prefix) int { + if l.Addr().Less(r.Addr()) { + return -1 + } + return 1 + }) + for _, candidate := range candidates { + if prefer.Addr() != candidate.Addr() { + return prefer + } + } + t.Fatal("no free IPv4 subnet found") + return netip.Prefix{} +} + +// funcName returns the name of the calling function. +// It is used to get the test function name for launchctl registration. +func funcName(t *testing.T, skip int) string { + pc, _, _, ok := runtime.Caller(skip) + if !ok { + t.Fatal("failed to get caller info") + } + funcNameComponents := strings.Split(runtime.FuncForPC(pc).Name(), ".") + return funcNameComponents[len(funcNameComponents)-1] +} + +// randomMACAddress generates a random locally administered MAC address. +func randomMACAddress(t *testing.T) *vz.MACAddress { + mac, err := vz.NewRandomLocallyAdministeredMACAddress() + if err != nil { + t.Fatal(err) + } + return mac +} + +// xpcClientRequestingSubnet requests a VmnetNetwork serialization for the given subnet +// from the Mach service and returns the deserialized VmnetNetwork instance. +func xpcClientRequestingSubnet(t *testing.T, machServiceName string, subnet netip.Prefix) (*vmnet.Network, error) { + session, err := xpc.NewSession(machServiceName) + if err != nil { + return nil, err + } + defer session.Cancel() + + resp, err := session.SendDictionaryWithReply( + t.Context(), + xpc.KeyValue("Subnet", xpc.NewString(subnet.String())), + ) + if err != nil { + return nil, err + } + errorStr := resp.GetString("Error") + if errorStr != "" { + return nil, fmt.Errorf("xpc service error: %s", errorStr) + } + serialization := resp.GetValue("Serialization") + log.Printf("%v", serialization) + if serializationDic, ok := serialization.(*xpc.Dictionary); ok { + serializationData := serializationDic.GetData("networkSerialization") + fmt.Printf("serialization data: %q\n", hex.EncodeToString(serializationData)) + } + return vmnet.NewNetworkWithSerialization(serialization) +} + +const launchdPlistTemplate = ` + + + + Label + {{.Label}} + ProgramArguments + + {{- range $arg := .ProgramArguments}} + {{$arg}} + {{- end}} + + RunAtLoad + + WorkingDirectory + {{ .WorkingDirectory }} + StandardErrorPath + {{ .WorkingDirectory }}/vmnet_test.xpc_test.stderr.log + + MachServices + + {{- range $service := .MachServices}} + {{$service}} + + {{- end}} + + +` + +// xpcRegisterMachService registers the test executable as a Mach service +// using launchctl with the given label and machServiceName. +// The launched Mach service stderr output will be redirected to the ./vmnet_test.xpc_test.stderr.log file. +func xpcRegisterMachService(t *testing.T, label, machServiceName string) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + params := struct { + Label string + ProgramArguments []string + WorkingDirectory string + MachServices []string + }{ + Label: label, + ProgramArguments: []string{os.Args[0], "-test.run", "^" + funcName(t, 2) + "$", "-server"}, + WorkingDirectory: cwd, + MachServices: []string{machServiceName}, + } + template, err := template.New("plist").Parse(launchdPlistTemplate) + if err != nil { + t.Fatal(err) + } + var b bytes.Buffer + if err := template.Execute(&b, params); err != nil { + t.Fatal(err) + } + userHomeDir, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + launchAgentDir := path.Join(userHomeDir, "Library", "LaunchAgents", label+".plist") + if err := os.WriteFile(launchAgentDir, b.Bytes(), 0o644); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.Remove(launchAgentDir); err != nil { + t.Logf("failed to remove launch agent plist: %v", err) + } + }) + cmd := exec.CommandContext(t.Context(), "launchctl", "load", launchAgentDir) + if err := cmd.Run(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + // do not use t.Context() here to ensure unload runs + cmd := exec.CommandContext(context.Background(), "launchctl", "unload", launchAgentDir) + if err := cmd.Run(); err != nil { + t.Logf("failed to unload launch agent: %v", err) + } + }) +} + +// xpcServerProvidingSubnet creates an Mach service XPC listener +// that provides VmnetNetwork serialization for requested subnet. +// It also boots a VM using the provided VmnetNetwork to ensure the network is functional on the server side. +func xpcServerProvidingSubnet(t *testing.T, machServiceName string) (*xpc.Listener, error) { + return xpc.NewListener( + machServiceName, + xpc.Accept( + xpc.MessageHandler(func(dic *xpc.Dictionary) *xpc.Dictionary { + createErrorReply := func(errMsg string, args ...any) *xpc.Dictionary { + errorString := fmt.Sprintf(errMsg, args...) + log.Print(errorString) + t.Log(errorString) + return dic.CreateReply( + xpc.KeyValue("Error", xpc.NewString(errorString)), + ) + } + var reply *xpc.Dictionary + if subnet := dic.GetString("Subnet"); subnet == "" { + reply = createErrorReply("missing Subnet in request") + } else if config, err := vmnet.NewNetworkConfiguration(vmnet.SharedMode); err != nil { + reply = createErrorReply("failed to create vmnet network configuration: %v", err) + } else if err := config.SetIPv4Subnet(netip.MustParsePrefix(subnet)); err != nil { + reply = createErrorReply("failed to set ipv4 subnet: %v", err) + } else if network, err := vmnet.NewNetwork(config); err != nil { + reply = createErrorReply("failed to create vmnet network: %v", err) + } else if serialization, err := network.CopySerialization(); err != nil { + reply = createErrorReply("failed to copy serialization: %v", err) + } else { + container := newVirtualizationMachine(t, configureNetworkDevice(network, randomMACAddress(t))) + t.Cleanup(func() { + if err := container.Shutdown(); err != nil { + log.Println(err) + } + }) + containerIPv4 := container.DetectIPv4(t, "eth0") + log.Printf("Container IPv4: %s", containerIPv4) + t.Logf("Container IPv4: %s", containerIPv4) + if netip.MustParsePrefix(subnet).Contains(netip.MustParseAddr(containerIPv4)) { + reply = dic.CreateReply( + xpc.KeyValue("Serialization", serialization), + ) + } else { + reply = createErrorReply("allocated container IPv4 %s is not within requested subnet %s", containerIPv4, subnet) + } + } + return reply + }), + ), + ) +} diff --git a/xpc/array.go b/xpc/array.go new file mode 100644 index 00000000..821c0a66 --- /dev/null +++ b/xpc/array.go @@ -0,0 +1,262 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc_darwin.h" +*/ +import "C" +import ( + "iter" + "runtime/cgo" + "time" + "unsafe" + + "github.com/Code-Hex/vz/v3/internal/cgohandler" + "github.com/Code-Hex/vz/v3/internal/objc" +) + +// Array represents an XPC array([XPC_TYPE_ARRAY]) object. [TypeArray] +// +// [XPC_TYPE_ARRAY]: https://developer.apple.com/documentation/xpc/xpc_type_array-c.macro?language=objc +type Array struct { + *xpcObject +} + +var _ Object = &Array{} + +// MARK: - Constructor + +// NewArray creates a new [Array] from the given [Object]s. +// - https://developer.apple.com/documentation/xpc/xpc_array_create(_:_:)?language=objc +func NewArray(objects ...Object) *Array { + cObjects := make([]unsafe.Pointer, len(objects)) + for i, obj := range objects { + cObjects[i] = objc.Ptr(obj) + } + return ReleaseOnCleanup(&Array{newXpcObject(C.xpcArrayCreate( + (*unsafe.Pointer)(unsafe.Pointer(&cObjects[0])), + C.size_t(len(cObjects)), + ))}) +} + +// MARK: - Value Accessors + +// GetValue retrieves the [Object] at the given index in the [Array]. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_value(_:_:)?language=objc +func (a *Array) GetValue(index int) Object { + ptr := C.xpcArrayGetValue(objc.Ptr(a), C.size_t(index)) + return NewObject(ptr) +} + +// SetValue sets the [Object] at the given index in the [Array]. +// - https://developer.apple.com/documentation/xpc/xpc_array_set_value(_:_:_:)?language=objc +func (a *Array) SetValue(index int, value Object) { + C.xpcArraySetValue(objc.Ptr(a), C.size_t(index), objc.Ptr(value)) +} + +// AppendValue appends the given [Object] to the end of the [Array]. +// - https://developer.apple.com/documentation/xpc/xpc_array_append_value(_:_:)?language=objc +func (a *Array) AppendValue(value Object) { + C.xpcArrayAppendValue(objc.Ptr(a), objc.Ptr(value)) +} + +// MARK: - Iteration + +// Count returns the number of elements in the [Array]. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_count(_:)?language=objc +func (a *Array) Count() int { + return int(C.xpcArrayGetCount(objc.Ptr(a))) +} + +// ArrayApplier is a function type for applying to each element in the Array. +type ArrayApplier func(uint64, Object) bool + +// callArrayApplier is called from C to apply a function to each element in the Array. +// +//export callArrayApplier +func callArrayApplier(cgoApplier uintptr, index C.size_t, cgoValue uintptr) C.bool { + applier := cgohandler.Unwrap[ArrayApplier](cgoApplier) + value := unwrapObject[Object](cgoValue) + result := applier(uint64(index), value) + return C.bool(result) +} + +// All iterates over all elements in the [Array]. +// - https://developer.apple.com/documentation/xpc/xpc_array_apply(_:_:)?language=objc +func (a *Array) All() iter.Seq2[uint64, Object] { + return func(yieald func(uint64, Object) bool) { + cgoApplier := cgo.NewHandle(ArrayApplier(yieald)) + defer cgoApplier.Delete() + _ = C.xpcArrayApply( + objc.Ptr(a), + C.uintptr_t(cgoApplier), + ) + } +} + +// Values iterates over all values in the [Array] using [Array.All]. +func (a *Array) Values() iter.Seq[Object] { + return func(yieald func(Object) bool) { + for _, value := range a.All() { + if !yieald(value) { + return + } + } + } +} + +// MARK: - Typed Getters + +// DupFd retrieves duplicated file descriptors from the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_dup_fd(_:_:)?language=objc +func (a *Array) DupFd(index int) uintptr { + return uintptr(C.xpcArrayDupFd(objc.Ptr(a), C.size_t(index))) +} + +// GetArray retrieves an [Array] value from the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_array(_:_:)?language=objc +func (a *Array) GetArray(index int) *Array { + ptr := C.xpcArrayGetArray(objc.Ptr(a), C.size_t(index)) + return &Array{newXpcObject(ptr)} +} + +// GetBool retrieves a boolean value from the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_bool(_:_:)?language=objc +func (a *Array) GetBool(index int) bool { + return bool(C.xpcArrayGetBool(objc.Ptr(a), C.size_t(index))) +} + +// GetData retrieves a byte slice from the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_data(_:_:_:)?language=objc +func (a *Array) GetData(index int) []byte { + var length C.size_t + dataPtr := C.xpcArrayGetData(objc.Ptr(a), C.size_t(index), &length) + if dataPtr == nil || length == 0 { + return nil + } + return C.GoBytes(unsafe.Pointer(dataPtr), C.int(length)) +} + +// GetDate retrieves a date value from the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_date(_:_:)?language=objc +func (a *Array) GetDate(index int) time.Time { + unixNano := C.xpcArrayGetDate(objc.Ptr(a), C.size_t(index)) + return time.Unix(0, int64(unixNano)) +} + +// GetDictionary retrieves a [Dictionary] value from the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_dictionary(_:_:)?language=objc +func (a *Array) GetDictionary(index int) *Dictionary { + ptr := C.xpcArrayGetDictionary(objc.Ptr(a), C.size_t(index)) + return &Dictionary{newXpcObject(ptr)} +} + +// GetDouble retrieves a double value from the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_double(_:_:)?language=objc +func (a *Array) GetDouble(index int) float64 { + return float64(C.xpcArrayGetDouble(objc.Ptr(a), C.size_t(index))) +} + +// GetInt64 retrieves an int64 value from the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_int64(_:_:)?language=objc +func (a *Array) GetInt64(index int) int64 { + return int64(C.xpcArrayGetInt64(objc.Ptr(a), C.size_t(index))) +} + +// GetString retrieves a string value from the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_string(_:_:)?language=objc +func (a *Array) GetString(index int) string { + cstr := C.xpcArrayGetString(objc.Ptr(a), C.size_t(index)) + return C.GoString(cstr) +} + +// GetUInt64 retrieves a uint64 value from the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_uint64(_:_:)?language=objc +func (a *Array) GetUInt64(index int) uint64 { + return uint64(C.xpcArrayGetUInt64(objc.Ptr(a), C.size_t(index))) +} + +// GetUUID retrieves a UUID value from the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_get_uuid(_:_:)?language=objc +func (a *Array) GetUUID(index int) [16]byte { + var uuid [16]byte + ptr := C.xpcArrayGetUUID(objc.Ptr(a), C.size_t(index)) + if ptr == nil { + return uuid + } + copy(uuid[:], unsafe.Slice((*byte)(ptr), 16)) + return uuid +} + +// MARK: - Typed Setters + +// SetBool sets a boolean value in the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_set_bool(_:_:_:)?language=objc +func (a *Array) SetBool(index int, value bool) { + cvalue := C.bool(value) + C.xpcArraySetBool(objc.Ptr(a), C.size_t(index), cvalue) +} + +// SetData sets a byte slice in the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_set_data(_:_:_:_:)?language=objc +func (a *Array) SetData(index int, value []byte) { + var ptr unsafe.Pointer + var length C.size_t + if len(value) > 0 { + ptr = unsafe.Pointer(&value[0]) + length = C.size_t(len(value)) + } + C.xpcArraySetData(objc.Ptr(a), C.size_t(index), ptr, length) +} + +// SetDate sets a date value in the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_set_date(_:_:_:)?language=objc +func (a *Array) SetDate(index int, value time.Time) { + unixNano := C.int64_t(value.UnixNano()) + C.xpcArraySetDate(objc.Ptr(a), C.size_t(index), unixNano) +} + +// SetDouble sets a double value in the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_set_double(_:_:_:)?language=objc +func (a *Array) SetDouble(index int, value float64) { + cvalue := C.double(value) + C.xpcArraySetDouble(objc.Ptr(a), C.size_t(index), cvalue) +} + +// SetFd sets a file descriptor in the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_set_fd(_:_:_:)?language=objc +func (a *Array) SetFd(index int, fd uintptr) { + cfd := C.int(fd) + C.xpcArraySetFd(objc.Ptr(a), C.size_t(index), cfd) +} + +// SetInt64 sets an int64 value in the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_set_int64(_:_:_:)?language=objc +func (a *Array) SetInt64(index int, value int64) { + cvalue := C.int64_t(value) + C.xpcArraySetInt64(objc.Ptr(a), C.size_t(index), cvalue) +} + +// SetString sets a string value in the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_set_string(_:_:_:)?language=objc +func (a *Array) SetString(index int, value string) { + cstr := C.CString(value) + defer C.free(unsafe.Pointer(cstr)) + C.xpcArraySetString(objc.Ptr(a), C.size_t(index), cstr) +} + +// SetUInt64 sets a uint64 value in the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_set_uint64(_:_:_:)?language=objc +func (a *Array) SetUInt64(index int, value uint64) { + cvalue := C.uint64_t(value) + C.xpcArraySetUInt64(objc.Ptr(a), C.size_t(index), cvalue) +} + +// SetUUID sets a UUID value in the [Array] at the given index. +// - https://developer.apple.com/documentation/xpc/xpc_array_set_uuid(_:_:_:)?language=objc +func (a *Array) SetUUID(index int, value [16]byte) { + C.xpcArraySetUUID(objc.Ptr(a), C.size_t(index), (*C.uint8_t)(unsafe.Pointer(&value[0]))) +} + +// ArrayApppendIndex is a constant to append an element to the end of the [Array]. +const ArrayApppendIndex = C.XPC_ARRAY_APPEND diff --git a/xpc/dictionary.go b/xpc/dictionary.go new file mode 100644 index 00000000..1f2f8ce8 --- /dev/null +++ b/xpc/dictionary.go @@ -0,0 +1,369 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc_darwin.h" +*/ +import "C" +import ( + "fmt" + "iter" + "runtime/cgo" + "time" + "unsafe" + + "github.com/Code-Hex/vz/v3/internal/cgohandler" + "github.com/Code-Hex/vz/v3/internal/objc" +) + +// Dictionary represents an XPC dictionary ([XPC_TYPE_DICTIONARY]) object. [TypeDictionary] +// +// [XPC_TYPE_DICTIONARY]: https://developer.apple.com/documentation/xpc/xpc_type_dictionary-c.macro?language=objc +type Dictionary struct { + *xpcObject +} + +var _ Object = &Dictionary{} + +// MARK: - Constructor + +// NewDictionary creates a new empty [Dictionary] object and applies the given entries. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_create_empty()?language=objc +// +// The entries can be created using [DictionaryEntry] functions such as [KeyValue]. +func NewDictionary(entries ...DictionaryEntry) *Dictionary { + d := ReleaseOnCleanup(&Dictionary{newXpcObject(C.xpcDictionaryCreateEmpty())}) + for _, e := range entries { + e(d) + } + return d +} + +// DictionaryEntry defines a function type for customizing [NewDictionary] or [Dictionary.CreateReply]. +type DictionaryEntry func(*Dictionary) + +// KeyValue sets a [Object] value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_value(_:_:_:)?language=objc +func KeyValue(key string, val Object) DictionaryEntry { + return func(o *Dictionary) { + o.SetValue(key, val) + } +} + +// MARK: - CreateReply + +// DictionaryCreateReply creates a new reply [Dictionary] based on the current [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_create_reply(_:)?language=objc +// +// The entries can be created using [DictionaryEntry] functions such as [KeyValue]. +func (o *Dictionary) CreateReply(entries ...DictionaryEntry) *Dictionary { + // Do not use ReleaseOnCleanup here because the reply dictionary will be released in C after sending. + d := &Dictionary{newXpcObject(C.xpcDictionaryCreateReply(objc.Ptr(o)))} + for _, entry := range entries { + entry(d) + } + return d +} + +// MARK: - Value Accessors + +// SetValue sets an [Object] value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_value(_:_:_:)?language=objc +func (o *Dictionary) SetValue(key string, val Object) { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + C.xpcDictionarySetValue(objc.Ptr(o), cKey, objc.Ptr(val)) +} + +// Count returns the number of key-value pairs in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_count(_:)?language=objc +func (o *Dictionary) Count() int { + return int(C.xpcDictionaryGetCount(objc.Ptr(o))) +} + +// GetValue retrieves an [Object] value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_value(_:_:)?language=objc +// +// Returns nil if the key does not exist. +func (o *Dictionary) GetValue(key string) Object { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + val := C.xpcDictionaryGetValue(objc.Ptr(o), cKey) + if val == nil { + return nil + } + return NewObject(val) +} + +// MARK: - Iteration + +// DictionaryApplier is a function type for applying to each key-value pair in the XPC dictionary. +type DictionaryApplier func(string, Object) bool + +// callDictionaryApplier is called from C to apply a function to each key-value pair in the XPC dictionary object. +// +//export callDictionaryApplier +func callDictionaryApplier(cgoApplier uintptr, cKey *C.char, cgoValue uintptr) C.bool { + applier := cgohandler.Unwrap[DictionaryApplier](cgoApplier) + return C.bool(applier(C.GoString(cKey), unwrapObject[Object](cgoValue))) +} + +// All iterates over all key-value pairs in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_apply(_:_:)?language=objc +func (o *Dictionary) All() iter.Seq2[string, Object] { + return func(yieald func(string, Object) bool) { + cgoApplier := cgo.NewHandle(DictionaryApplier(yieald)) + defer cgoApplier.Delete() + C.xpcDictionaryApply(objc.Ptr(o), C.uintptr_t(cgoApplier)) + } +} + +// Keys iterates over all keys in the [Dictionary] using [Dictionary.All]. +func (o *Dictionary) Keys() iter.Seq[string] { + return func(yieald func(string) bool) { + for key := range o.All() { + if !yieald(key) { + return + } + } + } +} + +// Values iterates over all [Object] values in the [Dictionary] using [Dictionary.All]. +func (o *Dictionary) Values() iter.Seq[Object] { + return func(yieald func(Object) bool) { + for _, value := range o.All() { + if !yieald(value) { + return + } + } + } +} + +// Entries iterates over all [DictionaryEntry] entries in the [Dictionary] using [Dictionary.All]. +func (o *Dictionary) Entries() iter.Seq[DictionaryEntry] { + return func(yieald func(DictionaryEntry) bool) { + for key, value := range o.All() { + if !yieald(KeyValue(key, value)) { + return + } + } + } +} + +// MARK: - Typed Getters + +// DupFd retrieves a duplicated file descriptor from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_dup_fd(_:_:)?language=objc +func (o *Dictionary) DupFd(key string) uintptr { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + return uintptr(C.xpcDictionaryDupFd(objc.Ptr(o), cKey)) +} + +// GetArray retrieves an [Array] value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_array(_:_:)?language=objc +// +// Returns nil if the key does not exist. +func (o *Dictionary) GetArray(key string) *Array { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + p := C.xpcDictionaryGetArray(objc.Ptr(o), cKey) + if p == nil { + return nil + } + return &Array{newXpcObject(p)} +} + +// GetBool retrieves a boolean value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_bool(_:_:)?language=objc +func (o *Dictionary) GetBool(key string) bool { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + val := C.xpcDictionaryGetBool(objc.Ptr(o), cKey) + return bool(val) +} + +// GetData retrieves a byte slice value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_data(_:_:_:)?language=objc +// +// Returns nil if the key does not exist. +func (o *Dictionary) GetData(key string) []byte { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + var n C.size_t + p := C.xpcDictionaryGetData(objc.Ptr(o), cKey, &n) + if p == nil || n == 0 { + return nil + } + return C.GoBytes(p, C.int(n)) +} + +// GetDate retrieves a date value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_date(_:_:)?language=objc +func (o *Dictionary) GetDate(key string) time.Time { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + unixNano := C.xpcDictionaryGetDate(objc.Ptr(o), cKey) + return time.Unix(0, int64(unixNano)) +} + +// GetDictionary retrieves a [Dictionary] value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_dictionary(_:_:)?language=objc +func (o *Dictionary) GetDictionary(key string) *Dictionary { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + p := C.xpcDictionaryGetDictionary(objc.Ptr(o), cKey) + if p == nil { + return nil + } + return &Dictionary{newXpcObject(p)} +} + +// GetDouble retrieves a double value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_double(_:_:)?language=objc +func (o *Dictionary) GetDouble(key string) float64 { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + val := C.xpcDictionaryGetDouble(objc.Ptr(o), cKey) + return float64(val) +} + +// GetInt64 retrieves an int64 value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_int64(_:_:)?language=objc +func (o *Dictionary) GetInt64(key string) int64 { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + val := C.xpcDictionaryGetInt64(objc.Ptr(o), cKey) + return int64(val) +} + +// GetString retrieves a string value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_string(_:_:)?language=objc +// +// Returns an empty string if the key does not exist. +func (o *Dictionary) GetString(key string) string { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + val := C.xpcDictionaryGetString(objc.Ptr(o), cKey) + return C.GoString(val) +} + +// GetUInt64 retrieves a uint64 value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_uint64(_:_:)?language=objc +func (o *Dictionary) GetUInt64(key string) uint64 { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + val := C.xpcDictionaryGetUInt64(objc.Ptr(o), cKey) + return uint64(val) +} + +// GetUUID retrieves a UUID value from the [Dictionary] by key. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_get_uuid(_:_:)?language=objc +func (o *Dictionary) GetUUID(key string) [16]byte { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + var uuid [16]byte + ptr := C.xpcDictionaryGetUUID(objc.Ptr(o), cKey) + if ptr == nil { + return uuid + } + copy(uuid[:], unsafe.Slice((*byte)(ptr), 16)) + return uuid +} + +// MARK: - Typed Setters + +// SetBool sets a boolean value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_bool(_:_:_:)?language=objc +func (o *Dictionary) SetBool(key string, value bool) { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + C.xpcDictionarySetBool(objc.Ptr(o), cKey, C.bool(value)) +} + +// SetData sets a byte slice value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_data(_:_:_:_:)?language=objc +func (o *Dictionary) SetData(key string, data []byte) { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + var ptr unsafe.Pointer + var length C.size_t + if len(data) > 0 { + ptr = unsafe.Pointer(&data[0]) + length = C.size_t(len(data)) + } + C.xpcDictionarySetData(objc.Ptr(o), cKey, ptr, length) +} + +// SetDate sets a date value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_date(_:_:_:)?language=objc +func (o *Dictionary) SetDate(key string, t time.Time) { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + timestamp := C.int64_t(t.UnixNano()) + C.xpcDictionarySetDate(objc.Ptr(o), cKey, timestamp) +} + +// SetDouble sets a double value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_double(_:_:_:)?language=objc +func (o *Dictionary) SetDouble(key string, value float64) { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + C.xpcDictionarySetDouble(objc.Ptr(o), cKey, C.double(value)) +} + +// SetFd sets a file descriptor value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_fd(_:_:_:)?language=objc +func (o *Dictionary) SetFd(key string, fd uintptr) { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + C.xpcDictionarySetFd(objc.Ptr(o), cKey, C.int(fd)) +} + +// SetInt64 sets an int64 value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_int64(_:_:_:)?language=objc +func (o *Dictionary) SetInt64(key string, value int64) { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + C.xpcDictionarySetInt64(objc.Ptr(o), cKey, C.int64_t(value)) +} + +// SetString sets a string value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_string(_:_:_:)?language=objc +func (o *Dictionary) SetString(key string, value string) { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + cValue := C.CString(value) + defer C.free(unsafe.Pointer(cValue)) + C.xpcDictionarySetString(objc.Ptr(o), cKey, cValue) +} + +// SetUInt64 sets a uint64 value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_uint64(_:_:_:)?language=objc +func (o *Dictionary) SetUInt64(key string, value uint64) { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + C.xpcDictionarySetUInt64(objc.Ptr(o), cKey, C.uint64_t(value)) +} + +// SetUUID sets a UUID value for the given key in the [Dictionary]. +// - https://developer.apple.com/documentation/xpc/xpc_dictionary_set_uuid(_:_:_:)?language=objc +func (o *Dictionary) SetUUID(key string, uuid [16]byte) { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + C.xpcDictionarySetUUID(objc.Ptr(o), cKey, (*C.uint8_t)(unsafe.Pointer(&uuid[0]))) +} + +// MARK: - Peer Requirement + +// SenderSatisfies checks if the sender of the message [Dictionary] satisfies the given [PeerRequirement]. +// - https://developer.apple.com/documentation/xpc/xpc_peer_requirement_match_received_message?language=objc +func (d *Dictionary) SenderSatisfies(requirement *PeerRequirement) (bool, error) { + var err_out unsafe.Pointer + res := C.xpcPeerRequirementMatchReceivedMessage(objc.Ptr(requirement), objc.Ptr(d), &err_out) + if err_out != nil { + return false, fmt.Errorf("error matching peer requirement: %w", newRichError(err_out)) + } + return bool(res), nil +} diff --git a/xpc/error.go b/xpc/error.go new file mode 100644 index 00000000..e38d8ec1 --- /dev/null +++ b/xpc/error.go @@ -0,0 +1,48 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc_darwin.h" +*/ +import "C" +import ( + "unsafe" + + "github.com/Code-Hex/vz/v3/internal/objc" +) + +// RichError represents an XPC rich error. ([XPC_TYPE_RICH_ERROR]) [TypeRichError] +// +// [XPC_TYPE_RICH_ERROR]: https://developer.apple.com/documentation/xpc/xpc_rich_error_t?language=objc +type RichError struct { + *xpcObject +} + +var _ Object = &RichError{} + +var _ error = RichError{} + +// newRichError creates a new RichError from an existing xpc_rich_error_t. +// internal use only. +func newRichError(richErr unsafe.Pointer) *RichError { + if richErr == nil { + return nil + } + return &RichError{newXpcObject(richErr)} +} + +// CanRetry indicates whether the operation that caused the [RichError] can be retried. +// +// - https://developer.apple.com/documentation/xpc/xpc_rich_error_can_retry(_:)?language=objc +func (e RichError) CanRetry() bool { + return bool(C.xpcRichErrorCanRetry(objc.Ptr(e))) +} + +// Error implements the [error] interface. +// +// - https://developer.apple.com/documentation/xpc/xpc_rich_error_copy_description(_:)?language=objc +func (e RichError) Error() string { + desc := C.xpcRichErrorCopyDescription(objc.Ptr(e)) + defer C.free(unsafe.Pointer(desc)) + return C.GoString(desc) +} diff --git a/xpc/listener.go b/xpc/listener.go new file mode 100644 index 00000000..e186c16d --- /dev/null +++ b/xpc/listener.go @@ -0,0 +1,100 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation +# include "xpc_darwin.h" +*/ +import "C" +import ( + "unsafe" + + "github.com/Code-Hex/vz/v3/internal/cgohandler" + "github.com/Code-Hex/vz/v3/internal/objc" +) + +// Listener represents an XPC listener. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_t?language=objc +type Listener struct { + *xpcObject + sessionHandler *cgohandler.Handler +} + +// SessionHandler is a function that handles incoming sessions. +type SessionHandler func(session *Session) + +// Option represents an option for creating a [Listener]. +type ListenerOption interface { + inactiveListenerSet(*Listener) +} + +var ( + _ ListenerOption = (*PeerRequirement)(nil) +) + +// NewListener creates a new [Listener] for the given service name. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_create +// +// You need to call [Listener.Activate] to start accepting incoming connections. +func NewListener(service string, handler SessionHandler, options ...ListenerOption) (*Listener, error) { + if err := macOSAvailable(14); err != nil { + return nil, err + } + + cname := C.CString(service) + defer C.free(unsafe.Pointer(cname)) + // Use a serial dispatch queue for the listener, + // because the vmnet framework API does not seem to work well with concurrent queues. + // For example, vmnet_network_create fails when using a concurrent queue. + q := C.dispatchQueueCreateSerial(cname) + defer C.dispatchRelease(q) + cgoHandler, p := cgohandler.New(handler) + var err_out unsafe.Pointer + ptr := C.xpcListenerCreate(cname, q, C.XPC_LISTENER_CREATE_INACTIVE, C.uintptr_t(p), &err_out) + if err_out != nil { + return nil, newRichError(err_out) + } + listener := ReleaseOnCleanup(&Listener{ + xpcObject: newXpcObject(ptr), + sessionHandler: cgoHandler, + }) + for _, opt := range options { + opt.inactiveListenerSet(listener) + } + return listener, nil +} + +// callSessionHandler is called from C to handle incoming sessions. +// +//export callSessionHandler +func callSessionHandler(cgoSessionHandler, cgoSession uintptr) { + handler := cgohandler.Unwrap[SessionHandler](cgoSessionHandler) + session := unwrapObject[*Session](cgoSession) + handler(session) +} + +// String returns a description of the [Listener]. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_copy_description +func (l *Listener) String() string { + desc := C.xpcListenerCopyDescription(objc.Ptr(l)) + defer C.free(unsafe.Pointer(desc)) + return C.GoString(desc) +} + +// Activate starts the [Listener] to accept incoming connections. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_activate +func (l *Listener) Activate() error { + var err_out unsafe.Pointer + C.xpcListenerActivate(objc.Ptr(l), &err_out) + if err_out != nil { + return newRichError(err_out) + } + return nil +} + +// Close stops the [Listener] from accepting incoming connections. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_cancel +func (l *Listener) Close() error { + C.xpcListenerCancel(objc.Ptr(l)) + return nil +} diff --git a/xpc/object.go b/xpc/object.go new file mode 100644 index 00000000..23b499cf --- /dev/null +++ b/xpc/object.go @@ -0,0 +1,350 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc_darwin.h" +*/ +import "C" +import ( + "runtime/cgo" + "time" + "unsafe" + + "github.com/Code-Hex/vz/v3/internal/objc" +) + +// MARK: - Object: Untyped XPC Object + +// Object represents an untyped XPC object ([xpc_object_t]). +// +// [xpc_object_t]: https://developer.apple.com/documentation/xpc/xpc_object_t?language=objc +type Object interface { + objc.NSObject + String() string // String returns the description of the XPC object. + releaseOnCleanup() // releaseOnCleanup releases the XPC object on cleanup. +} + +// GetType returns the [Type] of the given [Object]. +// - https://developer.apple.com/documentation/xpc/xpc_get_type(_:)?language=objc +func GetType(o Object) Type { + return Type{C.xpcGetType(objc.Ptr(o))} +} + +// NewObject creates a new [Object] from an existing [xpc_object_t]. +// The XPC APIs should be wrapped in C to use void* instead of [xpc_object_t]. +// This function accepts an [unsafe.Pointer] that represents void* in C. +// It determines the specific type and returns the appropriate wrapper. +// +// [xpc_object_t]: https://developer.apple.com/documentation/xpc/xpc_object_t?language=objc +func NewObject(o unsafe.Pointer) Object { + if o == nil { + return nil + } + xpcObject := newXpcObject(o) + // Determine the specific type and return the appropriate wrapper. + // It allows users to use type assertions to access type-specific methods. + switch GetType(xpcObject) { + case TypeArray: + return &Array{xpcObject} + case TypeBool: + return &Bool{xpcObject} + case TypeData: + return &Data{xpcObject} + case TypeDate: + return &Date{xpcObject} + case TypeDictionary: + return &Dictionary{xpcObject} + case TypeDouble: + return &Double{xpcObject} + case TypeFD: + return &Fd{xpcObject} + case TypeInt64: + return &Int64{xpcObject} + case TypeNull: + return &Null{xpcObject} + case TypeRichError: + return &RichError{xpcObject} + case TypeSession: + return &Session{xpcObject: xpcObject} + case TypeString: + return &String{xpcObject} + case TypeUInt64: + return &UInt64{xpcObject} + case TypeUUID: + return &UUID{xpcObject} + default: + return xpcObject + } +} + +// wrapRawObject wraps an existing xpc_object_t into an Object and returns a handle. +// intended to be called from C. +// +//export wrapRawObject +func wrapRawObject(ptr unsafe.Pointer) uintptr { + o := NewObject(ptr) + if o == nil { + return 0 + } + return uintptr(cgo.NewHandle(o)) +} + +// unwrapObject unwraps the [cgo.Handle] from the given uintptr and returns the associated Object. +// It also deletes the handle to avoid memory leaks. +func unwrapObject[T any](handle uintptr) T { + if handle == 0 { + var zero T + return zero + } + defer cgo.Handle(handle).Delete() + return cgo.Handle(handle).Value().(T) +} + +// MARK: - Bool: XPC_TYPE_BOOL represents an XPC boolean object. + +// Bool represents an XPC boolean([XPC_TYPE_BOOL]) object. [TypeBool] +// +// [XPC_TYPE_BOOL]: https://developer.apple.com/documentation/xpc/xpc_type_bool-c.macro?language=objc +type Bool struct{ *xpcObject } + +var _ Object = &Bool{} + +// NewBool returns a new [Bool] object from the given Go bool. +// - https://developer.apple.com/documentation/xpc/xpc_bool_create(_:)?language=objc +func NewBool(b bool) Object { + cbool := C.bool(b) + return ReleaseOnCleanup(&Bool{newXpcObject(C.xpcBoolCreate(cbool))}) +} + +// Value returns the boolean value of the [Bool] object. +// - https://developer.apple.com/documentation/xpc/xpc_bool_get_value(_:)?language=objc +func (b *Bool) Bool() bool { + return bool(C.xpcBoolGetValue(objc.Ptr(b))) +} + +var ( + BoolTrue = &Bool{newXpcObject(C.xpcBoolTrue())} + BoolFalse = &Bool{newXpcObject(C.xpcBoolFalse())} +) + +// MARK: - Data: XPC_TYPE_DATA represents an XPC data object. + +// Data represents an XPC data([XPC_TYPE_DATA]) object. [TypeData] +// +// [XPC_TYPE_DATA]: https://developer.apple.com/documentation/xpc/xpc_type_data-c.macro?language=objc +type Data struct{ *xpcObject } + +var _ Object = &Data{} + +// NewData returns a new [Data] object from the given byte slice. +// - https://developer.apple.com/documentation/xpc/xpc_data_create(_:_:)?language=objc +func NewData(b []byte) Object { + if len(b) == 0 { + return ReleaseOnCleanup(&Data{newXpcObject(C.xpcDataCreate(nil, 0))}) + } + return ReleaseOnCleanup(&Data{newXpcObject(C.xpcDataCreate( + unsafe.Pointer(&b[0]), + C.size_t(len(b)), + ))}) +} + +// Bytes returns the byte slice of the [Data] object. +// - https://developer.apple.com/documentation/xpc/xpc_data_get_bytes_ptr(_:)?language=objc +// - https://developer.apple.com/documentation/xpc/xpc_data_get_length(_:)?language=objc +func (d *Data) Bytes() []byte { + size := C.xpcDataGetLength(objc.Ptr(d)) + ptr := C.xpcDataGetBytesPtr(objc.Ptr(d)) + if ptr == nil || size == 0 { + return nil + } + return C.GoBytes(ptr, C.int(size)) +} + +// MARK: - Double: XPC_TYPE_DOUBLE represents an XPC double object. + +// Double represents an XPC double([XPC_TYPE_DOUBLE]) object. [TypeDouble] +// +// [XPC_TYPE_DOUBLE]: https://developer.apple.com/documentation/xpc/xpc_type_double-c.macro?language=objc +type Double struct{ *xpcObject } + +var _ Object = &Double{} + +// NewDouble returns a new [Double] object from the given Go float64. +// - https://developer.apple.com/documentation/xpc/xpc_double_create(_:)?language=objc +func NewDouble(f float64) Object { + cdouble := C.double(f) + return ReleaseOnCleanup(&Double{newXpcObject(C.xpcDoubleCreate(cdouble))}) +} + +// Value returns the float64 value of the [Double] object. +// - https://developer.apple.com/documentation/xpc/xpc_double_get_value(_:)?language=objc +func (d *Double) Float64() float64 { + return float64(C.xpcDoubleGetValue(objc.Ptr(d))) +} + +// MARK: - Int64: XPC_TYPE_INT64 represents an XPC int64 object. + +// Int64 represents an XPC int64([XPC_TYPE_INT64]) object. [TypeInt64] +// +// [XPC_TYPE_INT64]: https://developer.apple.com/documentation/xpc/xpc_type_int64-c.macro?language=objc +type Int64 struct{ *xpcObject } + +var _ Object = &Int64{} + +// NewInt64 returns a new [Int64] object from the given Go int64. +// - https://developer.apple.com/documentation/xpc/xpc_int64_create(_:)?language=objc +func NewInt64(i int64) Object { + cint64 := C.int64_t(i) + return ReleaseOnCleanup(&Int64{newXpcObject(C.xpcInt64Create(cint64))}) +} + +// Value returns the int64 value of the [Int64] object. +// - https://developer.apple.com/documentation/xpc/xpc_int64_get_value(_:)?language=objc +func (i *Int64) Int64() int64 { + return int64(C.xpcInt64GetValue(objc.Ptr(i))) +} + +// MARK: - UInt64: XPC_TYPE_UINT64 represents an XPC uint64 object. + +// UInt64 represents an XPC uint64([XPC_TYPE_UINT64]) object. [TypeUInt64] +// +// [XPC_TYPE_UINT64]: https://developer.apple.com/documentation/xpc/xpc_type_uint64-c.macro?language=objc +type UInt64 struct{ *xpcObject } + +var _ Object = &UInt64{} + +// NewUInt64 returns a new [UInt64] object from the given Go uint64. +// - https://developer.apple.com/documentation/xpc/xpc_uint64_create(_:)?language=objc +func NewUInt64(u uint64) Object { + cuint64 := C.uint64_t(u) + return ReleaseOnCleanup(&UInt64{newXpcObject(C.xpcUInt64Create(cuint64))}) +} + +// Value returns the uint64 value of the [UInt64] object. +// - https://developer.apple.com/documentation/xpc/xpc_uint64_get_value(_:)?language=objc +func (u *UInt64) UInt64() uint64 { + return uint64(C.xpcUInt64GetValue(objc.Ptr(u))) +} + +// MARK: - String: XPC_TYPE_STRING represents an XPC string object. + +// String represents an XPC string([XPC_TYPE_STRING]) object. [TypeString] +// +// [XPC_TYPE_STRING]: https://developer.apple.com/documentation/xpc/xpc_type_string-c.macro?language=objc +type String struct{ *xpcObject } + +var _ Object = &String{} + +// NewString returns a new [String] object from the given Go string. +// - https://developer.apple.com/documentation/xpc/xpc_string_create(_:)?language=objc +func NewString(s string) Object { + cstr := C.CString(s) + defer C.free(unsafe.Pointer(cstr)) + return ReleaseOnCleanup(&String{newXpcObject(C.xpcStringCreate(cstr))}) +} + +// String returns the Go string value of the [String] object. +// - https://developer.apple.com/documentation/xpc/xpc_string_get_string_ptr(_:)?language=objc +func (s *String) String() string { + cstr := C.xpcStringGetStringPtr(objc.Ptr(s)) + len := C.xpcStringGetLength(objc.Ptr(s)) + if cstr == nil || len == 0 { + return "" + } + return C.GoStringN(cstr, C.int(len)) +} + +// MARK: - Fd: XPC_TYPE_FD represents an XPC file descriptor object. + +// Fd represents an XPC file descriptor([XPC_TYPE_FD]) object. [TypeFd] +// +// [XPC_TYPE_FD]: https://developer.apple.com/documentation/xpc/xpc_type_fd-c.macro?language=objc +type Fd struct{ *xpcObject } + +var _ Object = &Fd{} + +// NewFd returns a new [Fd] object from the given file descriptor. +// - https://developer.apple.com/documentation/xpc/xpc_fd_create(_:)?language=objc +func NewFd(fd uintptr) Object { + cfd := C.int(fd) + return ReleaseOnCleanup(&Fd{newXpcObject(C.xpcFdCreate(cfd))}) +} + +// Dup returns a duplicated file descriptor from the [Fd] object. +// - https://developer.apple.com/documentation/xpc/xpc_fd_dup(_:)?language=objc +func (f *Fd) Dup() uintptr { + return uintptr(C.xpcFdDup(objc.Ptr(f))) +} + +// MARK: - Date: XPC_TYPE_DATE represents an XPC date object. + +// Date represents an XPC date([XPC_TYPE_DATE]) object. [TypeDate] +// +// [XPC_TYPE_DATE]: https://developer.apple.com/documentation/xpc/xpc_type_date-c.macro?language=objc +type Date struct{ *xpcObject } + +var _ Object = &Date{} + +// NewDate returns a new [Date] object from the given Go int64 representing nanoseconds since epoch. +// - https://developer.apple.com/documentation/xpc/xpc_date_create(_:)?language=objc +func NewDate(nanoseconds int64) Object { + cinterval := C.int64_t(nanoseconds) + return ReleaseOnCleanup(&Date{newXpcObject(C.xpcDateCreate(cinterval))}) +} + +// NewDateFromCurrent returns a new [Date] object representing the current date and time. +// - https://developer.apple.com/documentation/xpc/xpc_date_from_current()?language=objc +func NewDateFromCurrent() Object { + return ReleaseOnCleanup(&Date{newXpcObject(C.xpcDateCreateFromCurrent())}) +} + +// Value returns the [time.Time] value of the [Date] object. +// - https://developer.apple.com/documentation/xpc/xpc_date_get_value(_:)?language=objc +func (d *Date) Time() time.Time { + unixNano := int64(C.xpcDateGetValue(objc.Ptr(d))) + return time.Unix(0, unixNano) +} + +// MARK: - UUID: XPC_TYPE_UUID represents an XPC UUID object. + +// UUID represents an XPC UUID([XPC_TYPE_UUID]) object. [TypeUUID] +// +// [XPC_TYPE_UUID]: https://developer.apple.com/documentation/xpc/xpc_type_uuid-c.macro?language=objc +type UUID struct{ *xpcObject } + +var _ Object = &UUID{} + +// NewUUID returns a new [UUID] object from the given UUID byte array. +// - https://developer.apple.com/documentation/xpc/xpc_uuid_create(_:)?language=objc +func NewUUID(uuid [16]byte) Object { + cuuid := (*C.uint8_t)(unsafe.Pointer(&uuid[0])) + return ReleaseOnCleanup(&UUID{newXpcObject(C.xpcUUIDCreate(cuuid))}) +} + +// Bytes returns the UUID byte array of the [UUID] object. +// - https://developer.apple.com/documentation/xpc/xpc_uuid_get_bytes(_:)?language=objc +func (u *UUID) Bytes() [16]byte { + var uuid [16]byte + ptr := C.xpcUUIDGetBytes(objc.Ptr(u)) + if ptr == nil { + return uuid + } + copy(uuid[:], unsafe.Slice((*byte)(ptr), 16)) + return uuid +} + +// MARK: - Shared Memory: XPC_TYPE_SHMEM represents an XPC shared memory object. +// MARK: - Null: XPC_TYPE_NULL represents an XPC null object. + +// Null represents an XPC null([XPC_TYPE_NULL]) object. [TypeNull] +// +// [XPC_TYPE_NULL]: https://developer.apple.com/documentation/xpc/xpc_type_null-c.macro?language=objc +type Null struct{ *xpcObject } + +var _ Object = &Null{} + +// NewNull returns a new [Null] object. +// - https://developer.apple.com/documentation/xpc/xpc_null_create()?language=objc +func NewNull() Object { + return ReleaseOnCleanup(&Null{newXpcObject(C.xpcNullCreate())}) +} diff --git a/xpc/osversion_alias.go b/xpc/osversion_alias.go new file mode 100644 index 00000000..cb747790 --- /dev/null +++ b/xpc/osversion_alias.go @@ -0,0 +1,7 @@ +package xpc + +import ( + "github.com/Code-Hex/vz/v3/internal/osversion" +) + +var macOSAvailable = osversion.MacOSAvailable diff --git a/xpc/peer_requirement.go b/xpc/peer_requirement.go new file mode 100644 index 00000000..d3ea8c20 --- /dev/null +++ b/xpc/peer_requirement.go @@ -0,0 +1,62 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation +# include "xpc_darwin.h" +*/ +import "C" +import ( + "unsafe" + + "github.com/Code-Hex/vz/v3/internal/objc" +) + +// PeerRequirement represents an [xpc_peer_requirement_t]. (macOS 26.0+) +// +// [xpc_peer_requirement_t]: https://developer.apple.com/documentation/xpc/xpc_peer_requirement_t?language=objc +type PeerRequirement struct { + *xpcObject +} + +var _ Object = &PeerRequirement{} + +// NewPeerRequirementLwcr creates a [PeerRequirement] from a LWCR object *[Dictionary]. (macOS 26.0+) +// - https://developer.apple.com/documentation/xpc/xpc_peer_requirement_create_lwcr +// - https://developer.apple.com/documentation/security/defining-launch-environment-and-library-constraints?language=objc +func NewPeerRequirementLwcr(lwcr *Dictionary) (*PeerRequirement, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + var err_out unsafe.Pointer + ptr := C.xpcPeerRequirementCreateLwcr(objc.Ptr(lwcr), &err_out) + if err_out != nil { + return nil, newRichError(err_out) + } + return ReleaseOnCleanup(&PeerRequirement{newXpcObject(ptr)}), nil +} + +// NewPeerRequirementLwcrWithEntries creates a [PeerRequirement] from a LWCR object *[Dictionary] constructed +// with the given [DictionaryEntry]s. (macOS 26.0+) +// - https://developer.apple.com/documentation/xpc/xpc_peer_requirement_create_lwcr +// - https://developer.apple.com/documentation/security/defining-launch-environment-and-library-constraints?language=objc +func NewPeerRequirementLwcrWithEntries(entries ...DictionaryEntry) (*PeerRequirement, error) { + return NewPeerRequirementLwcr(NewDictionary(entries...)) +} + +// inactiveListenerSet configures the given [Listener] with the [PeerRequirement]. (macOS 26.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_set_peer_requirement +// +// This method implements the [ListenerOption] interface. +func (pr *PeerRequirement) inactiveListenerSet(listener *Listener) { + C.xpcListenerSetPeerRequirement(objc.Ptr(listener), objc.Ptr(pr)) +} + +// inactiveSessionSet configures the given [Session] with the [PeerRequirement]. (macOS 26.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_set_peer_requirement +// +// This method implements the [SessionOption] interface. +func (pr *PeerRequirement) inactiveSessionSet(session *Session) { + C.xpcSessionSetPeerRequirement(objc.Ptr(session), objc.Ptr(pr)) +} diff --git a/xpc/session.go b/xpc/session.go new file mode 100644 index 00000000..7d74aedc --- /dev/null +++ b/xpc/session.go @@ -0,0 +1,227 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +#cgo darwin LDFLAGS: -lobjc -framework Foundation +# include "xpc_darwin.h" +*/ +import "C" +import ( + "context" + "runtime/cgo" + "unsafe" + + "github.com/Code-Hex/vz/v3/internal/cgohandler" + "github.com/Code-Hex/vz/v3/internal/objc" +) + +// Session represents an [xpc_session_t]. (macOS 13.0+) +// +// [xpc_session_t]: https://developer.apple.com/documentation/xpc/xpc_session_t?language=objc +type Session struct { + // Exported for use in other packages since unimplemented XPC API may require direct access to xpc_session_t. + *xpcObject + cancellationHandler *cgohandler.Handler + incomingMessageHandler *cgohandler.Handler +} + +var _ Object = &Session{} + +// SessionOption represents an option for configuring a inactive [Session]. +type SessionOption interface { + inactiveSessionSet(*Session) +} + +var ( + _ SessionOption = (MessageHandler)(nil) + _ SessionOption = (CancellationHandler)(nil) + _ SessionOption = (*PeerRequirement)(nil) +) + +// NewSession creates a new [Session] for the given Mach service name. (macOS 13.0+) +// +// [SessionOption](s) can be provided to configure the inactive session before activation. +// Available options include [MessageHandler], [CancellationHandler], and [PeerRequirement]. +// - https://developer.apple.com/documentation/xpc/xpc_session_create_mach_service +func NewSession(macServiceName string, sessionOpts ...SessionOption) (*Session, error) { + if err := macOSAvailable(13); err != nil { + return nil, err + } + + cServiceName := C.CString(macServiceName) + defer C.free(unsafe.Pointer(cServiceName)) + + var err_out unsafe.Pointer + ptr := C.xpcSessionCreateMachService(cServiceName, nil, C.XPC_SESSION_CREATE_INACTIVE, &err_out) + if err_out != nil { + return nil, newRichError(err_out) + } + session := ReleaseOnCleanup(&Session{xpcObject: newXpcObject(ptr)}) + for _, o := range sessionOpts { + o.inactiveSessionSet(session) + } + err := session.activate() + if err != nil { + session.Cancel() + return nil, err + } + return session, nil +} + +// MessageHandler is a function [SessionOption] that handles incoming messages in a [Session]. +// It receives the incoming message *[Dictionary] and returns a reply message *[Dictionary]. +type MessageHandler func(msg *Dictionary) (reply *Dictionary) + +// inactiveSessionSet configures the given [Session] with the [MessageHandler]. (macOS 13.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_set_incoming_message_handler +func (mh MessageHandler) inactiveSessionSet(s *Session) { + s.setIncomingMessageHandler(mh) +} + +// CancellationHandler is a function [SessionOption] that handles session cancellation in a [Session] +// It receives the [RichError] that caused the cancellation. +type CancellationHandler func(err *RichError) + +// inactiveSessionSet configures the given [Session] with the [CancellationHandler]. (macOS 13.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_set_cancel_handler +func (ch CancellationHandler) inactiveSessionSet(s *Session) { + s.setCancellationHandler(ch) +} + +// Reject rejects the incoming [Session] with the given reason. (macOS 14.0+) +// - https://developer.apple.com/documentation/xpc/xpc_listener_reject_peer +func (s *Session) Reject(reason string) { + cReason := C.CString(reason) + defer C.free(unsafe.Pointer(cReason)) + C.xpcListenerRejectPeer(objc.Ptr(s), cReason) +} + +// Accept creates a [SessionHandler] that accepts incoming sessions with the given [SessionOption]s. +func Accept(sessionOptions ...SessionOption) SessionHandler { + return func(session *Session) { + for _, opt := range sessionOptions { + opt.inactiveSessionSet(session) + } + } +} + +// String returns a description of the [Session]. (macOS 13.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_copy_description +func (s *Session) String() string { + desc := C.xpcSessionCopyDescription(objc.Ptr(s)) + defer C.free(unsafe.Pointer(desc)) + return C.GoString(desc) +} + +// activate activates the [Session]. (macOS 13.0+) +// It is called internally after applying all [SessionOption]s in [NewSession]. +// - https://developer.apple.com/documentation/xpc/xpc_session_activate +func (s *Session) activate() error { + var err_out unsafe.Pointer + C.xpcSessionActivate(objc.Ptr(s), &err_out) + if err_out != nil { + return newRichError(err_out) + } + return nil +} + +// callMessageHandler is called from C to handle incoming messages. +// +//export callMessageHandler +func callMessageHandler(cgoMessageHandler, cgoMessage uintptr) (reply unsafe.Pointer) { + handler := cgohandler.Unwrap[MessageHandler](cgoMessageHandler) + message := unwrapObject[*Dictionary](cgoMessage) + return objc.Ptr(handler(message)) +} + +// setIncomingMessageHandler sets the [MessageHandler] for the inactive [Session]. (macOS 13.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_set_incoming_message_handler +func (s *Session) setIncomingMessageHandler(handler MessageHandler) { + cgoHandler, p := cgohandler.New(handler) + C.xpcSessionSetIncomingMessageHandler(objc.Ptr(s), C.uintptr_t(p)) + // Store the handler after setting it to avoid premature garbage collection of the previous handler. + s.incomingMessageHandler = cgoHandler +} + +// Cancel cancels the [Session]. (macOS 13.0+) +// - https://developer.apple.com/documentation/xpc/xpc_session_cancel +func (s *Session) Cancel() { + C.xpcSessionCancel(objc.Ptr(s)) +} + +// callCancelHandler is called from C to handle session cancellation. +// +//export callCancelHandler +func callCancelHandler(cgoCancelHandler, cgoErr uintptr) { + handler := cgohandler.Unwrap[CancellationHandler](cgoCancelHandler) + err := unwrapObject[*RichError](cgoErr) + handler(err) +} + +// setCancellationHandler sets the [CancellationHandler] for the inactive [Session]. (macOS 13.0+) +// The handler will call [Session.handleCancellation] after executing the provided handler. +// - https://developer.apple.com/documentation/xpc/xpc_session_set_cancel_handler +func (s *Session) setCancellationHandler(handler CancellationHandler) { + cgoHandler, p := cgohandler.New((CancellationHandler)(func(err *RichError) { + if handler != nil { + handler(err) + } + s.handleCancellation(err) + })) + C.xpcSessionSetCancelHandler(objc.Ptr(s), C.uintptr_t(p)) + // Store the handler after setting it to avoid premature garbage collection of the previous handler. + s.cancellationHandler = cgoHandler +} + +// handleCancellation handles [Session] cancellation by deleting the associated handles. +func (s *Session) handleCancellation(_ *RichError) { +} + +type ReplyHandler func(*Dictionary, *RichError) + +// callReplyHandler is called from C to handle reply messages. +// +//export callReplyHandler +func callReplyHandler(cgoReplyHandler uintptr, cgoReply, cgoError uintptr) { + handler := cgohandler.Unwrap[ReplyHandler](cgoReplyHandler) + reply := unwrapObject[*Dictionary](uintptr(cgoReply)) + err := unwrapObject[*RichError](cgoError) + handler(reply, err) +} + +// SendMessageWithReply sends a message *[Dictionary] to the [Session] and waits for a reply *[Dictionary]. (macOS 13.0+) +// +// Use [context.Context] to control cancellation and timeouts. +// - https://developer.apple.com/documentation/xpc/xpc_session_send_message_with_reply_async +func (s *Session) SendMessageWithReply(ctx context.Context, message *Dictionary) (*Dictionary, error) { + replyCh := make(chan *Dictionary, 1) + errCh := make(chan *RichError, 1) + replyHandler := (ReplyHandler)(func(reply *Dictionary, err *RichError) { + defer close(replyCh) + defer close(errCh) + if err != nil { + errCh <- ReleaseOnCleanup(Retain(err)) + } else { + replyCh <- ReleaseOnCleanup(Retain(reply)) + } + }) + cgoReplyHandler := cgo.NewHandle(replyHandler) + defer cgoReplyHandler.Delete() + C.xpcSessionSendMessageWithReplyAsync(objc.Ptr(s), objc.Ptr(message), C.uintptr_t(cgoReplyHandler)) + select { + case reply := <-replyCh: + return reply, nil + case err := <-errCh: + return nil, err + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +// SendDictionaryWithReply creates a message *[Dictionary] and calls [Session.SendMessageWithReply] with it. A`(macOS 13.0+) +// +// Use [context.Context] to control cancellation and timeouts. +// The message *[Dictionary] can be customized using [DictionaryEntry]. +func (s *Session) SendDictionaryWithReply(ctx context.Context, entries ...DictionaryEntry) (*Dictionary, error) { + return s.SendMessageWithReply(ctx, NewDictionary(entries...)) +} diff --git a/xpc/type.go b/xpc/type.go new file mode 100644 index 00000000..5a80d67d --- /dev/null +++ b/xpc/type.go @@ -0,0 +1,46 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc_darwin.h" +*/ +import "C" + +// Type represents an XPC type (xpc_type_t). +// - https://developer.apple.com/documentation/xpc/xpc_type_t?language=objc +type Type struct { + xpcType C.xpc_type_t +} + +var ( + TypeActivity = Type{C.XPC_TYPE_ACTIVITY} // https://developer.apple.com/documentation/xpc/xpc_type_activity-c.macro?language=objc + TypeArray = Type{C.XPC_TYPE_ARRAY} // https://developer.apple.com/documentation/xpc/xpc_type_array-c.macro?language=objc + TypeBool = Type{C.XPC_TYPE_BOOL} // https://developer.apple.com/documentation/xpc/xpc_type_bool-c.macro?language=objc + TypeConnection = Type{C.XPC_TYPE_CONNECTION} // https://developer.apple.com/documentation/xpc/xpc_type_connection-c.macro?language=objc + TypeData = Type{C.XPC_TYPE_DATA} // https://developer.apple.com/documentation/xpc/xpc_type_data-c.macro?language=objc + TypeDate = Type{C.XPC_TYPE_DATE} // https://developer.apple.com/documentation/xpc/xpc_type_date-c.macro?language=objc + TypeDictionary = Type{C.XPC_TYPE_DICTIONARY} // https://developer.apple.com/documentation/xpc/xpc_type_dictionary-c.macro?language=objc + TypeDouble = Type{C.XPC_TYPE_DOUBLE} // https://developer.apple.com/documentation/xpc/xpc_type_double-c.macro?language=objc + TypeEndpoint = Type{C.XPC_TYPE_ENDPOINT} // https://developer.apple.com/documentation/xpc/xpc_type_endpoint-c.macro?language=objc + TypeError = Type{C.XPC_TYPE_ERROR} // https://developer.apple.com/documentation/xpc/xpc_type_error-c.macro?language=objc + TypeFD = Type{C.XPC_TYPE_FD} // https://developer.apple.com/documentation/xpc/xpc_type_fd-c.macro?language=objc + TypeInt64 = Type{C.XPC_TYPE_INT64} // https://developer.apple.com/documentation/xpc/xpc_type_int64-c.macro?language=objc + TypeNull = Type{C.XPC_TYPE_NULL} // https://developer.apple.com/documentation/xpc/xpc_type_null-c.macro?language=objc + TypeRichError = Type{C.XPC_TYPE_RICH_ERROR} // does not have official documentation, but defined in + TypeSession = Type{C.XPC_TYPE_SESSION} // does not have official documentation, but defined in + TypeShmem = Type{C.XPC_TYPE_SHMEM} // https://developer.apple.com/documentation/xpc/xpc_type_shmem-c.macro?language=objc + TypeString = Type{C.XPC_TYPE_STRING} // https://developer.apple.com/documentation/xpc/xpc_type_string-c.macro?language=objc + TypeUInt64 = Type{C.XPC_TYPE_UINT64} // https://developer.apple.com/documentation/xpc/xpc_type_uint64-c.macro?language=objc + TypeUUID = Type{C.XPC_TYPE_UUID} // https://developer.apple.com/documentation/xpc/xpc_type_uuid-c.macro?language=objc +) + +// String returns the name of the [XpcType]. +// see: https://developer.apple.com/documentation/xpc/xpc_type_get_name(_:)?language=objc +func (t Type) String() string { + cs := C.xpc_type_get_name(t.xpcType) + if cs == nil { + return "" + } + // do not free cs since it is managed by XPC runtime. + return C.GoString(cs) +} diff --git a/xpc/xpc_darwin.h b/xpc/xpc_darwin.h new file mode 100644 index 00000000..d0682cdf --- /dev/null +++ b/xpc/xpc_darwin.h @@ -0,0 +1,212 @@ +#pragma once + +#import "../internal/osversion/virtualization_helper.h" +#import + +// MARK: - dispatch_queue_t + +void *dispatchQueueCreateSerial(const char *label); +void dispatchRelease(void *queue); + +// MARK: - xpc.h types +// +// The following types are listed in the same order as the XPC documentation index page. +// https://developer.apple.com/documentation/xpc?language=objc + +// MARK: - xpc_listener_t (macOS 14+) + +void *xpcListenerCreate(const char *service_name, void *queue, uint64_t flags, uintptr_t cgo_session_handler, void **error_out); +const char *xpcListenerCopyDescription(void *listener); +bool xpcListenerActivate(void *listener, void **error_out); +void xpcListenerCancel(void *listener); +void xpcListenerRejectPeer(void *session, const char *reason); +// int xpcListenerSetPeerCodeSigningRequirement(void *listener, const char *requirement); + +// MARK: - xpc_session_t (XPC_TYPE_SESSION) (macOS 13+) + +// void *xpcSessionCreateXpcService(const char *service_name, void *queue, uint64_t flags, void **error_out); +void *xpcSessionCreateMachService(const char *service_name, void *queue, uint64_t flags, void **error_out); +// void xpcSessionSetTargetQueue(void *session, void *queue); +const char *xpcSessionCopyDescription(void *session); +bool xpcSessionActivate(void *session, void **error_out); +void xpcSessionSetIncomingMessageHandler(void *session, uintptr_t cgo_message_handler); +void xpcSessionCancel(void *session); +void xpcSessionSetCancelHandler(void *session, uintptr_t cgo_cancel_handler); +// void *xpcSessionSendMessage(void *session, void *message); +void xpcSessionSendMessageWithReplyAsync(void *session, void *message, uintptr_t cgo_reply_handler); +// void *xpcSessionSendMessageWithReplySync(void *session, void *message, void **error_out); + +// MARK: - xpc_rich_error_t (XPC_TYPE_RICH_ERROR) +bool xpcRichErrorCanRetry(void *err); +const char *xpcRichErrorCopyDescription(void *err); + +// MARK: - Identity + +// # xpc_type_t +xpc_type_t xpcGetType(void *object); +const char *xpcTypeGetName(xpc_type_t type); +// # xpc_object_t +// size_t xpxHash(void *object); + +// MARK: - Comparison + +// # xpc_object_t +// bool xpcEqual(void *object1, void *object2); + +// MARK: - Copying + +// # xpc_object_t +// void *xpcCopy(void *object); +const char *xpcCopyDescription(void *object); + +// MARK: - Boolean objects + +// # xpc_object_t (XPC_TYPE_BOOL) +void *xpcBoolCreate(bool value); +bool xpcBoolGetValue(void *object); +void *xpcBoolTrue(); // XPC_BOOL_TRUE +void *xpcBoolFalse(); // XPC_BOOL_FALSE + +// MARK: - Data objects + +// # xpc_object_t (XPC_TYPE_DATA) +void *xpcDataCreate(const void *bytes, size_t length); +// size_t xpcDataGetBytes(void *object, void *buffer, size_t offset, size_t length); +const void *xpcDataGetBytesPtr(void *object); +size_t xpcDataGetLength(void *object); + +// MARK: - Number objects + +// # xpc_object_t (XPC_TYPE_DOUBLE) +void *xpcDoubleCreate(double value); +double xpcDoubleGetValue(void *object); + +// # xpc_object_t (XPC_TYPE_INT64) +void *xpcInt64Create(int64_t value); +int64_t xpcInt64GetValue(void *object); + +// # xpc_object_t (XPC_TYPE_UINT64) +void *xpcUInt64Create(uint64_t value); +uint64_t xpcUInt64GetValue(void *object); + +// MARK: - Array objects + +// # xpc_object_t (XPC_TYPE_ARRAY) +void *xpcArrayCreate(void *const *object, size_t count); +// void *xpcArrayCreateEmpty(); +// void *xpcArrayCreateConnection +void *xpcArrayGetValue(void *object, size_t index); +void xpcArraySetValue(void *object, size_t index, void *value); +void xpcArrayAppendValue(void *object, void *value); +size_t xpcArrayGetCount(void *object); +bool xpcArrayApply(void *object, uintptr_t cgo_applier); +int xpcArrayDupFd(void *object, size_t index); +void *xpcArrayGetArray(void *object, size_t index); +bool xpcArrayGetBool(void *object, size_t index); +const void *xpcArrayGetData(void *object, size_t index, size_t *length); +int64_t xpcArrayGetDate(void *object, size_t index); +void *xpcArrayGetDictionary(void *object, size_t index); +double xpcArrayGetDouble(void *object, size_t index); +int64_t xpcArrayGetInt64(void *object, size_t index); +const char *xpcArrayGetString(void *object, size_t index); +uint64_t xpcArrayGetUInt64(void *object, size_t index); +const uint8_t *xpcArrayGetUUID(void *object, size_t index); +void xpcArraySetBool(void *object, size_t index, bool value); +// void xpcArraySetConnection +void xpcArraySetData(void *object, size_t index, const void *bytes, size_t length); +void xpcArraySetDate(void *object, size_t index, int64_t value); +void xpcArraySetDouble(void *object, size_t index, double value); +void xpcArraySetFd(void *object, size_t index, int fd); +void xpcArraySetInt64(void *object, size_t index, int64_t value); +void xpcArraySetString(void *object, size_t index, const char *string); +void xpcArraySetUInt64(void *object, size_t index, uint64_t value); +void xpcArraySetUUID(void *object, size_t index, const uuid_t uuid); +// XPC_ARRAY_APPEND + +// MARK: - Dictionary objects + +// # xpc_object_t (XPC_TYPE_DICTIONARY) +// void *xpcDictionaryCreate(const char *const *keys, void *const *values, size_t count); +void *xpcDictionaryCreateEmpty(void); +// void *xpcDictionaryCreateConnection +void *xpcDictionaryCreateReply(void *object); +void xpcDictionarySetValue(void *object, const char *key, void *value); +size_t xpcDictionaryGetCount(void *object); +void *xpcDictionaryGetValue(void *object, const char *key); +bool xpcDictionaryApply(void *object, uintptr_t cgo_applier); +int xpcDictionaryDupFd(void *object, const char *key); +void *xpcDictionaryGetArray(void *object, const char *key); +bool xpcDictionaryGetBool(void *object, const char *key); +const void *xpcDictionaryGetData(void *object, const char *key, size_t *length); +int64_t xpcDictionaryGetDate(void *object, const char *key); +void *xpcDictionaryGetDictionary(void *object, const char *key); +double xpcDictionaryGetDouble(void *object, const char *key); +int64_t xpcDictionaryGetInt64(void *object, const char *key); +// void *xpcDictionaryGetRemoteConnection +const char *xpcDictionaryGetString(void *object, const char *key); +uint64_t xpcDictionaryGetUInt64(void *object, const char *key); +const uint8_t *xpcDictionaryGetUUID(void *object, const char *key); +void xpcDictionarySetBool(void *object, const char *key, bool value); +// void xpcDictionarySetConnection +void xpcDictionarySetData(void *object, const char *key, const void *bytes, size_t length); +void xpcDictionarySetDate(void *object, const char *key, int64_t value); +void xpcDictionarySetDouble(void *object, const char *key, double value); +void xpcDictionarySetFd(void *object, const char *key, int fd); +void xpcDictionarySetInt64(void *object, const char *key, int64_t value); +void xpcDictionarySetString(void *object, const char *key, const char *value); +void xpcDictionarySetUInt64(void *object, const char *key, uint64_t value); +void xpcDictionarySetUUID(void *object, const char *key, const uint8_t *uuid); +// void *xpcDictionaryCopyMachSend +// void xpcDictionarySetMachSend + +// MARK: - String objects + +// # xpc_object_t (XPC_TYPE_STRING) +void *xpcStringCreate(const char *string); +// void *xpcStringCreateWithFormat(const char *format, ...); +// void *xpcStringCreateWithFormatAndArguments(const char *format, va_list args); +size_t xpcStringGetLength(void *object); +const char *xpcStringGetStringPtr(void *object); + +// MARK: - File Descriptor objects + +// # xpc_object_t (XPC_TYPE_FD) +void *xpcFdCreate(int fd); +int xpcFdDup(void *object); + +// MARK: - Date objects + +// # xpc_object_t (XPC_TYPE_DATE) +void *xpcDateCreate(int64_t interval); +void *xpcDateCreateFromCurrent(); +int64_t xpcDateGetValue(void *object); + +// MARK: - UUID objects + +// # xpc_object_t (XPC_TYPE_UUID) +void *xpcUUIDCreate(const uuid_t uuid); +const uint8_t *xpcUUIDGetBytes(void *object); + +// MARK: - Shared Memory objects + +// # xpc_object_t (XPC_TYPE_SHMEM) +// void *xpcShmemCreate(void *region, size_t length); +// size_t xpcShmemMap(void *object, void **region); + +// MARK: - Null objects +// # xpc_object_t (XPC_TYPE_NULL) +void *xpcNullCreate(); + +// MARK: - Object life cycle +void *xpcRetain(void *object); +void xpcRelease(void *object); + +// MARK: - xpc_peer_requirement_t (macOS 26+) +void xpcListenerSetPeerRequirement(void *listener, void *peer_requirement); +// void *xpcPeerRequirementCreateEntitlementExists(const char *entitlement, void **error_out); +// void *xpcPeerRequirementCreateEntitlementMatchesValue(const char *entitlement, void *value, void **error_out); +void *xpcPeerRequirementCreateLwcr(void *lwcr, void **error_out); +// void *xpcPeerRequirementCreatePlatformIdentity(const char * signing_identifier, void **error_out); +// void *xpcPeerRequirementCreateTeamIdentity(const char * team_identifier, void **error_out); +bool xpcPeerRequirementMatchReceivedMessage(void *peer_requirement, void *message, void **error_out); +void xpcSessionSetPeerRequirement(void *session, void *peer_requirement); diff --git a/xpc/xpc_darwin.m b/xpc/xpc_darwin.m new file mode 100644 index 00000000..9a3ea3a7 --- /dev/null +++ b/xpc/xpc_darwin.m @@ -0,0 +1,713 @@ +#include "xpc_darwin.h" + +// MARK: - Helper functions defined in Go + +// # xpc_object_t +extern uintptr_t wrapRawObject(void *obj); +// # xpc_listener_t +extern void callSessionHandler(uintptr_t cgoSessionHandler, uintptr_t cgoSession); + +// # xpc_session_t +extern void callReplyHandler(uintptr_t cgoReplyHandler, uintptr_t cgoReply, uintptr_t cgoError); +extern void callCancelHandler(uintptr_t cgoCancelHandler, uintptr_t cgoError); +extern void *callMessageHandler(uintptr_t cgoMessageHandler, uintptr_t cgoMessage); + +// # xpc_object_t (XPC_TYPE_ARRAY) +extern bool callArrayApplier(uintptr_t cgoApplier, size_t index, uintptr_t cgoValue); +// # xpc_object_t (XPC_TYPE_DICTIONARY) +extern bool callDictionaryApplier(uintptr_t cgoApplier, const char *_Nonnull key, uintptr_t cgoValue); + +// MARK: -dispatch_queue_t + +void *dispatchQueueCreateSerial(const char *label) +{ + return dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL); +} + +void dispatchRelease(void *queue) +{ + dispatch_release((dispatch_queue_t)queue); +} + +// MARK: - xpc.h types +// +// The following types are listed in the same order as the XPC documentation index page. +// https://developer.apple.com/documentation/xpc?language=objc + +// MARK: - xpc_listener_t (macOS 14+) + +void *xpcListenerCreate(const char *service_name, void *queue, uint64_t flags, uintptr_t cgo_session_handler, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + return xpc_listener_create( + service_name, + queue, + flags, + ^(xpc_session_t _Nonnull session) { + callSessionHandler(cgo_session_handler, wrapRawObject(session)); + }, + (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +const char *xpcListenerCopyDescription(void *listener) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + return xpc_listener_copy_description((xpc_listener_t)listener); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +bool xpcListenerActivate(void *listener, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + return xpc_listener_activate((xpc_listener_t)listener, (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcListenerCancel(void *listener) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + xpc_listener_cancel((xpc_listener_t)listener); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcListenerRejectPeer(void *session, const char *reason) +{ +#ifdef INCLUDE_TARGET_OSX_14 + if (@available(macOS 14, *)) { + xpc_listener_reject_peer((xpc_session_t)session, reason); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// MARK: - xpc_session_t (XPC_TYPE_SESSION) (macOS 13+) + +void *xpcSessionCreateMachService(const char *service_name, void *queue, uint64_t flags, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + return xpc_session_create_mach_service( + service_name, + (dispatch_queue_t)queue, + flags, + (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +const char *xpcSessionCopyDescription(void *session) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + return xpc_session_copy_description((xpc_session_t)session); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +bool xpcSessionActivate(void *session, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + return xpc_session_activate((xpc_session_t)session, (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcSessionSetIncomingMessageHandler(void *session, uintptr_t cgo_message_handler) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + xpc_session_set_incoming_message_handler( + (xpc_session_t)session, + ^(xpc_object_t _Nonnull message) { + // Ensure the message is a dictionary. + if (xpc_get_type(message) != XPC_TYPE_DICTIONARY) { + xpc_session_cancel((xpc_session_t)session); + return; + } + xpc_object_t reply = (xpc_object_t)callMessageHandler(cgo_message_handler, wrapRawObject(message)); + xpc_rich_error_t err; + do { + err = xpc_session_send_message(session, reply); + } while (err != nil && xpc_rich_error_can_retry(err)); + xpc_release(reply); + }); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcSessionCancel(void *session) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + xpc_session_cancel((xpc_session_t)session); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcSessionSetCancelHandler(void *session, uintptr_t cgo_cancel_handler) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + xpc_session_set_cancel_handler( + (xpc_session_t)session, + ^(xpc_rich_error_t _Nonnull err) { + callCancelHandler(cgo_cancel_handler, wrapRawObject(err)); + }); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcSessionSendMessageWithReplyAsync(void *session, void *message, uintptr_t cgo_reply_handler) +{ +#ifdef INCLUDE_TARGET_OSX_13 + if (@available(macOS 13, *)) { + xpc_session_send_message_with_reply_async( + (xpc_session_t)session, + (xpc_object_t)message, + ^(xpc_object_t _Nonnull reply, xpc_rich_error_t _Nullable error) { + callReplyHandler(cgo_reply_handler, wrapRawObject(reply), wrapRawObject(error)); + }); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +// MARK: - xpc_rich_error_t (XPC_TYPE_RICH_ERROR) + +bool xpcRichErrorCanRetry(void *err) +{ + return xpc_rich_error_can_retry((xpc_rich_error_t)err); +} + +const char *xpcRichErrorCopyDescription(void *err) +{ + return xpc_rich_error_copy_description((xpc_rich_error_t)err); +} + +// MARK: - Identity + +// # xpc_type_t + +xpc_type_t xpcGetType(void *object) +{ + return xpc_get_type((xpc_object_t)object); +} + +const char *xpcTypeGetName(xpc_type_t type) +{ + return xpc_type_get_name(type); +} + +// MARK: - Comparison +// MARK: - Copying + +// # xpc_object_t +const char *xpcCopyDescription(void *object) +{ + return xpc_copy_description((xpc_object_t)object); +} + +// MARK: - Boolean objects + +// # xpc_object_t (XPC_TYPE_BOOL) + +void *xpcBoolCreate(bool value) +{ + return xpc_bool_create(value); +} + +bool xpcBoolGetValue(void *object) +{ + return xpc_bool_get_value((xpc_object_t)object); +} + +void *xpcBoolTrue() +{ + return XPC_BOOL_TRUE; +} + +void *xpcBoolFalse() +{ + return XPC_BOOL_FALSE; +} + +// MARK: - Data objects + +// # xpc_object_t (XPC_TYPE_DATA) + +void *xpcDataCreate(const void *bytes, size_t length) +{ + return xpc_data_create(bytes, length); +} + +const void *xpcDataGetBytesPtr(void *object) +{ + return xpc_data_get_bytes_ptr((xpc_object_t)object); +} + +size_t xpcDataGetLength(void *object) +{ + return xpc_data_get_length((xpc_object_t)object); +} + +// MARK: - Number objects + +// # xpc_object_t (XPC_TYPE_DOUBLE) +void *xpcDoubleCreate(double value) +{ + return xpc_double_create(value); +} + +double xpcDoubleGetValue(void *object) +{ + return xpc_double_get_value((xpc_object_t)object); +} + +// MARK: - Int64 objects +// # xpc_object_t (XPC_TYPE_INT64) +void *xpcInt64Create(int64_t value) +{ + return xpc_int64_create(value); +} + +int64_t xpcInt64GetValue(void *object) +{ + return xpc_int64_get_value((xpc_object_t)object); +} + +// MARK: - UInt64 objects +// # xpc_object_t (XPC_TYPE_UINT64) +void *xpcUInt64Create(uint64_t value) +{ + return xpc_uint64_create(value); +} + +uint64_t xpcUInt64GetValue(void *object) +{ + return xpc_uint64_get_value((xpc_object_t)object); +} + +// MARK: - Array objects + +// # xpc_object_t (XPC_TYPE_ARRAY) + +void *xpcArrayCreate(void *const *object, size_t count) +{ + return xpc_array_create((xpc_object_t const *)object, count); +} + +void *xpcArrayGetValue(void *object, size_t index) +{ + return xpc_array_get_value((xpc_object_t)object, index); +} + +void xpcArraySetValue(void *object, size_t index, void *value) +{ + xpc_array_set_value((xpc_object_t)object, index, (xpc_object_t)value); +} + +void xpcArrayAppendValue(void *object, void *value) +{ + xpc_array_append_value((xpc_object_t)object, (xpc_object_t)value); +} + +size_t xpcArrayGetCount(void *object) +{ + return xpc_array_get_count((xpc_object_t)object); +} + +bool xpcArrayApply(void *object, uintptr_t cgo_applier) +{ + return xpc_array_apply((xpc_object_t)object, ^bool(size_t index, xpc_object_t _Nonnull value) { + return callArrayApplier(cgo_applier, index, wrapRawObject(value)); + }); +} + +int xpcArrayDupFd(void *object, size_t index) +{ + return xpc_array_dup_fd((xpc_object_t)object, index); +} + +void *xpcArrayGetArray(void *object, size_t index) +{ + return xpc_array_get_array((xpc_object_t)object, index); +} + +bool xpcArrayGetBool(void *object, size_t index) +{ + return xpc_array_get_bool((xpc_object_t)object, index); +} + +const void *xpcArrayGetData(void *object, size_t index, size_t *length) +{ + return xpc_array_get_data((xpc_object_t)object, index, length); +} + +int64_t xpcArrayGetDate(void *object, size_t index) +{ + return xpc_array_get_date((xpc_object_t)object, index); +} + +void *xpcArrayGetDictionary(void *object, size_t index) +{ + return xpc_array_get_dictionary((xpc_object_t)object, index); +} + +double xpcArrayGetDouble(void *object, size_t index) +{ + return xpc_array_get_double((xpc_object_t)object, index); +} + +int64_t xpcArrayGetInt64(void *object, size_t index) +{ + return xpc_array_get_int64((xpc_object_t)object, index); +} + +const char *xpcArrayGetString(void *object, size_t index) +{ + return xpc_array_get_string((xpc_object_t)object, index); +} + +uint64_t xpcArrayGetUInt64(void *object, size_t index) +{ + return xpc_array_get_uint64((xpc_object_t)object, index); +} + +const uint8_t *xpcArrayGetUUID(void *object, size_t index) +{ + return xpc_array_get_uuid((xpc_object_t)object, index); +} + +void xpcArraySetBool(void *object, size_t index, bool value) +{ + xpc_array_set_bool((xpc_object_t)object, index, value); +} + +void xpcArraySetData(void *object, size_t index, const void *bytes, size_t length) +{ + xpc_array_set_data((xpc_object_t)object, index, bytes, length); +} + +void xpcArraySetDate(void *object, size_t index, int64_t value) +{ + xpc_array_set_date((xpc_object_t)object, index, value); +} + +void xpcArraySetDouble(void *object, size_t index, double value) +{ + xpc_array_set_double((xpc_object_t)object, index, value); +} + +void xpcArraySetFd(void *object, size_t index, int fd) +{ + xpc_array_set_fd((xpc_object_t)object, index, fd); +} + +void xpcArraySetInt64(void *object, size_t index, int64_t value) +{ + xpc_array_set_int64((xpc_object_t)object, index, value); +} + +void xpcArraySetString(void *object, size_t index, const char *value) +{ + xpc_array_set_string((xpc_object_t)object, index, value); +} + +void xpcArraySetUInt64(void *object, size_t index, uint64_t value) +{ + xpc_array_set_uint64((xpc_object_t)object, index, value); +} + +void xpcArraySetUUID(void *object, size_t index, const uint8_t *uuid) +{ + xpc_array_set_uuid((xpc_object_t)object, index, uuid); +} + +// MARK: - Dictionary objects + +// xpc_object_t (XPC_TYPE_DICTIONARY) + +void *xpcDictionaryCreateEmpty(void) +{ + return xpc_dictionary_create_empty(); +} + +void *xpcDictionaryCreateReply(void *object) +{ + return xpc_dictionary_create_reply((xpc_object_t)object); +} + +void xpcDictionarySetValue(void *object, const char *key, void *value) +{ + xpc_dictionary_set_value((xpc_object_t)object, key, (xpc_object_t)value); +} + +size_t xpcDictionaryGetCount(void *object) +{ + return xpc_dictionary_get_count((xpc_object_t)object); +} + +void *xpcDictionaryGetValue(void *object, const char *key) +{ + return xpc_dictionary_get_value((xpc_object_t)object, key); +} + +bool xpcDictionaryApply(void *object, uintptr_t cgo_applier) +{ + return xpc_dictionary_apply((xpc_object_t)object, ^bool(const char *_Nonnull key, xpc_object_t _Nonnull value) { + return callDictionaryApplier(cgo_applier, key, wrapRawObject(value)); + }); +} + +int xpcDictionaryDupFd(void *object, const char *key) +{ + return xpc_dictionary_dup_fd((xpc_object_t)object, key); +} + +void *xpcDictionaryGetArray(void *object, const char *key) +{ + return xpc_dictionary_get_array((xpc_object_t)object, key); +} + +bool xpcDictionaryGetBool(void *object, const char *key) +{ + return xpc_dictionary_get_bool((xpc_object_t)object, key); +} + +const void *xpcDictionaryGetData(void *object, const char *key, size_t *length) +{ + return xpc_dictionary_get_data((xpc_object_t)object, key, length); +} + +int64_t xpcDictionaryGetDate(void *object, const char *key) +{ + return xpc_dictionary_get_date((xpc_object_t)object, key); +} + +void *xpcDictionaryGetDictionary(void *object, const char *key) +{ + return xpc_dictionary_get_dictionary((xpc_object_t)object, key); +} + +double xpcDictionaryGetDouble(void *object, const char *key) +{ + return xpc_dictionary_get_double((xpc_object_t)object, key); +} + +int64_t xpcDictionaryGetInt64(void *object, const char *key) +{ + return xpc_dictionary_get_int64((xpc_object_t)object, key); +} + +const char *xpcDictionaryGetString(void *object, const char *key) +{ + return xpc_dictionary_get_string((xpc_object_t)object, key); +} + +uint64_t xpcDictionaryGetUInt64(void *object, const char *key) +{ + return xpc_dictionary_get_uint64((xpc_object_t)object, key); +} + +const uint8_t *xpcDictionaryGetUUID(void *object, const char *key) +{ + return xpc_dictionary_get_uuid((xpc_object_t)object, key); +} + +void xpcDictionarySetBool(void *object, const char *key, bool value) +{ + xpc_dictionary_set_bool((xpc_object_t)object, key, value); +} + +void xpcDictionarySetData(void *object, const char *key, const void *bytes, size_t length) +{ + xpc_dictionary_set_data((xpc_object_t)object, key, bytes, length); +} + +void xpcDictionarySetDate(void *object, const char *key, int64_t value) +{ + xpc_dictionary_set_date((xpc_object_t)object, key, value); +} + +void xpcDictionarySetDouble(void *object, const char *key, double value) +{ + xpc_dictionary_set_double((xpc_object_t)object, key, value); +} + +void xpcDictionarySetFd(void *object, const char *key, int fd) +{ + xpc_dictionary_set_fd((xpc_object_t)object, key, fd); +} + +void xpcDictionarySetInt64(void *object, const char *key, int64_t value) +{ + xpc_dictionary_set_int64((xpc_object_t)object, key, value); +} + +void xpcDictionarySetString(void *object, const char *key, const char *value) +{ + xpc_dictionary_set_string((xpc_object_t)object, key, value); +} + +void xpcDictionarySetUInt64(void *object, const char *key, uint64_t value) +{ + xpc_dictionary_set_uint64((xpc_object_t)object, key, value); +} + +void xpcDictionarySetUUID(void *object, const char *key, const uint8_t *uuid) +{ + xpc_dictionary_set_uuid((xpc_object_t)object, key, uuid); +} + +// MARK: - String objects + +void *xpcStringCreate(const char *string) +{ + return xpc_string_create(string); +} + +size_t xpcStringGetLength(void *object) +{ + return xpc_string_get_length((xpc_object_t)object); +} + +const char *xpcStringGetStringPtr(void *object) +{ + return xpc_string_get_string_ptr((xpc_object_t)object); +} + +// MARK: - File descriptor objects + +void *xpcFdCreate(int fd) +{ + return xpc_fd_create(fd); +} + +int xpcFdDup(void *object) +{ + return xpc_fd_dup((xpc_object_t)object); +} + +// MARK: - Date objects + +void *xpcDateCreate(int64_t interval) +{ + return xpc_date_create(interval); +} + +void *xpcDateCreateFromCurrent() +{ + return xpc_date_create_from_current(); +} + +int64_t xpcDateGetValue(void *object) +{ + return xpc_date_get_value((xpc_object_t)object); +} + +// MARK: - UUID objects + +void *xpcUUIDCreate(const uuid_t uuid) +{ + return xpc_uuid_create(uuid); +} + +const uint8_t *xpcUUIDGetBytes(void *object) +{ + return xpc_uuid_get_bytes((xpc_object_t)object); +} + +// MARK: - Shared memory objects +// MARK: - Null objects + +void *xpcNullCreate() +{ + return xpc_null_create(); +} + +// MARK: - Object life cycle + +// xpc_object_t + +void *xpcRetain(void *object) +{ + return xpc_retain((xpc_object_t)object); +} + +void xpcRelease(void *object) +{ + xpc_release((xpc_object_t)object); +} + +// MARK: - xpc_peer_requirement_t (macOS 26+) + +void xpcListenerSetPeerRequirement(void *listener, void *peer_requirement) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + xpc_listener_set_peer_requirement((xpc_listener_t)listener, (xpc_peer_requirement_t)peer_requirement); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void *xpcPeerRequirementCreateLwcr(void *lwcr, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return xpc_peer_requirement_create_lwcr((xpc_object_t)lwcr, (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +bool xpcPeerRequirementMatchReceivedMessage(void *peer_requirement, void *message, void **error_out) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + return xpc_peer_requirement_match_received_message( + (xpc_peer_requirement_t)peer_requirement, + (xpc_object_t)message, + (xpc_rich_error_t *)error_out); + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +void xpcSessionSetPeerRequirement(void *session, void *peer_requirement) +{ +#ifdef INCLUDE_TARGET_OSX_26 + if (@available(macOS 26, *)) { + xpc_session_set_peer_requirement((xpc_session_t)session, (xpc_peer_requirement_t)peer_requirement); + return; + } +#endif + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} \ No newline at end of file diff --git a/xpc/xpc_object.go b/xpc/xpc_object.go new file mode 100644 index 00000000..8dde1070 --- /dev/null +++ b/xpc/xpc_object.go @@ -0,0 +1,64 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc_darwin.h" +*/ +import "C" +import ( + "runtime" + "unsafe" + + "github.com/Code-Hex/vz/v3/internal/objc" +) + +type pointer = objc.Pointer + +// xpcObject wraps an XPC object ([xpc_object_t]). +// It is expected to be embedded in other structs as pointer to provide common functionality. +// +// [xpc_object_t]: https://developer.apple.com/documentation/xpc/xpc_object_t?language=objc +type xpcObject struct { + *pointer +} + +var _ Object = (*xpcObject)(nil) + +// newXpcObject creates a new [xpcObject] from an existing xpc_object_t. +func newXpcObject(ptr unsafe.Pointer) *xpcObject { + return &xpcObject{objc.NewPointer(ptr)} +} + +// String returns the description of the [xpcObject]. +// - https://developer.apple.com/documentation/xpc/xpc_copy_description(_:)?language=objc +func (x *xpcObject) String() string { + cs := C.xpcCopyDescription(objc.Ptr(x)) + defer C.free(unsafe.Pointer(cs)) + return C.GoString(cs) +} + +// retain retains the [xpcObject]. +// - https://developer.apple.com/documentation/xpc/xpc_retain?language=objc +func (x *xpcObject) retain() { + C.xpcRetain(objc.Ptr(x)) +} + +// releaseOnCleanup registers a cleanup function to release the [xpcObject] when cleaned up. +// - https://developer.apple.com/documentation/xpc/xpc_release?language=objc +func (x *xpcObject) releaseOnCleanup() { + runtime.AddCleanup(x, func(p unsafe.Pointer) { + C.xpcRelease(p) + }, objc.Ptr(x)) +} + +// Retain calls retain method on the given object and returns it. +func Retain[T interface{ retain() }](o T) T { + o.retain() + return o +} + +// ReleaseOnCleanup calls releaseOnCleanup method on the given object and returns it. +func ReleaseOnCleanup[T interface{ releaseOnCleanup() }](o T) T { + o.releaseOnCleanup() + return o +} diff --git a/xpc/xpc_test.go b/xpc/xpc_test.go new file mode 100644 index 00000000..43a4aa03 --- /dev/null +++ b/xpc/xpc_test.go @@ -0,0 +1,213 @@ +package xpc_test + +import ( + "bytes" + "context" + "flag" + "fmt" + "log" + "os" + "os/exec" + "os/signal" + "path" + "runtime" + "strings" + "syscall" + "testing" + "text/template" + + "github.com/Code-Hex/vz/v3/internal/osversion" + "github.com/Code-Hex/vz/v3/xpc" +) + +var macOSAvailable = osversion.MacOSAvailable + +var server bool + +func init() { + // Determine if running as server or client based on command-line arguments + flag.BoolVar(&server, "server", false, "run as mach service server") +} + +func TestMachService(t *testing.T) { + if err := macOSAvailable(14); err != nil { + t.Skip("xpc listener is supported from macOS 14") + } + + label := "dev.code-hex.vz.xpc.test" + machServiceName := label + ".greeting" + + if server { + t.Log("running as mach service server") + listener, err := xpcGreetingServer(t, machServiceName) + if err != nil { + log.Printf("failed to create mach service server: %v", err) + t.Fatal(err) + } + if err := listener.Activate(); err != nil { + log.Printf("failed to activate mach service server: %v", err) + t.Fatal(err) + } + ctx, stop := signal.NotifyContext(t.Context(), os.Interrupt, syscall.SIGTERM) + defer stop() + <-ctx.Done() + _ = listener.Close() + } else { + t.Log("running as mach service client") + xpcRegisterMachService(t, label, machServiceName) + greeting := "Hello, Mach Service!" + greetingReply, err := xpcClientRequestingGreeting(t, machServiceName, greeting) + if err != nil { + t.Fatal(err) + } + if greetingReply != greeting { + t.Fatalf("expected greeting reply %q to equal greeting %q", greetingReply, greeting) + } + } +} + +// xpcGreetingServer creates an Mach service XPC listener that echoes back greetings. +func xpcGreetingServer(t *testing.T, machServiceName string) (*xpc.Listener, error) { + return xpc.NewListener( + machServiceName, + xpc.Accept( + xpc.MessageHandler(func(dic *xpc.Dictionary) *xpc.Dictionary { + createErrorReply := func(errMsg string, args ...any) *xpc.Dictionary { + errorString := fmt.Sprintf(errMsg, args...) + log.Print(errorString) + t.Error(errorString) + return dic.CreateReply( + xpc.KeyValue("Error", xpc.NewString(errorString)), + ) + } + var reply *xpc.Dictionary + if greeting := dic.GetString("Greeting"); greeting == "" { + reply = createErrorReply("missing greeting in request") + } else { + reply = dic.CreateReply( + xpc.KeyValue("Greeting", xpc.NewString(greeting)), + ) + } + return reply + }), + ), + ) +} + +const launchdPlistTemplate = ` + + + + Label + {{.Label}} + ProgramArguments + + {{- range $arg := .ProgramArguments}} + {{$arg}} + {{- end}} + + RunAtLoad + + WorkingDirectory + {{ .WorkingDirectory }} + StandardErrorPath + {{ .WorkingDirectory }}/xpc_test.stderr.log + + MachServices + + {{- range $service := .MachServices}} + {{$service}} + + {{- end}} + + +` + +// xpcRegisterMachService registers the test executable as a Mach service +// using launchctl with the given label and machServiceName. +func xpcRegisterMachService(t *testing.T, label, machServiceName string) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + params := struct { + Label string + ProgramArguments []string + WorkingDirectory string + MachServices []string + }{ + Label: label, + ProgramArguments: []string{os.Args[0], "-test.run", "^" + funcName(t, 2) + "$", "-server"}, + WorkingDirectory: cwd, + MachServices: []string{machServiceName}, + } + template, err := template.New("plist").Parse(launchdPlistTemplate) + if err != nil { + t.Fatal(err) + } + var b bytes.Buffer + if err := template.Execute(&b, params); err != nil { + t.Fatal(err) + } + userHomeDir, err := os.UserHomeDir() + if err != nil { + t.Fatal(err) + } + launchAgentDir := path.Join(userHomeDir, "Library", "LaunchAgents", label+".plist") + if err := os.WriteFile(launchAgentDir, b.Bytes(), 0o644); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.Remove(launchAgentDir); err != nil { + t.Logf("failed to remove launch agent plist: %v", err) + } + }) + cmd := exec.CommandContext(t.Context(), "launchctl", "load", launchAgentDir) + if err := cmd.Run(); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + // do not use t.Context() here to ensure unload runs + cmd := exec.CommandContext(context.Background(), "launchctl", "unload", launchAgentDir) + if err := cmd.Run(); err != nil { + t.Logf("failed to unload launch agent: %v", err) + } + }) +} + +// funcName returns the name of the calling function. +// It is used to get the test function name for launchctl registration. +func funcName(t *testing.T, skip int) string { + pc, _, _, ok := runtime.Caller(skip) + if !ok { + t.Fatal("failed to get caller info") + } + funcNameComponents := strings.Split(runtime.FuncForPC(pc).Name(), ".") + return funcNameComponents[len(funcNameComponents)-1] +} + +// xpcClientRequestingGreeting requests a VmnetNetwork serialization for the given subnet +// from the Mach service and returns the deserialized VmnetNetwork instance. +func xpcClientRequestingGreeting(t *testing.T, machServiceName, greeting string) (string, error) { + session, err := xpc.NewSession( + machServiceName, + ) + if err != nil { + return "", err + } + defer session.Cancel() + + resp, err := session.SendDictionaryWithReply( + t.Context(), xpc.KeyValue("Greeting", xpc.NewString(greeting)), + ) + if err != nil { + return "", err + } + errorStr := resp.GetString("Error") + if errorStr != "" { + return "", fmt.Errorf("xpc service error: %s", errorStr) + } + greetingReply := resp.GetString("Greeting") + return greetingReply, nil +}