From 29bc2df89ebf5c391a0c5ff9c5b7bed097a01f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Amaro=20Lagedo?= Date: Sat, 23 May 2026 17:46:34 -0300 Subject: [PATCH 1/2] feat: expose runtime directory-sharing devices + live share swap Add VZVirtualMachine.directorySharingDevices() and a runtime VirtioFileSystemDevice type with SetShare(), so a host can change which directories a guest sees on a running VM (macOS 12+) without recreating it. Apple supports this (VZVirtioFileSystemDevice.share is get/set on the runtime device, reachable via VZVirtualMachine.directorySharingDevices), but the binding only exposed the config-time setter. Mirrors the existing SocketDevices() pattern: NSArray -> ToPointerSlice -> wrap each pointer with the VM's serial dispatch queue. SetShare runs the mutation on that queue, as the framework requires. Co-Authored-By: Claude Opus 4.7 (1M context) --- shared_directory.go | 53 +++++++++++++++++++++++++++++++++++++++++++++ virtualization_12.h | 3 +++ virtualization_12.m | 33 ++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/shared_directory.go b/shared_directory.go index 8b628890..42d313d6 100644 --- a/shared_directory.go +++ b/shared_directory.go @@ -10,6 +10,7 @@ package vz import "C" import ( "os" + "unsafe" "github.com/Code-Hex/vz/v3/internal/objc" ) @@ -182,3 +183,55 @@ func MacOSGuestAutomountTag() (string, error) { cstring := (*char)(C.getMacOSGuestAutomountTag()) return cstring.String(), nil } + +// VirtioFileSystemDevice is a runtime Virtio file system device obtained from a running +// VirtualMachine via DirectorySharingDevices. Unlike VirtioFileSystemDeviceConfiguration +// (a configuration object set before start), this is the live device: its directory share +// can be swapped while the VM runs (macOS 12+), so the host can add or remove the +// directories a guest sees without recreating the VM. +// +// Don't create a VirtioFileSystemDevice directly. Request a file system device in your +// configuration and obtain the running instance via VirtualMachine.DirectorySharingDevices. +// +// see: https://developer.apple.com/documentation/virtualization/vzvirtiofilesystemdevice?language=objc +type VirtioFileSystemDevice struct { + dispatchQueue unsafe.Pointer + *pointer +} + +func newVirtioFileSystemDevice(ptr, dispatchQueue unsafe.Pointer) *VirtioFileSystemDevice { + return &VirtioFileSystemDevice{ + dispatchQueue: dispatchQueue, + pointer: objc.NewPointer(ptr), + } +} + +// SetShare swaps the directory share on this running device. The mutation runs on the +// VM's serial dispatch queue, as the framework requires for every VZVirtualMachine call. +// +// see: https://developer.apple.com/documentation/virtualization/vzvirtiofilesystemdevice/share?language=objc +func (d *VirtioFileSystemDevice) SetShare(share DirectoryShare) { + C.setShareVZVirtioFileSystemDevice(objc.Ptr(d), objc.Ptr(share), d.dispatchQueue) +} + +// DirectorySharingDevices returns the list of directory sharing devices configured on this +// running virtual machine, or an empty slice if none is configured. Since vz only creates +// VirtioFileSystemDevices, every element is one. +// +// This is only supported on macOS 12 and newer; nil is returned on older versions. +// +// see: https://developer.apple.com/documentation/virtualization/vzvirtualmachine/directorysharingdevices?language=objc +func (v *VirtualMachine) DirectorySharingDevices() []*VirtioFileSystemDevice { + if err := macOSAvailable(12); err != nil { + return nil + } + nsArray := objc.NewNSArray( + C.VZVirtualMachine_directorySharingDevices(objc.Ptr(v)), + ) + ptrs := nsArray.ToPointerSlice() + devices := make([]*VirtioFileSystemDevice, len(ptrs)) + for i, ptr := range ptrs { + devices[i] = newVirtioFileSystemDevice(ptr, v.dispatchQueue) + } + return devices +} diff --git a/virtualization_12.h b/virtualization_12.h index dd679a7f..322b3e21 100644 --- a/virtualization_12.h +++ b/virtualization_12.h @@ -33,6 +33,9 @@ void *newVZMultipleDirectoryShare(void *sharedDirectories); void *newVZVirtioFileSystemDeviceConfiguration(const char *tag, void **error); void setVZVirtioFileSystemDeviceConfigurationShare(void *config, void *share); +void *VZVirtualMachine_directorySharingDevices(void *machine); +void setShareVZVirtioFileSystemDevice(void *device, void *share, void *queue); + void setDirectorySharingDevicesVZVirtualMachineConfiguration(void *config, void *directorySharingDevices); void setPlatformVZVirtualMachineConfiguration(void *config, void *platform); diff --git a/virtualization_12.m b/virtualization_12.m index 1ab6b524..c8d7f925 100644 --- a/virtualization_12.m +++ b/virtualization_12.m @@ -59,6 +59,39 @@ void setDirectorySharingDevicesVZVirtualMachineConfiguration(void *config, void RAISE_UNSUPPORTED_MACOS_EXCEPTION(); } +/*! + @abstract Return the list of directory sharing devices configured on this virtual machine. Return an empty array if none is configured. + @see VZDirectorySharingDevice + @see VZVirtualMachineConfiguration + */ +void *VZVirtualMachine_directorySharingDevices(void *machine) +{ + if (@available(macOS 12, *)) { + return [(VZVirtualMachine *)machine directorySharingDevices]; // NSArray + } + + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +/*! + @abstract Swap the directory share on a running VZVirtioFileSystemDevice. + @discussion + VZVirtioFileSystemDevice.share is get/set on the runtime device (macOS 12+), so the + host can change which directories a guest sees without recreating the VM. The mutation + must run on the VM's serial dispatch queue, like every other VZVirtualMachine interaction. + */ +void setShareVZVirtioFileSystemDevice(void *device, void *share, void *queue) +{ + if (@available(macOS 12, *)) { + dispatch_sync((dispatch_queue_t)queue, ^{ + [(VZVirtioFileSystemDevice *)device setShare:(VZDirectoryShare *)share]; + }); + return; + } + + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + /*! @abstract The hardware platform to use. @discussion From 913d0435574da896eced36bf48cebd7a879283fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Amaro=20Lagedo?= Date: Sat, 23 May 2026 18:12:21 -0300 Subject: [PATCH 2/2] feat: add VirtioFileSystemDevice.Share getter with runtime tests Expose a Share() getter on the running file system device so the directory share set via SetShare (or at configuration time) can be read back, restoring get/set symmetry with the VZVirtioFileSystemDevice.share property. The getter reads on the VM's serial dispatch queue and reconstructs the concrete *SingleDirectoryShare or *MultipleDirectoryShare via Obj-C class introspection. Add integration tests covering the runtime directory-sharing API: - TestDirectorySharingDevices verifies a configured device is returned. - TestVirtioFileSystemDeviceSetShare boots a VM, asserts the reported share type, hot-swaps single -> multiple share at runtime, and verifies the guest sees the swapped-in content after remount. Co-Authored-By: Claude Opus 4.7 (1M context) --- shared_directory.go | 25 ++++++ shared_directory_test.go | 159 +++++++++++++++++++++++++++++++++++++++ virtualization_12.h | 2 + virtualization_12.m | 35 +++++++++ 4 files changed, 221 insertions(+) diff --git a/shared_directory.go b/shared_directory.go index 42d313d6..795b9343 100644 --- a/shared_directory.go +++ b/shared_directory.go @@ -214,6 +214,31 @@ func (d *VirtioFileSystemDevice) SetShare(share DirectoryShare) { C.setShareVZVirtioFileSystemDevice(objc.Ptr(d), objc.Ptr(share), d.dispatchQueue) } +// Share returns the directory share currently set on this running device, or nil if none +// is set. The concrete type is *SingleDirectoryShare or *MultipleDirectoryShare, matching +// what was last passed to SetShare or set in the configuration. The read runs on the VM's +// serial dispatch queue. +// +// see: https://developer.apple.com/documentation/virtualization/vzvirtiofilesystemdevice/share?language=objc +func (d *VirtioFileSystemDevice) Share() DirectoryShare { + ptr := C.getShareVZVirtioFileSystemDevice(objc.Ptr(d), d.dispatchQueue) + if ptr == nil { + return nil + } + if bool(C.isMultipleDirectoryShare(ptr)) { + share := &MultipleDirectoryShare{pointer: objc.NewPointer(ptr)} + objc.SetFinalizer(share, func(self *MultipleDirectoryShare) { + objc.Release(self) + }) + return share + } + share := &SingleDirectoryShare{pointer: objc.NewPointer(ptr)} + objc.SetFinalizer(share, func(self *SingleDirectoryShare) { + objc.Release(self) + }) + return share +} + // DirectorySharingDevices returns the list of directory sharing devices configured on this // running virtual machine, or an empty slice if none is configured. Since vz only creates // VirtioFileSystemDevices, every element is one. diff --git a/shared_directory_test.go b/shared_directory_test.go index ed02464a..3b2bf4ea 100644 --- a/shared_directory_test.go +++ b/shared_directory_test.go @@ -139,6 +139,165 @@ func TestSingleDirectoryShare(t *testing.T) { } } +func TestDirectorySharingDevices(t *testing.T) { + if vz.Available(12) { + t.Skip("VirtioFileSystemDevice is supported from macOS 12") + } + + bootLoader, err := vz.NewLinuxBootLoader( + "./testdata/Image", + vz.WithCommandLine("console=hvc0"), + ) + if err != nil { + t.Fatalf("failed to create boot loader: %v", err) + } + + config, err := vz.NewVirtualMachineConfiguration(bootLoader, 1, 256*1024*1024) + if err != nil { + t.Fatalf("failed to create virtual machine configuration: %v", err) + } + + sharedDirectory, err := vz.NewSharedDirectory(t.TempDir(), false) + if err != nil { + t.Fatal(err) + } + single, err := vz.NewSingleDirectoryShare(sharedDirectory) + if err != nil { + t.Fatal(err) + } + fsConfig, err := vz.NewVirtioFileSystemDeviceConfiguration("share") + if err != nil { + t.Fatal(err) + } + fsConfig.SetDirectoryShare(single) + config.SetDirectorySharingDevicesVirtualMachineConfiguration( + []vz.DirectorySharingDeviceConfiguration{fsConfig}, + ) + + vm, err := vz.NewVirtualMachine(config) + if err != nil { + t.Fatalf("failed to create virtual machine: %v", err) + } + + devices := vm.DirectorySharingDevices() + if len(devices) != 1 { + t.Fatalf("expected 1 directory sharing device, got %d", len(devices)) + } + if devices[0] == nil { + t.Fatal("directory sharing device should not be nil") + } +} + +func TestVirtioFileSystemDeviceSetShare(t *testing.T) { + if vz.Available(12) { + t.Skip("VirtioFileSystemDevice is supported from macOS 12") + } + + const tag = "swap" + + // Initial share: a single directory exposing fileA. + dirA := t.TempDir() + fileA := "a.txt" + if f, err := os.Create(filepath.Join(dirA, fileA)); err != nil { + t.Fatal(err) + } else { + f.Close() + } + sharedA, err := vz.NewSharedDirectory(dirA, false) + if err != nil { + t.Fatal(err) + } + single, err := vz.NewSingleDirectoryShare(sharedA) + if err != nil { + t.Fatal(err) + } + fsConfig, err := vz.NewVirtioFileSystemDeviceConfiguration(tag) + if err != nil { + t.Fatal(err) + } + fsConfig.SetDirectoryShare(single) + + container := newVirtualizationMachine(t, + func(vmc *vz.VirtualMachineConfiguration) error { + vmc.SetDirectorySharingDevicesVirtualMachineConfiguration( + []vz.DirectorySharingDeviceConfiguration{fsConfig}, + ) + return nil + }, + ) + t.Cleanup(func() { + if err := container.Shutdown(); err != nil { + log.Println(err) + } + }) + + run := func(cmd string, wantErr bool) { + t.Helper() + session := container.NewSession(t) + defer session.Close() + var buf bytes.Buffer + session.Stderr = &buf + err := session.Run(cmd) + switch { + case err != nil && !wantErr: + t.Fatalf("failed to run command %q: %v\nstderr: %q", cmd, err, buf) + case err == nil && wantErr: + t.Fatalf("expected command %q to fail but it succeeded", cmd) + } + } + + devices := container.DirectorySharingDevices() + if len(devices) != 1 { + t.Fatalf("expected 1 directory sharing device, got %d", len(devices)) + } + device := devices[0] + + // The running device reports the single share it was configured with. + if got := device.Share(); got == nil { + t.Fatal("expected a share on the running device, got nil") + } else if _, ok := got.(*vz.SingleDirectoryShare); !ok { + t.Fatalf("expected *vz.SingleDirectoryShare, got %T", got) + } + + // The guest sees fileA through the initial share. + run("mkdir -p /mnt/shared", false) + run(fmt.Sprintf("mount -t virtiofs %s /mnt/shared", tag), false) + run("ls /mnt/shared/"+fileA, false) + + // Swap to a multiple share exposing a different directory under "sub". + dirB := t.TempDir() + fileB := "b.txt" + if f, err := os.Create(filepath.Join(dirB, fileB)); err != nil { + t.Fatal(err) + } else { + f.Close() + } + sharedB, err := vz.NewSharedDirectory(dirB, false) + if err != nil { + t.Fatal(err) + } + multiple, err := vz.NewMultipleDirectoryShare(map[string]*vz.SharedDirectory{ + "sub": sharedB, + }) + if err != nil { + t.Fatal(err) + } + device.SetShare(multiple) + + // The running device now reports the swapped-in multiple share. + if got := device.Share(); got == nil { + t.Fatal("expected a share after swap, got nil") + } else if _, ok := got.(*vz.MultipleDirectoryShare); !ok { + t.Fatalf("expected *vz.MultipleDirectoryShare after swap, got %T", got) + } + + // After remounting, the guest sees the swapped-in content and no longer fileA. + run("umount /mnt/shared", false) + run(fmt.Sprintf("mount -t virtiofs %s /mnt/shared", tag), false) + run("ls /mnt/shared/sub/"+fileB, false) + run("ls /mnt/shared/"+fileA, true) +} + func TestMultipleDirectoryShare(t *testing.T) { if vz.Available(12) { t.Skip("MultipleDirectoryShare is supported from macOS 12") diff --git a/virtualization_12.h b/virtualization_12.h index 322b3e21..27efb1ce 100644 --- a/virtualization_12.h +++ b/virtualization_12.h @@ -35,6 +35,8 @@ void setVZVirtioFileSystemDeviceConfigurationShare(void *config, void *share); void *VZVirtualMachine_directorySharingDevices(void *machine); void setShareVZVirtioFileSystemDevice(void *device, void *share, void *queue); +void *getShareVZVirtioFileSystemDevice(void *device, void *queue); +bool isMultipleDirectoryShare(void *share); void setDirectorySharingDevicesVZVirtualMachineConfiguration(void *config, void *directorySharingDevices); void setPlatformVZVirtualMachineConfiguration(void *config, diff --git a/virtualization_12.m b/virtualization_12.m index c8d7f925..cbba5025 100644 --- a/virtualization_12.m +++ b/virtualization_12.m @@ -92,6 +92,41 @@ void setShareVZVirtioFileSystemDevice(void *device, void *share, void *queue) RAISE_UNSUPPORTED_MACOS_EXCEPTION(); } +/*! + @abstract Return the directory share currently set on a running VZVirtioFileSystemDevice, or NULL if none is set. + @discussion + Reading happens on the VM's serial dispatch queue like every other VZVirtualMachine + interaction. The returned share is retained for the Go caller, which releases it via + its finalizer. + */ +void *getShareVZVirtioFileSystemDevice(void *device, void *queue) +{ + if (@available(macOS 12, *)) { + __block VZDirectoryShare *share; + dispatch_sync((dispatch_queue_t)queue, ^{ + share = [(VZVirtioFileSystemDevice *)device share]; + }); + return [share retain]; + } + + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +/*! + @abstract Report whether a VZDirectoryShare is a VZMultipleDirectoryShare. + @discussion + Lets the Go side rebuild the right concrete share type for a share read back from a + running device. A single directory share returns false. + */ +bool isMultipleDirectoryShare(void *share) +{ + if (@available(macOS 12, *)) { + return [(NSObject *)share isKindOfClass:[VZMultipleDirectoryShare class]]; + } + + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + /*! @abstract The hardware platform to use. @discussion