use std::path::Path;
use std::sync::Mutex;
use block2::RcBlock;
use dispatch2::{DispatchQueue, DispatchQueueAttr, DispatchRetained};
use objc2::AnyThread;
use objc2::rc::Retained;
use objc2_foundation::{NSArray, NSError, NSFileHandle, NSString, NSURL};
use objc2_virtualization::{
VZDiskImageCachingMode, VZDiskImageStorageDeviceAttachment, VZDiskImageSynchronizationMode,
VZFileHandleSerialPortAttachment, VZGenericPlatformConfiguration, VZLinuxBootLoader,
VZNATNetworkDeviceAttachment, VZNetworkDeviceAttachment, VZSerialPortAttachment,
VZStorageDeviceAttachment, VZStorageDeviceConfiguration, VZVirtioBlockDeviceConfiguration,
VZVirtioConsoleDeviceSerialPortConfiguration, VZVirtioEntropyDeviceConfiguration,
VZVirtioNetworkDeviceConfiguration, VZVirtioSocketDeviceConfiguration, VZVirtualMachine,
VZVirtualMachineConfiguration,
};
use crate::VzError;
use crate::machine::VmConfig;
pub(crate) struct VzHandle {
vm: Retained<VZVirtualMachine>,
queue: DispatchRetained<DispatchQueue>,
}
struct QueueBoundVm(Retained<VZVirtualMachine>);
unsafe impl Send for QueueBoundVm {}
impl QueueBoundVm {
fn submit<F>(self, submit: F, block: &block2::DynBlock<dyn Fn(*mut NSError)>)
where
F: FnOnce(&VZVirtualMachine, &block2::DynBlock<dyn Fn(*mut NSError)>),
{
submit(&self.0, block);
}
}
unsafe impl Send for VzHandle {}
unsafe impl Sync for VzHandle {}
impl VzHandle {
pub fn new(config: &VmConfig) -> Result<Self, VzError> {
let queue = DispatchQueue::new("com.microvm.vz", DispatchQueueAttr::SERIAL);
let vz_config = build_vz_config(config)?;
let vm = unsafe {
VZVirtualMachine::initWithConfiguration_queue(
VZVirtualMachine::alloc(),
&vz_config,
&queue,
)
};
Ok(Self { vm, queue })
}
pub async fn start(&self) -> Result<(), VzError> {
self.with_completion("start", |vm, block| {
unsafe { vm.startWithCompletionHandler(block) };
})
.await
}
pub async fn stop(&self) -> Result<(), VzError> {
self.with_completion("stop", |vm, block| {
unsafe { vm.stopWithCompletionHandler(block) };
})
.await
}
pub async fn pause(&self) -> Result<(), VzError> {
self.with_completion("pause", |vm, block| {
unsafe { vm.pauseWithCompletionHandler(block) };
})
.await
}
pub async fn resume(&self) -> Result<(), VzError> {
self.with_completion("resume", |vm, block| {
unsafe { vm.resumeWithCompletionHandler(block) };
})
.await
}
pub async fn save_state(&self, path: &Path) -> Result<(), VzError> {
let url_path = path_string(path)?;
self.with_completion("save_state", move |vm, block| {
let url = nsurl_from_str(&url_path);
unsafe { vm.saveMachineStateToURL_completionHandler(&url, block) };
})
.await
}
pub async fn restore_state(&self, path: &Path) -> Result<(), VzError> {
let url_path = path_string(path)?;
self.with_completion("restore_state", move |vm, block| {
let url = nsurl_from_str(&url_path);
unsafe { vm.restoreMachineStateFromURL_completionHandler(&url, block) };
})
.await
}
async fn with_completion<F>(&self, operation: &'static str, submit: F) -> Result<(), VzError>
where
F: FnOnce(&VZVirtualMachine, &block2::DynBlock<dyn Fn(*mut NSError)>) + Send + 'static,
{
let (tx, rx) = tokio::sync::oneshot::channel::<Result<(), VzError>>();
let vm = QueueBoundVm(self.vm.clone());
self.queue.exec_async(move || {
let tx = Mutex::new(Some(tx));
let block = RcBlock::new(move |err: *mut NSError| {
let result = completion_result(operation, err);
let mut tx = tx.lock().expect("completion sender mutex poisoned");
if let Some(tx) = tx.take() {
let _ = tx.send(result);
}
});
vm.submit(submit, &block);
});
rx.await
.map_err(|_| VzError::CompletionDropped { operation })?
}
}
fn build_vz_config(config: &VmConfig) -> Result<Retained<VZVirtualMachineConfiguration>, VzError> {
let kernel_url = nsurl_from_path(&config.kernel)?;
let rootfs_url = nsurl_from_path(&config.rootfs)?;
unsafe {
let vz_config = VZVirtualMachineConfiguration::new();
vz_config.setCPUCount(config.cpus as usize);
let aligned = (config.memory_bytes + (1 << 20) - 1) & !((1 << 20) - 1);
vz_config.setMemorySize(aligned);
let boot_loader =
VZLinuxBootLoader::initWithKernelURL(VZLinuxBootLoader::alloc(), &kernel_url);
let cmdline = config.kernel_cmdline.join(" ");
boot_loader.setCommandLine(&NSString::from_str(&cmdline));
vz_config.setBootLoader(Some(&boot_loader));
let platform = VZGenericPlatformConfiguration::new();
if config.nested_virt {
if !VZGenericPlatformConfiguration::isNestedVirtualizationSupported() {
return Err(VzError::InvalidConfig(
"nested virtualization not supported on this hardware".into(),
));
}
platform.setNestedVirtualizationEnabled(true);
}
vz_config.setPlatform(&platform);
let entropy = VZVirtioEntropyDeviceConfiguration::new();
vz_config.setEntropyDevices(&NSArray::from_retained_slice(&[Retained::into_super(
entropy,
)]));
let stdin_handle = NSFileHandle::fileHandleWithStandardInput();
let stdout_handle = NSFileHandle::fileHandleWithStandardOutput();
let serial_attachment =
VZFileHandleSerialPortAttachment::initWithFileHandleForReading_fileHandleForWriting(
VZFileHandleSerialPortAttachment::alloc(),
Some(&stdin_handle),
Some(&stdout_handle),
);
let serial_attachment: Retained<VZSerialPortAttachment> =
Retained::into_super(serial_attachment);
let serial = VZVirtioConsoleDeviceSerialPortConfiguration::new();
serial.setAttachment(Some(&serial_attachment));
vz_config.setSerialPorts(&NSArray::from_retained_slice(&[Retained::into_super(
serial,
)]));
let socket = VZVirtioSocketDeviceConfiguration::new();
vz_config.setSocketDevices(&NSArray::from_retained_slice(&[Retained::into_super(
socket,
)]));
let net_dev = VZVirtioNetworkDeviceConfiguration::new();
let nat = VZNATNetworkDeviceAttachment::new();
let nat: Retained<VZNetworkDeviceAttachment> = Retained::into_super(nat);
net_dev.setAttachment(Some(&nat));
vz_config.setNetworkDevices(&NSArray::from_retained_slice(&[Retained::into_super(
net_dev,
)]));
let disk: Retained<VZDiskImageStorageDeviceAttachment> =
VZDiskImageStorageDeviceAttachment::initWithURL_readOnly_cachingMode_synchronizationMode_error(
VZDiskImageStorageDeviceAttachment::alloc(),
&rootfs_url,
false,
VZDiskImageCachingMode::Automatic,
VZDiskImageSynchronizationMode::Full,
).map_err(|e| VzError::Framework {
operation: "disk_attach",
message: e.localizedDescription().to_string(),
})?;
let disk_base: Retained<VZStorageDeviceAttachment> = Retained::into_super(disk);
let block_dev = VZVirtioBlockDeviceConfiguration::initWithAttachment(
VZVirtioBlockDeviceConfiguration::alloc(),
&disk_base,
);
let block_as_storage: Retained<VZStorageDeviceConfiguration> =
Retained::into_super(block_dev);
vz_config.setStorageDevices(&NSArray::from_retained_slice(&[block_as_storage]));
vz_config
.validateWithError()
.map_err(|e| VzError::InvalidConfig(e.localizedDescription().to_string()))?;
Ok(vz_config)
}
}
fn completion_result(operation: &'static str, err: *mut NSError) -> Result<(), VzError> {
let Some(err) = (unsafe { err.as_ref() }) else {
return Ok(());
};
Err(VzError::Framework {
operation,
message: err.localizedDescription().to_string(),
})
}
fn nsurl_from_path(path: &Path) -> Result<Retained<NSURL>, VzError> {
path_string(path).map(|path| nsurl_from_str(&path))
}
fn nsurl_from_str(path: &str) -> Retained<NSURL> {
NSURL::initFileURLWithPath(NSURL::alloc(), &NSString::from_str(path))
}
fn path_string(path: &Path) -> Result<String, VzError> {
path.to_str()
.map(ToOwned::to_owned)
.ok_or_else(|| VzError::NonUtf8Path {
path: path.to_path_buf(),
})
}