WWDC Quick Look 💓 By SwiftGGTeam
Virtualizationアプリの機能を広げる

Virtualizationアプリの機能を広げる

元の動画を見る

ハイライト

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へ渡す必要があるすべてのパラメータをまとめます
  • fullNameusernamepasswordはユーザーアカウント作成に使われます
  • 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上で実行する必要があります。これはスレッド安全性の要件です
  • VZUSBPassthroughDeviceConfigurationAAUSBAccessoryを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_serializationvmnet_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

関連セッション

コメント

GitHub Issues · utterances