Skip to main content

maolan_engine/plugins/vst3/
interfaces.rs

1// VST3 COM interface wrappers using vst3 crate
2//
3// This module provides safe Rust wrappers around VST3 COM interfaces
4// using the vst3 crate's trait-based API.
5
6// The vst3 crate uses platform-dependent types (c_int / DefaultEnumType) for
7// enum constants, so explicit casts are required for cross-platform compilation
8// even though they appear unnecessary on some platforms.
9#![allow(clippy::unnecessary_cast)]
10
11use std::ffi::c_void;
12use std::path::Path;
13use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
14use std::sync::{Arc, Mutex, OnceLock};
15use std::time::{Duration, Instant};
16use vst3::Steinberg::Vst::ProcessModes_::kRealtime;
17use vst3::Steinberg::Vst::SymbolicSampleSizes_::kSample32;
18use vst3::Steinberg::Vst::*;
19use vst3::Steinberg::*;
20use vst3::{Class, ComPtr, ComWrapper, Interface};
21
22/// Wrap a potentially-panicking plugin call in `catch_unwind`.
23/// Returns the closure's result on success, or an error string if it panics.
24pub fn protected_call<T, F>(op: F) -> Result<T, String>
25where
26    F: FnOnce() -> T + std::panic::UnwindSafe,
27{
28    match std::panic::catch_unwind(op) {
29        Ok(result) => Ok(result),
30        Err(_) => Err("Plugin call panicked".to_string()),
31    }
32}
33
34static HOST_RUN_LOOP_STATE: OnceLock<Mutex<HostRunLoopState>> = OnceLock::new();
35
36struct HostRunLoopState {
37    event_handlers: Vec<RunLoopEventHandler>,
38    timer_handlers: Vec<RunLoopTimerHandler>,
39}
40
41struct RunLoopEventHandler {
42    handler: usize,
43    fd: i32,
44}
45
46struct RunLoopTimerHandler {
47    handler: usize,
48    interval: Duration,
49    next_tick: Instant,
50}
51
52fn run_loop_state() -> &'static Mutex<HostRunLoopState> {
53    HOST_RUN_LOOP_STATE.get_or_init(|| {
54        Mutex::new(HostRunLoopState {
55            event_handlers: Vec::new(),
56            timer_handlers: Vec::new(),
57        })
58    })
59}
60
61pub fn pump_host_run_loop() {
62    // Linux VST3 UIs can rely on host-owned timers/fd callbacks.
63    // Drive those callbacks from the GUI-side UI loop.
64    let (event_calls, timer_calls): (Vec<(usize, i32)>, Vec<usize>) = {
65        let mut state = run_loop_state().lock().expect("run loop mutex poisoned");
66        let now = Instant::now();
67        let event_calls = state
68            .event_handlers
69            .iter()
70            .map(|h| (h.handler, h.fd))
71            .collect::<Vec<_>>();
72        let mut timer_calls = Vec::new();
73        for timer in &mut state.timer_handlers {
74            if now >= timer.next_tick {
75                timer_calls.push(timer.handler);
76                timer.next_tick = now + timer.interval;
77            }
78        }
79        (event_calls, timer_calls)
80    };
81
82    for (handler, fd) in event_calls {
83        let handler_ptr = handler as *mut Linux::IEventHandler;
84        if handler_ptr.is_null() {
85            continue;
86        }
87        unsafe {
88            ((*(*handler_ptr).vtbl).onFDIsSet)(handler_ptr, fd);
89        }
90    }
91    for handler in timer_calls {
92        let handler_ptr = handler as *mut Linux::ITimerHandler;
93        if handler_ptr.is_null() {
94            continue;
95        }
96        unsafe {
97            ((*(*handler_ptr).vtbl).onTimer)(handler_ptr);
98        }
99    }
100}
101
102/// Safe wrapper around VST3 plugin factory
103pub struct PluginFactory {
104    // Keep COM objects before the module so they are released before dlclose.
105    factory: ComPtr<IPluginFactory>,
106    module: libloading::Library,
107    module_inited: bool,
108}
109
110impl std::fmt::Debug for PluginFactory {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.debug_struct("PluginFactory")
113            .field("factory", &"<COM ptr>")
114            .field("module", &"<library>")
115            .finish()
116    }
117}
118
119impl PluginFactory {
120    /// Load a VST3 plugin bundle and create a factory
121    pub fn from_module(bundle_path: &Path) -> Result<Self, String> {
122        // Determine the actual module path based on platform
123        let module_path = get_module_path(bundle_path)?;
124
125        // Load the shared library
126        let library = unsafe {
127            libloading::Library::new(&module_path)
128                .map_err(|e| format!("Failed to load VST3 module {:?}: {}", module_path, e))?
129        };
130
131        // Many Windows plugins (including iPlug2-based VST3) rely on InitDll to
132        // initialize module globals used for resource lookup and UI setup.
133        let module_inited = unsafe {
134            match library.get::<unsafe extern "system" fn() -> bool>(b"InitDll") {
135                Ok(init_dll) => init_dll(),
136                Err(_) => false,
137            }
138        };
139
140        // Get the factory function
141        // VST3 plugins export: extern "system" fn GetPluginFactory() -> *mut c_void
142        let get_factory: libloading::Symbol<unsafe extern "system" fn() -> *mut c_void> = unsafe {
143            library
144                .get(b"GetPluginFactory")
145                .map_err(|e| format!("Failed to find GetPluginFactory: {}", e))?
146        };
147
148        // Call it to get the factory
149        let factory_ptr = unsafe { get_factory() };
150        if factory_ptr.is_null() {
151            return Err("GetPluginFactory returned null".to_string());
152        }
153
154        // Wrap in ComPtr - the vst3 crate provides this smart pointer
155        let factory = unsafe { ComPtr::from_raw(factory_ptr as *mut IPluginFactory) }
156            .ok_or("Failed to create ComPtr for IPluginFactory")?;
157
158        Ok(Self {
159            factory,
160            module: library,
161            module_inited,
162        })
163    }
164
165    /// Get information about a plugin class using the trait
166    pub fn get_class_info(&self, index: i32) -> Option<ClassInfo> {
167        use vst3::Steinberg::IPluginFactoryTrait;
168
169        let mut info = PClassInfo {
170            cid: [0; 16],
171            cardinality: 0,
172            category: [0; 32],
173            name: [0; 64],
174        };
175
176        let result = unsafe { self.factory.getClassInfo(index, &mut info) };
177
178        if result == kResultOk {
179            Some(ClassInfo {
180                name: extract_cstring(&info.name),
181                category: extract_cstring(&info.category),
182                cid: info.cid,
183            })
184        } else {
185            None
186        }
187    }
188
189    /// Count the number of classes using the trait
190    pub fn count_classes(&self) -> i32 {
191        use vst3::Steinberg::IPluginFactoryTrait;
192        unsafe { self.factory.countClasses() }
193    }
194
195    /// Create an instance of a plugin using the trait
196    pub fn create_instance(&self, class_id: &[i8; 16]) -> Result<PluginInstance, String> {
197        use vst3::Steinberg::IPluginFactoryTrait;
198
199        let mut instance_ptr: *mut c_void = std::ptr::null_mut();
200
201        let result = unsafe {
202            self.factory.createInstance(
203                class_id.as_ptr(),
204                IComponent::IID.as_ptr() as *const i8,
205                &mut instance_ptr,
206            )
207        };
208
209        if result != kResultOk || instance_ptr.is_null() {
210            return Err(format!(
211                "Failed to create plugin instance (result: {})",
212                result
213            ));
214        }
215
216        let component = unsafe { ComPtr::from_raw(instance_ptr as *mut IComponent) }
217            .ok_or("Failed to create ComPtr for IComponent")?;
218
219        Ok(PluginInstance::new(component))
220    }
221
222    /// Get factory information
223    pub fn get_factory_info(&self) -> Option<FactoryInfo> {
224        use vst3::Steinberg::IPluginFactoryTrait;
225
226        let mut info = PFactoryInfo {
227            vendor: [0; 64],
228            url: [0; 256],
229            email: [0; 128],
230            flags: 0,
231        };
232
233        let result = unsafe { self.factory.getFactoryInfo(&mut info) };
234
235        if result == kResultOk {
236            Some(FactoryInfo {
237                vendor: extract_cstring(&info.vendor),
238                url: extract_cstring(&info.url),
239                email: extract_cstring(&info.email),
240                flags: info.flags,
241            })
242        } else {
243            None
244        }
245    }
246
247    pub fn create_edit_controller(
248        &self,
249        class_id: &[i8; 16],
250    ) -> Result<ComPtr<IEditController>, String> {
251        use vst3::Steinberg::IPluginFactoryTrait;
252
253        let mut instance_ptr: *mut c_void = std::ptr::null_mut();
254
255        let result = unsafe {
256            self.factory.createInstance(
257                class_id.as_ptr(),
258                IEditController::IID.as_ptr() as *const i8,
259                &mut instance_ptr,
260            )
261        };
262
263        if result != kResultOk || instance_ptr.is_null() {
264            return Err(format!(
265                "Failed to create edit controller instance (result: {})",
266                result
267            ));
268        }
269
270        unsafe { ComPtr::from_raw(instance_ptr as *mut IEditController) }
271            .ok_or("Failed to create ComPtr for IEditController".to_string())
272    }
273}
274
275impl Drop for PluginFactory {
276    fn drop(&mut self) {
277        if !self.module_inited {
278            return;
279        }
280
281        unsafe {
282            if let Ok(exit_dll) = self
283                .module
284                .get::<unsafe extern "system" fn() -> bool>(b"ExitDll")
285            {
286                let _ = exit_dll();
287            }
288        }
289    }
290}
291
292/// Information about a plugin class
293pub struct ClassInfo {
294    pub name: String,
295    pub category: String,
296    pub cid: [i8; 16],
297}
298
299/// Information about the plugin factory
300#[derive(Debug, Clone)]
301pub struct FactoryInfo {
302    pub vendor: String,
303    pub url: String,
304    pub email: String,
305    pub flags: i32,
306}
307
308/// GUI capability information for a VST3 plugin
309#[derive(Debug, Clone)]
310pub struct Vst3GuiInfo {
311    pub has_gui: bool,
312    pub size: Option<(i32, i32)>,
313}
314
315/// Host-side IPlugFrame implementation that tracks resize requests from the plugin
316pub struct HostPlugFrame {
317    pub resize_requested: AtomicBool,
318    pub requested_size: Mutex<Option<(i32, i32)>>,
319}
320
321impl Default for HostPlugFrame {
322    fn default() -> Self {
323        Self {
324            resize_requested: AtomicBool::new(false),
325            requested_size: Mutex::new(None),
326        }
327    }
328}
329
330impl HostPlugFrame {
331    pub fn new() -> Self {
332        Self::default()
333    }
334}
335
336impl Class for HostPlugFrame {
337    type Interfaces = (IPlugFrame,);
338}
339
340impl IPlugFrameTrait for HostPlugFrame {
341    unsafe fn resizeView(&self, _view: *mut IPlugView, new_size: *mut ViewRect) -> tresult {
342        if !new_size.is_null() {
343            let rect = unsafe { *new_size };
344            let width = rect.right - rect.left;
345            let height = rect.bottom - rect.top;
346            if let Ok(mut size) = self.requested_size.lock() {
347                *size = Some((width, height));
348            }
349            self.resize_requested.store(true, Ordering::Relaxed);
350        }
351        kResultOk
352    }
353}
354
355/// Component handler that tracks parameter changes from the plugin GUI
356pub struct ComponentHandler {
357    pub parameter_changes: Arc<Mutex<Vec<(u32, f64)>>>,
358}
359
360impl ComponentHandler {
361    pub fn new(parameter_changes: Arc<Mutex<Vec<(u32, f64)>>>) -> Self {
362        Self { parameter_changes }
363    }
364}
365
366impl Class for ComponentHandler {
367    type Interfaces = (IComponentHandler,);
368}
369
370impl IComponentHandlerTrait for ComponentHandler {
371    unsafe fn beginEdit(&self, _id: ParamID) -> tresult {
372        kResultOk
373    }
374
375    unsafe fn performEdit(&self, id: ParamID, value_normalized: ParamValue) -> tresult {
376        if let Ok(mut changes) = self.parameter_changes.lock() {
377            changes.push((id, value_normalized));
378        }
379        kResultOk
380    }
381
382    unsafe fn endEdit(&self, _id: ParamID) -> tresult {
383        kResultOk
384    }
385
386    unsafe fn restartComponent(&self, _flags: i32) -> tresult {
387        kResultOk
388    }
389}
390
391/// Safe wrapper around a VST3 plugin instance
392pub struct PluginInstance {
393    pub component: ComPtr<IComponent>,
394    pub audio_processor: Option<ComPtr<IAudioProcessor>>,
395    pub edit_controller: Option<ComPtr<IEditController>>,
396    host_context: Box<HostApplicationContext>,
397    component_handler: Option<ComWrapper<ComponentHandler>>,
398    pub parameter_changes: Arc<Mutex<Vec<(u32, f64)>>>,
399}
400
401impl std::fmt::Debug for PluginInstance {
402    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
403        f.debug_struct("PluginInstance")
404            .field("component", &"<COM ptr>")
405            .field("audio_processor", &self.audio_processor.is_some())
406            .field("edit_controller", &self.edit_controller.is_some())
407            .finish()
408    }
409}
410
411impl PluginInstance {
412    fn new(component: ComPtr<IComponent>) -> Self {
413        Self {
414            component,
415            audio_processor: None,
416            edit_controller: None,
417            host_context: Box::new(HostApplicationContext::new()),
418            component_handler: None,
419            parameter_changes: Arc::new(Mutex::new(Vec::new())),
420        }
421    }
422
423    pub fn audio_bus_counts(&self) -> (usize, usize) {
424        use vst3::Steinberg::Vst::{BusDirections_, IComponentTrait, MediaTypes_};
425
426        let input_count = unsafe {
427            self.component
428                .getBusCount(MediaTypes_::kAudio as i32, BusDirections_::kInput as i32)
429        }
430        .max(0) as usize;
431        let output_count = unsafe {
432            self.component
433                .getBusCount(MediaTypes_::kAudio as i32, BusDirections_::kOutput as i32)
434        }
435        .max(0) as usize;
436        (input_count, output_count)
437    }
438
439    pub fn event_bus_counts(&self) -> (usize, usize) {
440        use vst3::Steinberg::Vst::{BusDirections_, IComponentTrait, MediaTypes_};
441
442        let input_count = unsafe {
443            self.component
444                .getBusCount(MediaTypes_::kEvent as i32, BusDirections_::kInput as i32)
445        }
446        .max(0) as usize;
447        let output_count = unsafe {
448            self.component
449                .getBusCount(MediaTypes_::kEvent as i32, BusDirections_::kOutput as i32)
450        }
451        .max(0) as usize;
452        (input_count, output_count)
453    }
454
455    pub fn main_audio_channel_counts(&self) -> (usize, usize) {
456        use vst3::Steinberg::Vst::{BusDirections_, BusTypes_, IComponentTrait, MediaTypes_};
457
458        let main_channels_for_direction = |direction: i32| -> usize {
459            let bus_count = unsafe {
460                self.component
461                    .getBusCount(MediaTypes_::kAudio as i32, direction)
462            }
463            .max(0) as usize;
464            if bus_count == 0 {
465                return 0;
466            }
467
468            let mut first_nonzero = 0usize;
469            for idx in 0..bus_count {
470                let mut info: vst3::Steinberg::Vst::BusInfo = unsafe { std::mem::zeroed() };
471                let result = unsafe {
472                    self.component.getBusInfo(
473                        MediaTypes_::kAudio as i32,
474                        direction,
475                        idx as i32,
476                        &mut info,
477                    )
478                };
479                if result != kResultOk {
480                    continue;
481                }
482                let channels = info.channelCount.max(0) as usize;
483                if channels > 0 && first_nonzero == 0 {
484                    first_nonzero = channels;
485                }
486                if info.busType == BusTypes_::kMain as i32 {
487                    return channels.max(1);
488                }
489            }
490
491            first_nonzero.max(1)
492        };
493
494        (
495            main_channels_for_direction(BusDirections_::kInput as i32),
496            main_channels_for_direction(BusDirections_::kOutput as i32),
497        )
498    }
499
500    /// Initialize the component
501    #[allow(clippy::unnecessary_cast)]
502    pub fn initialize(&mut self, factory: &PluginFactory) -> Result<(), String> {
503        use vst3::Steinberg::IPluginBaseTrait;
504        use vst3::Steinberg::Vst::{IComponentTrait, IEditControllerTrait};
505
506        // Pass a stable host application context for plugins that require IHostApplication.
507        let context = &mut self.host_context.host as *mut IHostApplication as *mut FUnknown;
508        let result = unsafe { self.component.initialize(context) };
509
510        if result != kResultOk {
511            return Err(format!(
512                "Failed to initialize component (result: {})",
513                result
514            ));
515        }
516
517        // Query for IAudioProcessor
518        let mut processor_ptr: *mut c_void = std::ptr::null_mut();
519        let result = unsafe {
520            // Access the vtable through the raw pointer
521            let component_raw = self.component.as_ptr();
522            let vtbl = (*component_raw).vtbl;
523            let query_interface = (*vtbl).base.base.queryInterface;
524            // Cast IID from [u8; 16] to [i8; 16]
525            let iid = std::mem::transmute::<&[u8; 16], &[i8; 16]>(&IAudioProcessor::IID);
526            query_interface(component_raw as *mut _, iid, &mut processor_ptr)
527        };
528
529        if result == kResultOk && !processor_ptr.is_null() {
530            self.audio_processor =
531                unsafe { ComPtr::from_raw(processor_ptr as *mut IAudioProcessor) };
532        }
533
534        // Query for IEditController directly from component first.
535        let mut controller_ptr: *mut c_void = std::ptr::null_mut();
536        let query_result = unsafe {
537            let component_raw = self.component.as_ptr();
538            let vtbl = (*component_raw).vtbl;
539            let query_interface = (*vtbl).base.base.queryInterface;
540            let iid = std::mem::transmute::<&[u8; 16], &[i8; 16]>(&IEditController::IID);
541            query_interface(component_raw as *mut _, iid, &mut controller_ptr)
542        };
543        if query_result == kResultOk && !controller_ptr.is_null() {
544            self.edit_controller =
545                unsafe { ComPtr::from_raw(controller_ptr as *mut IEditController) };
546        }
547
548        // If not available directly, instantiate the dedicated controller class.
549        if self.edit_controller.is_none() {
550            let mut controller_cid: TUID = [0; 16];
551            let cid_result = unsafe { self.component.getControllerClassId(&mut controller_cid) };
552            if cid_result == kResultOk {
553                let mut maybe_controller = factory.create_edit_controller(&controller_cid).ok();
554                if let Some(controller) = maybe_controller.as_mut() {
555                    let controller_context =
556                        &mut self.host_context.host as *mut IHostApplication as *mut FUnknown;
557                    let init_result = unsafe { controller.initialize(controller_context) };
558                    if init_result != kResultOk {
559                        maybe_controller = None;
560                    }
561                }
562                self.edit_controller = maybe_controller;
563            }
564        }
565
566        if let Some(controller) = self.edit_controller.as_ref() {
567            let handler = ComWrapper::new(ComponentHandler::new(self.parameter_changes.clone()));
568            if let Some(handler_ptr) = handler.to_com_ptr::<IComponentHandler>() {
569                let _ = unsafe { controller.setComponentHandler(handler_ptr.into_raw()) };
570            }
571            self.component_handler = Some(handler);
572        }
573
574        // Connect component and controller via IConnectionPoint when separate
575        if let Some(ref controller) = self.edit_controller {
576            let _ = connect_component_and_controller(&self.component, controller);
577        }
578
579        Ok(())
580    }
581
582    /// Query parameter metadata from the edit controller.
583    /// Wrapped in catch_unwind so that buggy plugins don't crash the host.
584    pub fn query_parameters(&self) -> Vec<super::port::ParameterInfo> {
585        let Some(controller) = self.edit_controller.as_ref() else {
586            return Vec::new();
587        };
588
589        let result = protected_call(|| unsafe {
590            use vst3::Steinberg::Vst::IEditControllerTrait;
591            let mut params = Vec::new();
592            let count = controller.getParameterCount();
593            for i in 0..count {
594                let mut info: ParameterInfo = std::mem::zeroed();
595                if controller.getParameterInfo(i, &mut info) != kResultOk {
596                    continue;
597                }
598                let title = extract_string128(&info.title);
599                let short_title = extract_string128(&info.shortTitle);
600                let units = extract_string128(&info.units);
601                let default_value = controller.getParamNormalized(info.id);
602                params.push(super::port::ParameterInfo {
603                    id: info.id,
604                    title,
605                    short_title,
606                    units,
607                    step_count: info.stepCount,
608                    default_value,
609                    flags: info.flags,
610                });
611            }
612            params
613        });
614
615        result.unwrap_or_default()
616    }
617
618    /// Set the component active/inactive
619    pub fn set_active(&mut self, active: bool) -> Result<(), String> {
620        let result = unsafe { self.component.setActive(if active { 1 } else { 0 }) };
621
622        if result != kResultOk {
623            return Err(format!("Failed to set active state (result: {})", result));
624        }
625
626        Ok(())
627    }
628
629    /// Setup processing parameters
630    pub fn setup_processing(
631        &mut self,
632        sample_rate: f64,
633        max_samples: i32,
634        input_channels: i32,
635        output_channels: i32,
636    ) -> Result<(), String> {
637        use vst3::Steinberg::Vst::{
638            BusDirections_, BusInfo, BusInfo_::BusFlags_ as BusFlags, BusTypes_,
639            IAudioProcessorTrait, IComponentTrait, MediaTypes_, SpeakerArr,
640        };
641
642        let processor = self
643            .audio_processor
644            .as_ref()
645            .ok_or("No audio processor available")?;
646
647        let sample_size_result = unsafe { processor.canProcessSampleSize(kSample32 as i32) };
648        if sample_size_result != kResultOk {
649            return Err(format!(
650                "Plugin does not support 32-bit sample size (result: {})",
651                sample_size_result
652            ));
653        }
654
655        let configure_audio_buses = |direction: i32, requested_channels: i32| {
656            let bus_count = unsafe {
657                self.component
658                    .getBusCount(MediaTypes_::kAudio as i32, direction)
659            }
660            .max(0) as usize;
661            if bus_count == 0 {
662                return Vec::new();
663            }
664
665            let mut infos: Vec<BusInfo> = Vec::with_capacity(bus_count);
666            for idx in 0..bus_count {
667                let mut info: BusInfo = unsafe { std::mem::zeroed() };
668                let r = unsafe {
669                    self.component.getBusInfo(
670                        MediaTypes_::kAudio as i32,
671                        direction,
672                        idx as i32,
673                        &mut info,
674                    )
675                };
676                if r != kResultOk {
677                    info.channelCount = if idx == 0 { 2 } else { 0 };
678                    info.busType = if idx == 0 {
679                        BusTypes_::kMain as i32
680                    } else {
681                        BusTypes_::kAux as i32
682                    };
683                    #[allow(clippy::unnecessary_cast)]
684                    {
685                        info.flags = if idx == 0 {
686                            BusFlags::kDefaultActive as u32
687                        } else {
688                            0
689                        };
690                    }
691                }
692                infos.push(info);
693            }
694
695            let mut remaining = requested_channels.max(0);
696            let mut active = vec![false; bus_count];
697            let mut arrangements = vec![SpeakerArr::kEmpty; bus_count];
698
699            let mut ordered: Vec<usize> = (0..bus_count)
700                .filter(|&idx| infos[idx].busType == BusTypes_::kMain as i32)
701                .collect();
702            ordered.extend(
703                (0..bus_count).filter(|&idx| infos[idx].busType != BusTypes_::kMain as i32),
704            );
705
706            for idx in ordered {
707                if remaining <= 0 {
708                    break;
709                }
710                let bus_channels = infos[idx].channelCount.max(1);
711                let allocate = remaining.min(bus_channels);
712                if allocate > 0 {
713                    active[idx] = true;
714                    arrangements[idx] = if allocate > 1 {
715                        SpeakerArr::kStereo
716                    } else {
717                        SpeakerArr::kMono
718                    };
719                    remaining -= allocate;
720                }
721            }
722
723            if requested_channels > 0 && !active.iter().any(|v| *v) {
724                active[0] = true;
725                arrangements[0] = if requested_channels > 1 {
726                    SpeakerArr::kStereo
727                } else {
728                    SpeakerArr::kMono
729                };
730            }
731
732            for (idx, is_active) in active.iter().enumerate().take(bus_count) {
733                let _ = unsafe {
734                    self.component.activateBus(
735                        MediaTypes_::kAudio as i32,
736                        direction,
737                        idx as i32,
738                        if *is_active { 1 } else { 0 },
739                    )
740                };
741            }
742
743            arrangements
744        };
745
746        let mut input_arrangements =
747            configure_audio_buses(BusDirections_::kInput as i32, input_channels);
748        let mut output_arrangements =
749            configure_audio_buses(BusDirections_::kOutput as i32, output_channels);
750        if !input_arrangements.is_empty() || !output_arrangements.is_empty() {
751            let _ = unsafe {
752                processor.setBusArrangements(
753                    if input_arrangements.is_empty() {
754                        std::ptr::null_mut()
755                    } else {
756                        input_arrangements.as_mut_ptr()
757                    },
758                    input_arrangements.len() as i32,
759                    if output_arrangements.is_empty() {
760                        std::ptr::null_mut()
761                    } else {
762                        output_arrangements.as_mut_ptr()
763                    },
764                    output_arrangements.len() as i32,
765                )
766            };
767        }
768
769        let event_in_buses = unsafe {
770            self.component
771                .getBusCount(MediaTypes_::kEvent as i32, BusDirections_::kInput as i32)
772        }
773        .max(0) as usize;
774        for idx in 0..event_in_buses {
775            let _ = unsafe {
776                self.component.activateBus(
777                    MediaTypes_::kEvent as i32,
778                    BusDirections_::kInput as i32,
779                    idx as i32,
780                    1,
781                )
782            };
783        }
784        let event_out_buses = unsafe {
785            self.component
786                .getBusCount(MediaTypes_::kEvent as i32, BusDirections_::kOutput as i32)
787        }
788        .max(0) as usize;
789        for idx in 0..event_out_buses {
790            let _ = unsafe {
791                self.component.activateBus(
792                    MediaTypes_::kEvent as i32,
793                    BusDirections_::kOutput as i32,
794                    idx as i32,
795                    1,
796                )
797            };
798        }
799
800        let mut setup = ProcessSetup {
801            processMode: kRealtime as i32,
802            symbolicSampleSize: kSample32 as i32,
803            maxSamplesPerBlock: max_samples,
804            sampleRate: sample_rate,
805        };
806
807        let result = unsafe { processor.setupProcessing(&mut setup) };
808
809        if result != kResultOk {
810            return Err(format!("Failed to setup processing (result: {})", result));
811        }
812
813        Ok(())
814    }
815
816    pub fn start_processing(&mut self) -> Result<(), String> {
817        use vst3::Steinberg::Vst::IAudioProcessorTrait;
818
819        let Some(processor) = &self.audio_processor else {
820            return Ok(());
821        };
822        let result = unsafe { processor.setProcessing(1) };
823        if result != kResultOk {
824            return Err(format!(
825                "Failed to enable processing state (result: {})",
826                result
827            ));
828        }
829        Ok(())
830    }
831
832    pub fn stop_processing(&mut self) {
833        use vst3::Steinberg::Vst::IAudioProcessorTrait;
834
835        if let Some(processor) = &self.audio_processor {
836            unsafe {
837                let _ = processor.setProcessing(0);
838            }
839        }
840    }
841
842    /// Terminate the component
843    pub fn terminate(&mut self) -> Result<(), String> {
844        use vst3::Steinberg::IPluginBaseTrait;
845
846        let result = unsafe { self.component.terminate() };
847
848        if result != kResultOk {
849            return Err(format!(
850                "Failed to terminate component (result: {})",
851                result
852            ));
853        }
854
855        Ok(())
856    }
857}
858
859#[repr(C)]
860struct HostApplicationContext {
861    host: IHostApplication,
862    run_loop: HostRunLoopContext,
863    ref_count: AtomicU32,
864}
865
866impl HostApplicationContext {
867    fn new() -> Self {
868        Self {
869            host: IHostApplication {
870                vtbl: &HOST_APPLICATION_VTBL,
871            },
872            run_loop: HostRunLoopContext::new(),
873            ref_count: AtomicU32::new(1),
874        }
875    }
876}
877
878#[repr(C)]
879struct HostRunLoopContext {
880    iface: Linux::IRunLoop,
881    ref_count: AtomicU32,
882}
883
884impl HostRunLoopContext {
885    fn new() -> Self {
886        Self {
887            iface: Linux::IRunLoop {
888                vtbl: &HOST_RUN_LOOP_VTBL,
889            },
890            ref_count: AtomicU32::new(1),
891        }
892    }
893}
894
895fn connect_component_and_controller(
896    component: &ComPtr<IComponent>,
897    controller: &ComPtr<IEditController>,
898) -> Result<(), String> {
899    let comp_cp = component.cast::<IConnectionPoint>();
900    let ctrl_cp = controller.cast::<IConnectionPoint>();
901
902    if let (Some(comp_cp), Some(ctrl_cp)) = (comp_cp, ctrl_cp) {
903        unsafe {
904            let result1 = comp_cp.connect(ctrl_cp.as_ptr());
905            let result2 = ctrl_cp.connect(comp_cp.as_ptr());
906            if result1 == kResultOk && result2 == kResultOk {
907                Ok(())
908            } else {
909                Err(format!(
910                    "Connection failed: comp->ctrl={:#x}, ctrl->comp={:#x}",
911                    result1, result2
912                ))
913            }
914        }
915    } else {
916        Ok(())
917    }
918}
919
920unsafe extern "system" fn host_query_interface(
921    this: *mut FUnknown,
922    iid: *const TUID,
923    obj: *mut *mut c_void,
924) -> tresult {
925    if this.is_null() || iid.is_null() {
926        if !obj.is_null() {
927            // SAFETY: Caller provides output storage.
928            unsafe {
929                *obj = std::ptr::null_mut();
930            }
931        }
932        return kNoInterface;
933    }
934
935    let iid_bytes = unsafe { &*iid };
936    let requested_host = iid_bytes
937        .iter()
938        .zip(IHostApplication::IID.iter())
939        .all(|(a, b)| (*a as u8) == *b);
940    let requested_unknown = iid_bytes
941        .iter()
942        .zip(FUnknown::IID.iter())
943        .all(|(a, b)| (*a as u8) == *b);
944    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
945    let requested_run_loop = iid_bytes
946        .iter()
947        .zip(Linux::IRunLoop::IID.iter())
948        .all(|(a, b)| (*a as u8) == *b);
949    #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd")))]
950    let requested_run_loop = false;
951    if !(requested_host || requested_unknown || requested_run_loop) {
952        if !obj.is_null() {
953            // SAFETY: Caller provides output storage.
954            unsafe {
955                *obj = std::ptr::null_mut();
956            }
957        }
958        return kNoInterface;
959    }
960
961    let ctx = this as *mut HostApplicationContext;
962    if !ctx.is_null() {
963        unsafe {
964            if requested_run_loop {
965                (*ctx).run_loop.ref_count.fetch_add(1, Ordering::Relaxed);
966            } else {
967                // SAFETY: `this` is the first field in HostApplicationContext.
968                (*ctx).ref_count.fetch_add(1, Ordering::Relaxed);
969            }
970        }
971    }
972
973    if !obj.is_null() {
974        unsafe {
975            if requested_run_loop {
976                *obj = (&mut (*ctx).run_loop.iface as *mut Linux::IRunLoop).cast::<c_void>();
977            } else {
978                // SAFETY: Caller supplies storage for out-pointer.
979                *obj = this.cast::<c_void>();
980            }
981        }
982    }
983    kResultOk
984}
985
986unsafe extern "system" fn host_add_ref(this: *mut FUnknown) -> uint32 {
987    if this.is_null() {
988        return 0;
989    }
990    let ctx = this as *mut HostApplicationContext;
991    // SAFETY: `this` points to embedded host interface at offset 0.
992    unsafe { (*ctx).ref_count.fetch_add(1, Ordering::Relaxed) + 1 }
993}
994
995unsafe extern "system" fn host_release(this: *mut FUnknown) -> uint32 {
996    if this.is_null() {
997        return 0;
998    }
999    let ctx = this as *mut HostApplicationContext;
1000    // SAFETY: `this` points to embedded host interface at offset 0.
1001    unsafe { (*ctx).ref_count.fetch_sub(1, Ordering::Relaxed) - 1 }
1002}
1003
1004unsafe extern "system" fn host_get_name(
1005    _this: *mut IHostApplication,
1006    name: *mut String128,
1007) -> tresult {
1008    if name.is_null() {
1009        return kNoInterface;
1010    }
1011    let encoded: Vec<u16> = "Maolan".encode_utf16().collect();
1012    // SAFETY: `name` points to writable `String128`.
1013    unsafe {
1014        (*name).fill(0);
1015        for (idx, ch) in encoded
1016            .iter()
1017            .take((*name).len().saturating_sub(1))
1018            .enumerate()
1019        {
1020            (*name)[idx] = *ch;
1021        }
1022    }
1023    kResultOk
1024}
1025
1026unsafe extern "system" fn host_create_instance(
1027    _this: *mut IHostApplication,
1028    cid: *mut TUID,
1029    iid: *mut TUID,
1030    obj: *mut *mut c_void,
1031) -> tresult {
1032    if obj.is_null() {
1033        return kInvalidArgument;
1034    }
1035    // SAFETY: caller provided out pointer.
1036    unsafe {
1037        *obj = std::ptr::null_mut();
1038    }
1039
1040    let wants_message =
1041        iid_ptr_matches(cid, &IMessage::IID) || iid_ptr_matches(iid, &IMessage::IID);
1042    let wants_attributes =
1043        iid_ptr_matches(cid, &IAttributeList::IID) || iid_ptr_matches(iid, &IAttributeList::IID);
1044    if wants_message {
1045        let message = Box::new(HostMessage::new());
1046        let raw = Box::into_raw(message);
1047        // SAFETY: `iface` is first field and valid for COM client usage.
1048        unsafe {
1049            *obj = (&mut (*raw).iface as *mut IMessage).cast::<c_void>();
1050        }
1051        return kResultOk;
1052    }
1053
1054    if wants_attributes {
1055        let attrs = Box::new(HostAttributeList::new());
1056        let raw = Box::into_raw(attrs);
1057        // SAFETY: `iface` is first field and valid for COM client usage.
1058        unsafe {
1059            *obj = (&mut (*raw).iface as *mut IAttributeList).cast::<c_void>();
1060        }
1061        return kResultOk;
1062    }
1063
1064    kNotImplemented
1065}
1066
1067static HOST_APPLICATION_VTBL: IHostApplicationVtbl = IHostApplicationVtbl {
1068    base: FUnknownVtbl {
1069        queryInterface: host_query_interface,
1070        addRef: host_add_ref,
1071        release: host_release,
1072    },
1073    getName: host_get_name,
1074    createInstance: host_create_instance,
1075};
1076
1077unsafe extern "system" fn run_loop_query_interface(
1078    this: *mut FUnknown,
1079    iid: *const TUID,
1080    obj: *mut *mut c_void,
1081) -> tresult {
1082    if this.is_null() || iid.is_null() || obj.is_null() {
1083        return kInvalidArgument;
1084    }
1085    let requested_run_loop = iid_ptr_matches(iid, &Linux::IRunLoop::IID);
1086    let requested_unknown = iid_ptr_matches(iid, &FUnknown::IID);
1087    if !(requested_run_loop || requested_unknown) {
1088        unsafe { *obj = std::ptr::null_mut() };
1089        return kNoInterface;
1090    }
1091    let ctx = this as *mut HostRunLoopContext;
1092    unsafe {
1093        (*ctx).ref_count.fetch_add(1, Ordering::Relaxed);
1094        *obj = this.cast::<c_void>();
1095    }
1096    kResultOk
1097}
1098
1099unsafe extern "system" fn run_loop_add_ref(this: *mut FUnknown) -> uint32 {
1100    if this.is_null() {
1101        return 0;
1102    }
1103    let ctx = this as *mut HostRunLoopContext;
1104    unsafe { (*ctx).ref_count.fetch_add(1, Ordering::Relaxed) + 1 }
1105}
1106
1107unsafe extern "system" fn run_loop_release(this: *mut FUnknown) -> uint32 {
1108    if this.is_null() {
1109        return 0;
1110    }
1111    let ctx = this as *mut HostRunLoopContext;
1112    unsafe { (*ctx).ref_count.fetch_sub(1, Ordering::Relaxed) - 1 }
1113}
1114
1115unsafe extern "system" fn run_loop_register_event_handler(
1116    _this: *mut Linux::IRunLoop,
1117    handler: *mut Linux::IEventHandler,
1118    fd: i32,
1119) -> tresult {
1120    if handler.is_null() {
1121        return kInvalidArgument;
1122    }
1123    let unknown = handler as *mut FUnknown;
1124    unsafe {
1125        let _ = ((*(*unknown).vtbl).addRef)(unknown);
1126    }
1127    let mut state = run_loop_state().lock().expect("run loop mutex poisoned");
1128    state.event_handlers.push(RunLoopEventHandler {
1129        handler: handler as usize,
1130        fd,
1131    });
1132    kResultOk
1133}
1134
1135unsafe extern "system" fn run_loop_unregister_event_handler(
1136    _this: *mut Linux::IRunLoop,
1137    handler: *mut Linux::IEventHandler,
1138) -> tresult {
1139    if handler.is_null() {
1140        return kInvalidArgument;
1141    }
1142    let mut state = run_loop_state().lock().expect("run loop mutex poisoned");
1143    state.event_handlers.retain(|h| {
1144        if h.handler == handler as usize {
1145            let unknown = handler as *mut FUnknown;
1146            unsafe {
1147                let _ = ((*(*unknown).vtbl).release)(unknown);
1148            }
1149            false
1150        } else {
1151            true
1152        }
1153    });
1154    kResultOk
1155}
1156
1157unsafe extern "system" fn run_loop_register_timer(
1158    _this: *mut Linux::IRunLoop,
1159    handler: *mut Linux::ITimerHandler,
1160    milliseconds: u64,
1161) -> tresult {
1162    if handler.is_null() {
1163        return kInvalidArgument;
1164    }
1165    let unknown = handler as *mut FUnknown;
1166    unsafe {
1167        let _ = ((*(*unknown).vtbl).addRef)(unknown);
1168    }
1169    let interval = Duration::from_millis(milliseconds.max(1));
1170    let mut state = run_loop_state().lock().expect("run loop mutex poisoned");
1171    state.timer_handlers.push(RunLoopTimerHandler {
1172        handler: handler as usize,
1173        interval,
1174        next_tick: Instant::now() + interval,
1175    });
1176    unsafe {
1177        // Kick timer once so plugins that defer first paint to timer callbacks can initialize.
1178        ((*(*handler).vtbl).onTimer)(handler);
1179    }
1180    kResultOk
1181}
1182
1183unsafe extern "system" fn run_loop_unregister_timer(
1184    _this: *mut Linux::IRunLoop,
1185    handler: *mut Linux::ITimerHandler,
1186) -> tresult {
1187    if handler.is_null() {
1188        return kInvalidArgument;
1189    }
1190    let mut state = run_loop_state().lock().expect("run loop mutex poisoned");
1191    state.timer_handlers.retain(|t| {
1192        if t.handler == handler as usize {
1193            let unknown = handler as *mut FUnknown;
1194            unsafe {
1195                let _ = ((*(*unknown).vtbl).release)(unknown);
1196            }
1197            false
1198        } else {
1199            true
1200        }
1201    });
1202    kResultOk
1203}
1204
1205static HOST_RUN_LOOP_VTBL: Linux::IRunLoopVtbl = Linux::IRunLoopVtbl {
1206    base: FUnknownVtbl {
1207        queryInterface: run_loop_query_interface,
1208        addRef: run_loop_add_ref,
1209        release: run_loop_release,
1210    },
1211    registerEventHandler: run_loop_register_event_handler,
1212    unregisterEventHandler: run_loop_unregister_event_handler,
1213    registerTimer: run_loop_register_timer,
1214    unregisterTimer: run_loop_unregister_timer,
1215};
1216
1217#[repr(C)]
1218struct HostMessage {
1219    iface: IMessage,
1220    ref_count: AtomicU32,
1221    message_id: FIDString,
1222    attributes: *mut IAttributeList,
1223}
1224
1225impl HostMessage {
1226    fn new() -> Self {
1227        let attrs = Box::new(HostAttributeList::new());
1228        let attrs_raw = Box::into_raw(attrs);
1229        Self {
1230            iface: IMessage {
1231                vtbl: &HOST_MESSAGE_VTBL,
1232            },
1233            ref_count: AtomicU32::new(1),
1234            message_id: c"".as_ptr(),
1235            // SAFETY: `iface` is first field.
1236            attributes: unsafe { &mut (*attrs_raw).iface as *mut IAttributeList },
1237        }
1238    }
1239}
1240
1241#[repr(C)]
1242struct HostAttributeList {
1243    iface: IAttributeList,
1244    ref_count: AtomicU32,
1245}
1246
1247impl HostAttributeList {
1248    fn new() -> Self {
1249        Self {
1250            iface: IAttributeList {
1251                vtbl: &HOST_ATTRIBUTE_LIST_VTBL,
1252            },
1253            ref_count: AtomicU32::new(1),
1254        }
1255    }
1256}
1257
1258fn iid_ptr_matches(iid_ptr: *const TUID, guid: &[u8; 16]) -> bool {
1259    if iid_ptr.is_null() {
1260        return false;
1261    }
1262    // SAFETY: caller provides valid IID pointer for the duration of call.
1263    let iid = unsafe { &*iid_ptr };
1264    iid.iter()
1265        .zip(guid.iter())
1266        .all(|(lhs, rhs)| (*lhs as u8) == *rhs)
1267}
1268
1269unsafe extern "system" fn host_message_query_interface(
1270    this: *mut FUnknown,
1271    iid: *const TUID,
1272    obj: *mut *mut c_void,
1273) -> tresult {
1274    if this.is_null() || iid.is_null() || obj.is_null() {
1275        return kInvalidArgument;
1276    }
1277    let requested_message = iid_ptr_matches(iid, &IMessage::IID);
1278    let requested_unknown = iid_ptr_matches(iid, &FUnknown::IID);
1279    if !(requested_message || requested_unknown) {
1280        // SAFETY: out pointer valid.
1281        unsafe { *obj = std::ptr::null_mut() };
1282        return kNoInterface;
1283    }
1284    let msg = this as *mut HostMessage;
1285    // SAFETY: message context pointer is valid.
1286    unsafe {
1287        (*msg).ref_count.fetch_add(1, Ordering::Relaxed);
1288        *obj = this.cast::<c_void>();
1289    }
1290    kResultOk
1291}
1292
1293unsafe extern "system" fn host_message_add_ref(this: *mut FUnknown) -> uint32 {
1294    if this.is_null() {
1295        return 0;
1296    }
1297    let msg = this as *mut HostMessage;
1298    // SAFETY: pointer valid for atomic update.
1299    unsafe { (*msg).ref_count.fetch_add(1, Ordering::Relaxed) + 1 }
1300}
1301
1302unsafe extern "system" fn host_message_release(this: *mut FUnknown) -> uint32 {
1303    if this.is_null() {
1304        return 0;
1305    }
1306    let msg = this as *mut HostMessage;
1307    // SAFETY: pointer valid for atomic update.
1308    let remaining = unsafe { (*msg).ref_count.fetch_sub(1, Ordering::AcqRel) - 1 };
1309    if remaining == 0 {
1310        // SAFETY: Release the message-owned reference to attributes before freeing message.
1311        unsafe {
1312            if !(*msg).attributes.is_null() {
1313                let attrs_unknown = (*msg).attributes.cast::<FUnknown>();
1314                let _ = host_attr_release(attrs_unknown);
1315                (*msg).attributes = std::ptr::null_mut();
1316            }
1317            let _ = Box::from_raw(msg);
1318        }
1319    }
1320    remaining
1321}
1322
1323unsafe extern "system" fn host_message_get_id(this: *mut IMessage) -> FIDString {
1324    if this.is_null() {
1325        return c"".as_ptr();
1326    }
1327    let msg = this as *mut HostMessage;
1328    // SAFETY: valid pointer for message context.
1329    unsafe { (*msg).message_id }
1330}
1331
1332unsafe extern "system" fn host_message_set_id(this: *mut IMessage, id: FIDString) {
1333    if this.is_null() {
1334        return;
1335    }
1336    let msg = this as *mut HostMessage;
1337    // SAFETY: we only keep borrowed pointer; plugin controls lifetime for call scope.
1338    unsafe {
1339        (*msg).message_id = if id.is_null() { c"".as_ptr() } else { id };
1340    }
1341}
1342
1343unsafe extern "system" fn host_message_get_attributes(this: *mut IMessage) -> *mut IAttributeList {
1344    if this.is_null() {
1345        return std::ptr::null_mut();
1346    }
1347    let msg = this as *mut HostMessage;
1348    // SAFETY: return a referenced COM pointer to caller.
1349    unsafe {
1350        if !(*msg).attributes.is_null() {
1351            let attrs_unknown = (*msg).attributes.cast::<FUnknown>();
1352            let _ = host_attr_add_ref(attrs_unknown);
1353        }
1354        (*msg).attributes
1355    }
1356}
1357
1358static HOST_MESSAGE_VTBL: IMessageVtbl = IMessageVtbl {
1359    base: FUnknownVtbl {
1360        queryInterface: host_message_query_interface,
1361        addRef: host_message_add_ref,
1362        release: host_message_release,
1363    },
1364    getMessageID: host_message_get_id,
1365    setMessageID: host_message_set_id,
1366    getAttributes: host_message_get_attributes,
1367};
1368
1369unsafe extern "system" fn host_attr_query_interface(
1370    this: *mut FUnknown,
1371    iid: *const TUID,
1372    obj: *mut *mut c_void,
1373) -> tresult {
1374    if this.is_null() || iid.is_null() || obj.is_null() {
1375        return kInvalidArgument;
1376    }
1377    let requested_attr = iid_ptr_matches(iid, &IAttributeList::IID);
1378    let requested_unknown = iid_ptr_matches(iid, &FUnknown::IID);
1379    if !(requested_attr || requested_unknown) {
1380        // SAFETY: out pointer valid.
1381        unsafe { *obj = std::ptr::null_mut() };
1382        return kNoInterface;
1383    }
1384    let attrs = this as *mut HostAttributeList;
1385    // SAFETY: pointer valid.
1386    unsafe {
1387        (*attrs).ref_count.fetch_add(1, Ordering::Relaxed);
1388        *obj = this.cast::<c_void>();
1389    }
1390    kResultOk
1391}
1392
1393unsafe extern "system" fn host_attr_add_ref(this: *mut FUnknown) -> uint32 {
1394    if this.is_null() {
1395        return 0;
1396    }
1397    let attrs = this as *mut HostAttributeList;
1398    // SAFETY: pointer valid.
1399    unsafe { (*attrs).ref_count.fetch_add(1, Ordering::Relaxed) + 1 }
1400}
1401
1402unsafe extern "system" fn host_attr_release(this: *mut FUnknown) -> uint32 {
1403    if this.is_null() {
1404        return 0;
1405    }
1406    let attrs = this as *mut HostAttributeList;
1407    // SAFETY: pointer valid.
1408    let remaining = unsafe { (*attrs).ref_count.fetch_sub(1, Ordering::AcqRel) - 1 };
1409    if remaining == 0 {
1410        // SAFETY: pointer was allocated with Box::into_raw.
1411        unsafe {
1412            let _ = Box::from_raw(attrs);
1413        }
1414    }
1415    remaining
1416}
1417
1418unsafe extern "system" fn host_attr_set_int(
1419    _this: *mut IAttributeList,
1420    _id: IAttrID,
1421    _value: i64,
1422) -> tresult {
1423    kResultOk
1424}
1425
1426unsafe extern "system" fn host_attr_get_int(
1427    _this: *mut IAttributeList,
1428    _id: IAttrID,
1429    value: *mut i64,
1430) -> tresult {
1431    if value.is_null() {
1432        return kInvalidArgument;
1433    }
1434    // SAFETY: caller provides writable pointer.
1435    unsafe { *value = 0 };
1436    kResultFalse
1437}
1438
1439unsafe extern "system" fn host_attr_set_float(
1440    _this: *mut IAttributeList,
1441    _id: IAttrID,
1442    _value: f64,
1443) -> tresult {
1444    kResultOk
1445}
1446
1447unsafe extern "system" fn host_attr_get_float(
1448    _this: *mut IAttributeList,
1449    _id: IAttrID,
1450    value: *mut f64,
1451) -> tresult {
1452    if value.is_null() {
1453        return kInvalidArgument;
1454    }
1455    // SAFETY: caller provides writable pointer.
1456    unsafe { *value = 0.0 };
1457    kResultFalse
1458}
1459
1460unsafe extern "system" fn host_attr_set_string(
1461    _this: *mut IAttributeList,
1462    _id: IAttrID,
1463    _string: *const TChar,
1464) -> tresult {
1465    kResultOk
1466}
1467
1468unsafe extern "system" fn host_attr_get_string(
1469    _this: *mut IAttributeList,
1470    _id: IAttrID,
1471    string: *mut TChar,
1472    size_in_bytes: u32,
1473) -> tresult {
1474    if string.is_null() || size_in_bytes < std::mem::size_of::<TChar>() as u32 {
1475        return kInvalidArgument;
1476    }
1477    // SAFETY: buffer has at least one TChar cell.
1478    unsafe { *string = 0 };
1479    kResultFalse
1480}
1481
1482unsafe extern "system" fn host_attr_set_binary(
1483    _this: *mut IAttributeList,
1484    _id: IAttrID,
1485    _data: *const c_void,
1486    _size_in_bytes: u32,
1487) -> tresult {
1488    kResultOk
1489}
1490
1491unsafe extern "system" fn host_attr_get_binary(
1492    _this: *mut IAttributeList,
1493    _id: IAttrID,
1494    data: *mut *const c_void,
1495    size_in_bytes: *mut u32,
1496) -> tresult {
1497    if data.is_null() || size_in_bytes.is_null() {
1498        return kInvalidArgument;
1499    }
1500    // SAFETY: caller provides writable pointers.
1501    unsafe {
1502        *data = std::ptr::null();
1503        *size_in_bytes = 0;
1504    }
1505    kResultFalse
1506}
1507
1508static HOST_ATTRIBUTE_LIST_VTBL: IAttributeListVtbl = IAttributeListVtbl {
1509    base: FUnknownVtbl {
1510        queryInterface: host_attr_query_interface,
1511        addRef: host_attr_add_ref,
1512        release: host_attr_release,
1513    },
1514    setInt: host_attr_set_int,
1515    getInt: host_attr_get_int,
1516    setFloat: host_attr_set_float,
1517    getFloat: host_attr_get_float,
1518    setString: host_attr_set_string,
1519    getString: host_attr_get_string,
1520    setBinary: host_attr_set_binary,
1521    getBinary: host_attr_get_binary,
1522};
1523
1524/// Get the actual module path from a VST3 bundle path
1525fn get_module_path(bundle_path: &Path) -> Result<std::path::PathBuf, String> {
1526    #[cfg(target_os = "macos")]
1527    {
1528        // macOS: .vst3/Contents/MacOS/plugin
1529        let module = bundle_path.join("Contents").join("MacOS").join(
1530            bundle_path
1531                .file_stem()
1532                .and_then(|s| s.to_str())
1533                .unwrap_or("plugin"),
1534        );
1535        if module.exists() {
1536            Ok(module)
1537        } else {
1538            Err(format!("VST3 module not found at {:?}", module))
1539        }
1540    }
1541
1542    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
1543    {
1544        // Linux/FreeBSD: .vst3/Contents/x86_64-linux/plugin.so
1545        let module = bundle_path
1546            .join("Contents")
1547            .join("x86_64-linux")
1548            .join(format!(
1549                "{}.so",
1550                bundle_path
1551                    .file_stem()
1552                    .and_then(|s| s.to_str())
1553                    .unwrap_or("plugin")
1554            ));
1555        if module.exists() {
1556            Ok(module)
1557        } else {
1558            Err(format!("VST3 module not found at {:?}", module))
1559        }
1560    }
1561
1562    #[cfg(target_os = "windows")]
1563    {
1564        // Windows: .vst3/Contents/{x86_64-win|x86-win}/plugin.vst3
1565        // Scan the architecture directory like rust-vst3-host does,
1566        // since binary names don't always match the bundle name.
1567        let contents = bundle_path.join("Contents");
1568        let arch_dir = if cfg!(target_arch = "x86_64") {
1569            contents.join("x86_64-win")
1570        } else {
1571            contents.join("x86-win")
1572        };
1573
1574        if let Ok(entries) = std::fs::read_dir(&arch_dir) {
1575            for entry in entries.flatten() {
1576                let file_path = entry.path();
1577                if file_path.is_file()
1578                    && file_path
1579                        .extension()
1580                        .is_some_and(|ext| ext.eq_ignore_ascii_case("vst3"))
1581                {
1582                    return Ok(file_path);
1583                }
1584            }
1585        }
1586
1587        // Fallback: try the other architecture directory
1588        let fallback_dir = if cfg!(target_arch = "x86_64") {
1589            contents.join("x86-win")
1590        } else {
1591            contents.join("x86_64-win")
1592        };
1593        if let Ok(entries) = std::fs::read_dir(&fallback_dir) {
1594            for entry in entries.flatten() {
1595                let file_path = entry.path();
1596                if file_path.is_file()
1597                    && file_path
1598                        .extension()
1599                        .is_some_and(|ext| ext.eq_ignore_ascii_case("vst3"))
1600                {
1601                    return Ok(file_path);
1602                }
1603            }
1604        }
1605
1606        Err(format!(
1607            "VST3 module not found under {:?}",
1608            bundle_path.join("Contents")
1609        ))
1610    }
1611
1612    #[cfg(not(any(
1613        target_os = "macos",
1614        target_os = "linux",
1615        target_os = "freebsd",
1616        target_os = "openbsd",
1617        target_os = "windows"
1618    )))]
1619    {
1620        Err("Unsupported platform".to_string())
1621    }
1622}
1623
1624fn extract_cstring(bytes: &[i8]) -> String {
1625    let len = bytes.iter().position(|&c| c == 0).unwrap_or(bytes.len());
1626    let u8_bytes: Vec<u8> = bytes[..len].iter().map(|&b| b as u8).collect();
1627    String::from_utf8_lossy(&u8_bytes).to_string()
1628}
1629
1630fn extract_string128(s: &String128) -> String {
1631    let len = s.iter().position(|&c| c == 0).unwrap_or(s.len());
1632    String::from_utf16_lossy(&s[..len])
1633}
1634
1635#[cfg(test)]
1636mod tests {
1637    use super::*;
1638    use std::fs::{self, File};
1639    use std::path::PathBuf;
1640    use std::time::{SystemTime, UNIX_EPOCH};
1641
1642    fn unique_temp_dir(name: &str) -> PathBuf {
1643        let nanos = SystemTime::now()
1644            .duration_since(UNIX_EPOCH)
1645            .expect("clock before epoch")
1646            .as_nanos();
1647        std::env::temp_dir().join(format!(
1648            "maolan-engine-{name}-{}-{nanos}",
1649            std::process::id()
1650        ))
1651    }
1652
1653    #[test]
1654    fn iid_ptr_matches_rejects_null_and_non_matching_guids() {
1655        let iid: TUID = [1; 16];
1656        let matching_guid = [1_u8; 16];
1657        let different_guid = [2_u8; 16];
1658
1659        assert!(!iid_ptr_matches(std::ptr::null(), &matching_guid));
1660        assert!(iid_ptr_matches(&iid, &matching_guid));
1661        assert!(!iid_ptr_matches(&iid, &different_guid));
1662    }
1663
1664    #[test]
1665    fn extract_cstring_stops_at_nul_and_uses_lossy_utf8() {
1666        let bytes = [b'A' as i8, b'B' as i8, -1, 0, b'Z' as i8];
1667
1668        assert_eq!(extract_cstring(&bytes), "AB\u{FFFD}");
1669    }
1670
1671    #[test]
1672    fn extract_cstring_uses_full_slice_when_not_nul_terminated() {
1673        let bytes = [b'X' as i8, b'Y' as i8, b'Z' as i8];
1674
1675        assert_eq!(extract_cstring(&bytes), "XYZ");
1676    }
1677
1678    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
1679    #[test]
1680    fn get_module_path_returns_unix_shared_object_path() {
1681        let bundle_path = unique_temp_dir("vst3-module").join("Example.vst3");
1682        let module_path = bundle_path
1683            .join("Contents")
1684            .join("x86_64-linux")
1685            .join("Example.so");
1686        fs::create_dir_all(module_path.parent().expect("module parent"))
1687            .expect("create module directory");
1688        File::create(&module_path).expect("create module file");
1689
1690        let resolved = get_module_path(&bundle_path).expect("resolve module path");
1691
1692        assert_eq!(resolved, module_path);
1693
1694        let _ = fs::remove_dir_all(bundle_path.parent().expect("bundle parent"));
1695    }
1696
1697    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
1698    #[test]
1699    fn get_module_path_errors_when_unix_module_is_missing() {
1700        let bundle_path = unique_temp_dir("missing-vst3").join("Missing.vst3");
1701        fs::create_dir_all(&bundle_path).expect("create bundle directory");
1702
1703        let err = get_module_path(&bundle_path).expect_err("missing module should error");
1704
1705        assert!(err.contains("Missing.so"));
1706
1707        let _ = fs::remove_dir_all(bundle_path.parent().expect("bundle parent"));
1708    }
1709
1710    #[cfg(target_os = "windows")]
1711    #[test]
1712    fn get_module_path_returns_windows_vst3_binary_path() {
1713        let bundle_path = unique_temp_dir("vst3-module").join("Example.vst3");
1714        let module_path = bundle_path
1715            .join("Contents")
1716            .join("x86_64-win")
1717            .join("Example.vst3");
1718        fs::create_dir_all(module_path.parent().expect("module parent"))
1719            .expect("create module directory");
1720        File::create(&module_path).expect("create module file");
1721
1722        let resolved = get_module_path(&bundle_path).expect("resolve module path");
1723
1724        assert_eq!(resolved, module_path);
1725
1726        let _ = fs::remove_dir_all(bundle_path.parent().expect("bundle parent"));
1727    }
1728
1729    #[cfg(target_os = "windows")]
1730    #[test]
1731    fn get_module_path_errors_when_windows_module_is_missing() {
1732        let bundle_path = unique_temp_dir("missing-vst3").join("Missing.vst3");
1733        fs::create_dir_all(&bundle_path).expect("create bundle directory");
1734
1735        let err = get_module_path(&bundle_path).expect_err("missing module should error");
1736
1737        assert!(err.contains("Contents"));
1738
1739        let _ = fs::remove_dir_all(bundle_path.parent().expect("bundle parent"));
1740    }
1741}