ハイライト
macOS 27のVirtualizationフレームワークには、macOSゲストのユーザーアカウント自動設定、Accessory Accessフレームワークによる安全なUSBデバイスパススルー、DiskImageKitによるレイヤー化されたスパースディスクイメージ、カスタムVirtioデバイスという4つの中核機能が追加されます。これにより、サードパーティの仮想マシンアプリは、自動化、ストレージ効率、ハードウェア拡張性の面で本番品質に近づけます。
重要ポイント
macOSゲストの自動設定: Setup Assistantの手作業から離れる
以前はmacOS仮想マシンのインストール後、Setup Assistantを手動で進める必要がありました。ユーザーを作り、パスワードを設定し、利用規約に同意する操作です。CI/CDパイプラインや自動テストでは、この手順が最大のボトルネックでした。新しいVMを作るたびに人の操作が必要で、本当の無人実行はできませんでした。
macOS 27ではVZMacGuestProvisioningOptionsが導入されます。VMを起動するときにユーザー名、パスワード、フルネームを渡せるほか、自動ログインやSSHリモートログインも有効化できます。ゲストの初回起動時、これらのパラメータがSetup Assistantへ直接渡され、設定が自動で完了します。
(01:56)
制限もあります。このオプションは、ゲストがまだ設定されていない場合にだけ有効です。すでにユーザーが作成されている場合、その後の起動で渡したprovisioningオプションは無視されます。パスワードの扱いについて、AppleはハードコードではなくKeychainや環境変数から読み取ることを勧めています。
USBデバイスパススルー: Accessory Accessフレームワークの安全モデル
仮想マシン内でUSBメモリ、ドングル、デバッガを使うことは、以前から難しい問題でした。低レベルのIOKit hackに頼るか、不安定な体験を受け入れるしかなく、抜き差し時にVMが固まることもありました。
macOS 27ではAccessory Accessフレームワークが導入されます。中心にある設計思想は、ユーザーが常に制御権を持つことです。アプリが関心のあるデバイス種類を登録すると、macOSのメニューバーにアクセサリアイコンが表示されます。ユーザーが手動で「アプリに接続」を選んだときだけ、そのデバイスが仮想マシンで使えるように許可されます。ユーザーはいつでも切断できます。
(04:39)
この設計は完全自動化の便利さを少し犠牲にし、安全性と安定性を得ています。VMの実行中にデバイスをホットプラグでき、再起動や静的なVM設定変更は不要です。一般ユーザーにも分かりやすく、悪意あるソフトウェアがUSBデバイスをこっそり読むリスクも防げます。
レイヤー化ディスクイメージ: DiskImageKitでストレージ問題を解決する
従来のrawディスクイメージは1対1の対応でした。100GBの論理ディスクは100GBの物理ファイルです。スナップショットを作るにはファイル全体をコピーする必要があり、VMのクローン作成コストは非常に高くなります。
macOS 27のDiskImageKitフレームワークは、Apple Sparse Image Format(ASIF)を導入し、レイヤー化されたイメージスタックをサポートします。最下層はbase layerで、読み取り専用のきれいなシステムイメージを保持します。その上にcache layerまたはoverlay layerを追加でき、どちらもASIF形式です。
(11:40)
Cache layerは高速化に使います。下層データがネットワークファイルシステムなど低速なストレージにある場合、読み取ったデータはcache layerに保存され、次回以降はキャッシュから直接読めます。Overlay layerはcopy-on-writeを実現します。書き込みはoverlay層だけに行われ、base層は読み取り専用のままです。複数のVMが同じbase layerを共有し、それぞれ独立したoverlayを持てるため、ディスク容量を大きく節約できます。
ASIFはスパース形式です。論理サイズは実際に保存されているデータ量より大きくでき、未使用ブロックは物理容量を消費しません。
カスタムVirtioデバイス: Host-Guestの高性能チャネルを作る
Virtualizationフレームワークには一般的なデバイス種類がすでに組み込まれていますが、よりspecializedな通信チャネルが必要な場面もあります。たとえば、Host-Guest間の高性能通信のためにカスタムプロトコルを実装したり、機械学習アクセラレータをLinuxゲストへ公開したりする場合です。
macOS 27ではVZCustomVirtioDeviceが開放されます。Virtioは準仮想化デバイスの業界標準です。共有メモリバッファとVirtio queueを使ってHost-Guest通信を行い、設計上コンテキストスイッチを最小化します。
(15:57)
アプリはデバイスのVirtio device ID、PCI class/subclass、queue数を設定し、delegateを指定します。ゲストがVirtio queue経由でデータを送ると、delegateのdidReceiveNotificationForが呼ばれます。アプリはqueueから要素を取り出して処理し、処理後にqueueへ返します。アプリ側から割り込みを発火して、ゲストドライバへ通知することもできます。
詳細
macOSゲストを自動設定する
(01:56)
import Virtualization
let provisioningOptions = VZMacGuestProvisioningOptions()
provisioningOptions.fullName = fullName
provisioningOptions.username = username
provisioningOptions.password = password
provisioningOptions.logsInAutomatically = true
provisioningOptions.enablesRemoteLogin = true
let startOptions = VZMacOSVirtualMachineStartOptions()
try startOptions.setGuestProvisioning(provisioningOptions)
try await virtualMachine.start(options: startOptions)
ポイント:
VZMacGuestProvisioningOptionsは、初回起動時にSetup Assistantへ渡す必要があるすべてのパラメータをまとめますfullName、username、passwordはユーザーアカウント作成に使われますlogsInAutomatically = trueにすると、初回起動後にそのアカウントへ自動ログインしますenablesRemoteLogin = trueはSSHサービスを有効にし、リモート管理をしやすくしますsetGuestProvisioningで設定を起動オプションへ付加します- これらのパラメータは、ゲストの初回起動時かつまだユーザーが設定されていない場合にだけ有効です
USBアクセサリリスナーを登録する
(07:12)
import AccessoryAccess
let criteria: [AAUSBAccessoryMatchingCriteria] = []
let accessories = try await AAUSBAccessoryManager.shared.registerListener(self, matchingCriteria: criteria)
for accessory in accessories {
// Handle previously attached accessories.
}
ポイント:
AAUSBAccessoryMatchingCriteriaは関心のあるデバイス種類を絞り込むために使います。device class、vendor ID、product IDなどで条件指定できます- 空配列を渡すと、すべてのUSBデバイスに関心があることを意味します
registerListenerは、すでにアプリに許可されているデバイス一覧を返すので、走査して処理する必要があります- リスナーオブジェクトは
AAUSBAccessoryListenerプロトコルを実装する必要があります - Xcode TargetのCapabilitiesに”Claim USB Accessory”を追加します
USBアクセサリ接続に反応する
(07:39)
import AccessoryAccess
import Virtualization
class AccessoryListener: NSObject, AAUSBAccessoryListener {
func usbAccessoryDidConnect(_ usbAccessory: AAUSBAccessory) {
virtualMachine.queue.async {
do {
let configuration = VZUSBPassthroughDeviceConfiguration(device: usbAccessory)
let device = try VZUSBPassthroughDevice(configuration: configuration)
self.virtualMachine.usbControllers.first?.attach(device: device) { error in
// Handle error if necessary...
}
} catch {
// Handle error...
}
}
}
}
ポイント:
usbAccessoryDidConnectは、ユーザーがメニューバーからデバイスを許可したあとに呼ばれます- VMのハードウェア変更はすべて
virtualMachine.queue上で実行する必要があります。これはスレッド安全性の要件です VZUSBPassthroughDeviceConfigurationはAAUSBAccessoryをVMが認識できる設定へ包みますVZUSBPassthroughDeviceが実際のパススルーデバイスオブジェクトですattach(device:)でデバイスをVMのUSBコントローラへ動的に接続します- ユーザーはいつでもメニューバーからデバイスを切断できるため、アプリは
usbAccessoryDidDisconnectを実装して後処理する必要があります
カスタムvmnetネットワークを作成する
(10:04)
import Virtualization
import vmnet
var status: vmnet_return_t = .VMNET_FAILURE
guard let networkConfiguration =
vmnet_network_configuration_create(.VMNET_SHARED_MODE, &status) else { ... }
guard let network =
vmnet_network_create(networkConfiguration, &status) else { ... }
let attachment = VZVmnetNetworkDeviceAttachment(network: network)
let networkDeviceConfiguration = VZVirtioNetworkDeviceConfiguration()
networkDeviceConfiguration.attachment = attachment
virtualMachineConfiguration.networkDevices = [networkDeviceConfiguration]
let virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)
ポイント:
vmnet_network_configuration_createはネットワーク設定オブジェクトを作成します。.VMNET_SHARED_MODEは共有モードです- vmnet APIを使えば、DHCPパラメータやポートフォワーディングルールなどをさらに設定できます
vmnet_network_createは設定からネットワークオブジェクトを作成しますVZVmnetNetworkDeviceAttachmentはvmnetネットワークとVirtualizationフレームワークを橋渡しします- 複数のVMが同じ
networkオブジェクトを使うと、互いに通信できます - vmnetネットワークは参照カウントされるObjective-Cオブジェクトです。アプリ終了後にネットワークは消えるため、設定は自分で永続化する必要があります
vmnet_network_copy_serializationとvmnet_network_create_with_serializationを使うと、XPCをまたいでネットワークオブジェクトを渡せます
DiskImageKitのレイヤー化イメージ
(14:54)
import DiskImageKit
import Virtualization
let baseImage = try DiskImage(opening: .open(url: baseLayerURL, mode: .readOnly))
let cacheImage = try baseImage.appending(.asifLayer(url: cacheLayerURL, type: .cache))
let overlayImage = try DiskImage(opening: .open(url: overlayLayerURL))
let stackedImage = try cacheImage.appending(overlayImage)
let storageDeviceAttachment = try VZDiskImageStorageDeviceAttachment(diskImage: stackedImage)
let storageDeviceConfiguration =
VZVirtioBlockDeviceConfiguration(attachment: storageDeviceAttachment)
virtualMachineConfiguration.storageDevices = [storageDeviceConfiguration]
let virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)
ポイント:
DiskImage(opening: .open(url:mode:))で基礎イメージを開きます。.readOnlyによりbase layerが変更されないことを保証します.appending(.asifLayer(url:type:))はASIF形式の層を追加します。.cache型はパフォーマンスキャッシュに使います- 別の
DiskImageオブジェクトをoverlay layerとして追加することもできます - 読み取り時、DiskImageKitはレイヤースタックを上から下へたどり、対象ブロックを含む最初の層を使います
- cache層がある場合、下層データを読み取ると自動でcache層に保存されます
- overlay層がある場合、書き込みはoverlayに保存され、copy-on-writeが実現されます
VZDiskImageStorageDeviceAttachmentはDiskImageKitイメージをVMストレージデバイスのアタッチメントとして包みます- レイヤースタックは浅いほど性能が良いため、層の数は最小限に保つことが推奨されます
- 複数のVMがbase layerを共有する場合、それぞれが補助ストレージファイルとEFI変数ストレージファイルを別個にコピーする必要があります
カスタムVirtioデバイスを設定する
(17:41)
import Virtualization
let deviceConfiguration = VZCustomVirtioDeviceConfiguration()
deviceConfiguration.deviceID = 4
deviceConfiguration.pciClassID = 0x10
deviceConfiguration.pciSubclassID = 0x00
deviceConfiguration.virtioQueueCount = 1
deviceConfiguration.provider =
VZCustomVirtioDeviceDelegateProvider(deviceQueue: deviceQueue, delegate: provider)
virtualMachineConfiguration.customVirtioDevices = [deviceConfiguration]
let virtualMachine = VZVirtualMachine(configuration: virtualMachineConfiguration)
ポイント:
deviceID = 4はVirtio entropyデバイスに対応します。カスタムデバイスでは、未使用のIDを選ぶ必要がありますpciClassID = 0x10はPCI cryptoデバイスクラスですpciSubclassID = 0x00はネットワークおよび計算暗号コントローラのサブクラスですvirtioQueueCountはデバイス要件に応じてVirtio queue数を設定しますVZCustomVirtioDeviceDelegateProviderはdelegateとqueueをデバイス設定へ結びつけます- カスタムデバイスが動作するには、ゲスト側に対応するVirtioドライバが必要です
VirtioデバイスDelegate
(18:20)
import Virtualization
class DeviceConfigurationDelegate: NSObject, VZCustomVirtioDeviceConfigurationDelegate {
func customVirtioConfiguration(_ deviceConfiguration: VZCustomVirtioDeviceConfiguration,
didCreateDevice device: VZCustomVirtioDevice) {
device.delegate = deviceDelegate
self.device = device
}
}
ポイント:
VZCustomVirtioDeviceConfigurationDelegateは、VM起動後にデバイス作成が完了した時点で呼ばれます- ここでデバイスのdelegateを設定し、以後のVirtio queueイベントを処理します
- 後でゲスト割り込みを能動的に発火できるよう、
deviceへの参照を保持します
Virtio Queue要素を処理する
(18:42)
import Virtualization
class DeviceDelegate: NSObject, VZCustomVirtioDeviceDelegate {
func customVirtioDevice(_ device: VZCustomVirtioDevice,
didReceiveNotificationFor queue: VZVirtioQueue) {
while let element = queue.nextElement() {
// Process element...
element.returnToQueue()
}
}
}
ポイント:
didReceiveNotificationForは、ゲストドライバがqueueへデータを書き込んだときに呼ばれますqueue.nextElement()でqueue内の要素を取り出します。queueが空ならnilを返します- 要素の処理後は、必ず
element.returnToQueue()を呼んでqueueへ返す必要があります - カスタムデバイスには、ゲスト側に対応するVirtioドライバが必要です
- ドライバ設計ではVirtioのベストプラクティスに従い、queueのバッチ処理能力を十分に活用します
実装アイデア
1. CI/CD向けの無人macOSテストクラスター
VZMacGuestProvisioningOptionsでVMのzero-touch初期化を実現します。DiskImageKitのレイヤー化イメージと組み合わせると、1つのbaseイメージをすべてのテストノードで共有し、各ノードは数MBのoverlayだけを持てば済みます。テスト後にoverlayを削除すれば、きれいな状態へ戻せます。
入口: VZMacGuestProvisioningOptions + VZDiskImageStorageDeviceAttachment
2. USBデバイスパススルー付き開発環境
組み込み開発やハードウェアテストでは、USBデバッガ、ドングル、ファームウェア書き込みツールを仮想マシン内で使うことがよくあります。以前はデュアルブートや物理マシンが必要でした。今はAccessory Accessフレームワークにより、ユーザーが手動で許可したあとにデバイスをVMへパススルーでき、ホットプラグも安定して信頼できます。
入口: AAUSBAccessoryManager.shared.registerListener + VZUSBPassthroughDevice
3. マイクロサービスのローカル結合テスト用ネットワーク
vmnetフレームワークでカスタムネットワークトポロジを作成し、ホストの特定ポートをVM内部へ転送します。たとえばホストの8080をVMの80へマップすれば、ローカルブラウザからホストのlocalhost:8080へアクセスして、VM内のWebサービスをデバッグできます。
入口: vmnet_network_configuration_create + DHCP設定 + ポートフォワーディングルール
4. レイヤー化イメージによる「スナップショット即復元」システム
DiskImageKitのoverlay layerをスナップショットとして使います。各テスト前にclean baseから新しいoverlayを作成し、テスト後に保持すればスナップショット、破棄すれば復元になります。overlayはスパースファイルなので、作成コストは非常に低くなります。
入口: DiskImage + .appending(.asifLayer) + overlay layer管理
5. Linux VM内のカスタムハードウェアアクセラレーション
VZCustomVirtioDeviceを通じて、ホストの専用ハードウェア、たとえばNPU、FPGA、カスタムコプロセッサをLinuxゲストへ公開します。対応するLinux Virtioドライバを書けば、Host-Guest間の低遅延通信を実現できます。
入口: VZCustomVirtioDeviceConfiguration + VZCustomVirtioDeviceDelegate
関連セッション
- 389 - Virtualizationでコンテナマシンを作成・管理する - Container MachineはVirtualizationフレームワークのもう1つの使い方で、このセッションの高度な機能を補完します
- 260 - Xcodeの新しいDevice Hubを探る - Device Hubはデバイス管理とリモートデバッグを扱い、VM内のデバイスパススルー場面に関係します
- 243 - InstrumentsでMetalアプリをプロファイルして最適化する - VM内で動作するMetalアプリのパフォーマンス分析ツールです
- 279 - RealityKitの新機能 - VM内のMetal機能サポート、argument buffersやindirect command buffersはグラフィックス描画に関係します
- 262 - Swiftの新機能 - このセッションのコード例では、Swiftのasync/awaitと構造化並行処理が多く使われています
コメント
GitHub Issues · utterances