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/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/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..2b629569 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 ( @@ -12,6 +13,7 @@ import ( "net" "os" "syscall" + "unsafe" "github.com/Code-Hex/vz/v3/internal/objc" ) @@ -260,6 +262,50 @@ 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() unsafe.Pointer { + return C.VZVmnetNetworkDeviceAttachment_network(objc.Ptr(v)) +} + +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 unsafe.Pointer) (*VmnetNetworkDeviceAttachment, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + attachment := &VmnetNetworkDeviceAttachment{ + pointer: objc.NewPointer( + C.newVZVmnetNetworkDeviceAttachment(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/vmnet_darwin.go b/vmnet/vmnet_darwin.go new file mode 100644 index 00000000..a3fdb10d --- /dev/null +++ b/vmnet/vmnet_darwin.go @@ -0,0 +1,458 @@ +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" + "unsafe" + + "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 + +// object +type object struct { + p unsafe.Pointer +} + +// Raw returns the raw xpc_object_t as [unsafe.Pointer]. +func (o *object) Raw() unsafe.Pointer { + return o.p +} + +// 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) + }, o.p) +} + +// 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 is configuration for the [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_configuration_create(_:_:)?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. +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{p: 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( + c.Raw(), + &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( + c.Raw(), + 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(c.Raw()) +} + +// 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(c.Raw()) +} + +// 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(c.Raw()) +} + +// 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(c.Raw()) +} + +// 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(c.Raw()) +} + +// 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( + c.Raw(), + 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( + c.Raw(), + &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( + c.Raw(), + &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( + c.Raw(), + 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 [Network]. +// - https://developer.apple.com/documentation/vmnet/vmnet_network_create(_:_:)?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. +func NewNetwork(config *NetworkConfiguration) (*Network, error) { + if err := macOSAvailable(26); err != nil { + return nil, err + } + + var status Return + ptr := C.VmnetNetworkCreate( + config.Raw(), + (*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{p: 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{p: ptr}} + ReleaseOnCleanup(network) + return network, nil +} + +// 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 +func (n *Network) CopySerialization() (xpc.Object, error) { + var status Return + ptr := C.VmnetNetwork_copySerialization( + n.Raw(), + (*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(n.Raw(), &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(n.Raw(), &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) +} diff --git a/vmnet/vmnet_darwin.h b/vmnet/vmnet_darwin.h new file mode 100644 index 00000000..d4083245 --- /dev/null +++ b/vmnet/vmnet_darwin.h @@ -0,0 +1,36 @@ +#pragma once + +#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: - CFRelease Wrapper + +void vmnetRelease(void *obj); + +// MARK: - vmnet_network_configuration_t (macOS 26+) + +// VmnetNetworkConfiguration +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+) + +// vmnet_network +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); diff --git a/vmnet/vmnet_darwin.m b/vmnet/vmnet_darwin.m new file mode 100644 index 00000000..54cdd340 --- /dev/null +++ b/vmnet/vmnet_darwin.m @@ -0,0 +1,209 @@ +#import "vmnet_darwin.h" + +// MARK: - CFRelease Wrapper + +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(); +} diff --git a/vmnet_test.go b/vmnet_test.go new file mode 100644 index 00000000..cb0d90a9 --- /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.Raw()) + 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..e40399ab --- /dev/null +++ b/xpc/array.go @@ -0,0 +1,261 @@ +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/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 := unwrapHandler[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) int { + return int(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 int) { + 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/cgo_handle.go b/xpc/cgo_handle.go new file mode 100644 index 00000000..96f8de09 --- /dev/null +++ b/xpc/cgo_handle.go @@ -0,0 +1,44 @@ +package xpc + +/* +#cgo darwin CFLAGS: -mmacosx-version-min=11 -x objective-c -fno-objc-arc +# include "xpc_darwin.h" +*/ +import "C" +import ( + "runtime" + "runtime/cgo" +) + +// cgoHandler holds a cgo.Handle for an Object. +// It provides methods to hold and release the handle. +// handle will released when cgoHandler.release is called. +type cgoHandler struct { + handle cgo.Handle +} + +// releaseOnCleanup registers a cleanup function to delete the cgo.Handle when cleaned up. +func (h *cgoHandler) releaseOnCleanup() { + runtime.AddCleanup(h, func(h cgo.Handle) { + h.Delete() + }, h.handle) +} + +// newCgoHandler creates a new cgoHandler and holds the given value. +func newCgoHandler(v any) (*cgoHandler, C.uintptr_t) { + if v == nil { + return nil, 0 + } + h := &cgoHandler{cgo.NewHandle(v)} + return ReleaseOnCleanup(h), C.uintptr_t(h.handle) +} + +// unwrapHandler 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 cgoHandler or caller. +func unwrapHandler[T any](handle uintptr) T { + if handle == 0 { + var zero T + return zero + } + return cgo.Handle(handle).Value().(T) +} diff --git a/xpc/dictionary.go b/xpc/dictionary.go new file mode 100644 index 00000000..cf17dc3d --- /dev/null +++ b/xpc/dictionary.go @@ -0,0 +1,368 @@ +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/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 := unwrapHandler[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) int { + cKey := C.CString(key) + defer C.free(unsafe.Pointer(cKey)) + return int(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 int) { + 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..ccd07da1 --- /dev/null +++ b/xpc/listener.go @@ -0,0 +1,99 @@ +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" +) + +// 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 +} + +// 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 := newCgoHandler(handler) + var err_out unsafe.Pointer + ptr := C.xpcListenerCreate(cname, q, C.XPC_LISTENER_CREATE_INACTIVE, 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 := unwrapHandler[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..8a60325c --- /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 int) 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() int { + return int(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..47b17a81 --- /dev/null +++ b/xpc/session.go @@ -0,0 +1,226 @@ +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/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 + incomingMessageHandler *cgoHandler +} + +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 := unwrapHandler[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 := newCgoHandler(handler) + C.xpcSessionSetIncomingMessageHandler(objc.Ptr(s), 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 := unwrapHandler[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 := newCgoHandler((CancellationHandler)(func(err *RichError) { + if handler != nil { + handler(err) + } + s.handleCancellation(err) + })) + C.xpcSessionSetCancelHandler(objc.Ptr(s), 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 := unwrapHandler[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 <- Retain(err) + } else { + replyCh <- 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..55d0f43c --- /dev/null +++ b/xpc/xpc_object.go @@ -0,0 +1,66 @@ +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]. +// It also uses [ReleaseOnCleanup] to ensure it is released later. +// - https://developer.apple.com/documentation/xpc/xpc_retain?language=objc +func (x *xpcObject) retain() { + C.xpcRetain(objc.Ptr(x)) + _ = ReleaseOnCleanup(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 +}