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