use super::{ModuleLoader, VstModule};
use crate::error::{Error, Result};
use core_foundation::{
base::{Boolean, CFRelease, CFRetain, CFTypeRef, TCFType},
bundle::{
CFBundleCreate, CFBundleGetFunctionPointerForName, CFBundleLoadExecutable, CFBundleRef,
CFBundleUnloadExecutable,
},
string::CFString,
url::CFURLCreateFromFileSystemRepresentation,
};
use std::{ffi::CString, path::Path, ptr};
use vst3::Steinberg::IPluginFactory;
#[cfg(target_arch = "aarch64")]
const HOST_ARCH: &str = "arm64";
#[cfg(target_arch = "x86_64")]
const HOST_ARCH: &str = "x86_64";
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
const HOST_ARCH: &str = "unknown";
fn bundle_executable_archs(bundle_path: &Path) -> Vec<&'static str> {
let macos_dir = bundle_path.join("Contents/MacOS");
let exe = std::fs::read_dir(&macos_dir)
.ok()
.and_then(|it| it.flatten().map(|e| e.path()).find(|p| p.is_file()));
let Some(exe) = exe else {
return Vec::new();
};
let Ok(data) = std::fs::read(&exe) else {
return Vec::new();
};
parse_macho_archs(&data)
}
fn parse_macho_archs(data: &[u8]) -> Vec<&'static str> {
fn arch_name(cputype: u32) -> Option<&'static str> {
match cputype {
0x0100_0007 => Some("x86_64"),
0x0100_000c => Some("arm64"),
_ => None,
}
}
if data.len() < 8 {
return Vec::new();
}
let mut archs = Vec::new();
let magic = [data[0], data[1], data[2], data[3]];
if magic == [0xca, 0xfe, 0xba, 0xbe] {
let nfat = u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize;
for i in 0..nfat {
let off = 8 + i * 20;
if off + 4 > data.len() {
break;
}
let cputype =
u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]);
if let Some(a) = arch_name(cputype) {
if !archs.contains(&a) {
archs.push(a);
}
}
}
} else if magic == [0xcf, 0xfa, 0xed, 0xfe] {
let cputype = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
if let Some(a) = arch_name(cputype) {
archs.push(a);
}
}
archs
}
type BundleEntryFunc = unsafe extern "C" fn(bundle: CFBundleRef) -> Boolean;
type BundleExitFunc = unsafe extern "C" fn() -> Boolean;
type GetPluginFactoryFunc = unsafe extern "C" fn() -> *mut IPluginFactory;
pub struct MacOSModule {
bundle: CFBundleRef,
#[allow(dead_code)]
path: std::path::PathBuf,
bundle_exit: Option<BundleExitFunc>,
get_factory_fn: Option<GetPluginFactoryFunc>,
}
impl MacOSModule {
fn load_internal(path: &Path) -> Result<Self> {
unsafe {
log::info!("=== macOS VST3 MODULE LOADING START ===");
log::info!("Loading VST3 bundle: {}", path.display());
let bundle_path = if path.extension().and_then(|s| s.to_str()) == Some("vst3") {
path.to_path_buf()
} else {
let mut current = path;
loop {
if let Some(ext) = current.extension() {
if ext == "vst3" {
break current.to_path_buf();
}
}
match current.parent() {
Some(parent) => current = parent,
None => {
return Err(Error::PluginLoadFailed(
"Could not find .vst3 bundle in path hierarchy".to_string(),
))
}
}
}
};
log::debug!("Using bundle path: {}", bundle_path.display());
log::debug!("Step 1: Creating CFBundle from bundle path...");
let path_cstring = CString::new(bundle_path.to_string_lossy().as_bytes())
.map_err(|e| Error::PluginLoadFailed(format!("Invalid path string: {}", e)))?;
let url = CFURLCreateFromFileSystemRepresentation(
ptr::null_mut(),
path_cstring.as_ptr() as *const u8,
path_cstring.as_bytes().len() as isize,
1, );
if url.is_null() {
return Err(Error::PluginLoadFailed(
"Failed to create URL from path".to_string(),
));
}
let bundle = CFBundleCreate(ptr::null_mut(), url);
CFRelease(url as CFTypeRef);
if bundle.is_null() {
return Err(Error::PluginLoadFailed(
"Failed to create CFBundle".to_string(),
));
}
log::debug!("CFBundle created successfully");
log::debug!("Step 2: Loading bundle executable...");
let load_result = CFBundleLoadExecutable(bundle);
if load_result == 0 {
CFRelease(bundle as CFTypeRef);
let detail = match bundle_executable_archs(&bundle_path) {
archs if !archs.is_empty() && !archs.contains(&HOST_ARCH) => format!(
"Failed to load bundle executable: plugin is {}-only but this host \
is {HOST_ARCH}. Run the host under Rosetta or use a universal/{HOST_ARCH} \
build of the plugin.",
archs.join("+")
),
_ => "Failed to load bundle executable".to_string(),
};
return Err(Error::PluginLoadFailed(detail));
}
log::debug!("Bundle executable loaded");
log::debug!("Step 3: Getting bundleEntry function...");
let bundle_entry_name = CFString::new("bundleEntry");
let bundle_entry_ptr =
CFBundleGetFunctionPointerForName(bundle, bundle_entry_name.as_concrete_TypeRef());
if bundle_entry_ptr.is_null() {
CFBundleUnloadExecutable(bundle);
CFRelease(bundle as CFTypeRef);
return Err(Error::PluginLoadFailed(
"Bundle does not export the required 'bundleEntry' function".to_string(),
));
}
let bundle_entry: BundleEntryFunc = std::mem::transmute(bundle_entry_ptr);
log::debug!("bundleEntry function found");
log::debug!("Step 4: Getting bundleExit function...");
let bundle_exit_name = CFString::new("bundleExit");
let bundle_exit_ptr =
CFBundleGetFunctionPointerForName(bundle, bundle_exit_name.as_concrete_TypeRef());
if bundle_exit_ptr.is_null() {
CFBundleUnloadExecutable(bundle);
CFRelease(bundle as CFTypeRef);
return Err(Error::PluginLoadFailed(
"Bundle does not export the required 'bundleExit' function".to_string(),
));
}
let bundle_exit: BundleExitFunc = std::mem::transmute(bundle_exit_ptr);
log::debug!("bundleExit function found");
log::debug!("Step 5: Calling bundleEntry...");
CFRetain(bundle as CFTypeRef); let entry_result = bundle_entry(bundle);
if entry_result == 0 {
CFRelease(bundle as CFTypeRef); CFBundleUnloadExecutable(bundle);
CFRelease(bundle as CFTypeRef);
return Err(Error::PluginLoadFailed(
"bundleEntry function returned false".to_string(),
));
}
log::debug!("bundleEntry called successfully");
log::debug!("Step 6: Getting GetPluginFactory function...");
let factory_name = CFString::new("GetPluginFactory");
let factory_ptr =
CFBundleGetFunctionPointerForName(bundle, factory_name.as_concrete_TypeRef());
if factory_ptr.is_null() {
bundle_exit();
CFRelease(bundle as CFTypeRef); CFBundleUnloadExecutable(bundle);
CFRelease(bundle as CFTypeRef);
return Err(Error::PluginLoadFailed(
"Bundle does not export 'GetPluginFactory' function".to_string(),
));
}
let get_factory_fn: GetPluginFactoryFunc = std::mem::transmute(factory_ptr);
log::debug!("GetPluginFactory function found");
log::info!("=== macOS VST3 MODULE LOADING COMPLETE ===");
log::info!("Bundle loaded successfully: {}", path.display());
Ok(MacOSModule {
bundle,
path: bundle_path,
bundle_exit: Some(bundle_exit),
get_factory_fn: Some(get_factory_fn),
})
}
}
}
unsafe impl Send for MacOSModule {}
impl VstModule for MacOSModule {
fn get_factory(&self) -> Result<*mut IPluginFactory> {
match self.get_factory_fn {
Some(factory_fn) => {
let factory = unsafe { factory_fn() };
if factory.is_null() {
Err(Error::PluginLoadFailed(
"GetPluginFactory returned null".to_string(),
))
} else {
Ok(factory)
}
}
None => Err(Error::PluginLoadFailed(
"GetPluginFactory function not available".to_string(),
)),
}
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for MacOSModule {
fn drop(&mut self) {
unsafe {
log::debug!("=== macOS VST3 MODULE CLEANUP START ===");
if let Some(bundle_exit) = self.bundle_exit.take() {
log::debug!("Calling bundleExit...");
let exit_result = bundle_exit();
if exit_result != 0 {
log::debug!("bundleExit called successfully");
} else {
log::warn!("bundleExit returned false");
}
CFRelease(self.bundle as CFTypeRef);
}
log::debug!("Unloading bundle executable...");
CFBundleUnloadExecutable(self.bundle);
log::debug!("Bundle executable unloaded");
log::debug!("Releasing CFBundle...");
CFRelease(self.bundle as CFTypeRef);
log::debug!("CFBundle released");
log::debug!("=== macOS VST3 MODULE CLEANUP COMPLETE ===");
}
}
}
pub struct MacOSModuleLoader;
impl ModuleLoader for MacOSModuleLoader {
fn load(path: &Path) -> Result<Box<dyn VstModule>> {
let module = MacOSModule::load_internal(path)?;
Ok(Box::new(module))
}
}
#[cfg(test)]
mod arch_tests {
use super::*;
#[test]
fn parses_thin_x86_64() {
let mut data = vec![0xcf, 0xfa, 0xed, 0xfe, 0x07, 0x00, 0x00, 0x01];
data.resize(64, 0);
assert_eq!(parse_macho_archs(&data), vec!["x86_64"]);
}
#[test]
fn parses_fat_universal() {
let mut data = vec![0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 2];
data.extend_from_slice(&0x0100_0007u32.to_be_bytes()); data.extend_from_slice(&[0u8; 16]);
data.extend_from_slice(&0x0100_000cu32.to_be_bytes()); data.extend_from_slice(&[0u8; 16]);
assert_eq!(parse_macho_archs(&data), vec!["x86_64", "arm64"]);
}
#[test]
fn ignores_garbage() {
assert!(parse_macho_archs(&[0, 1, 2]).is_empty());
}
}