Skip to main content

maolan_engine/plugins/
clap.rs

1use crate::audio::io::AudioIO;
2use crate::midi::io::MidiEvent;
3use crate::mutex::UnsafeMutex;
4#[cfg(any(
5    target_os = "macos",
6    target_os = "linux",
7    target_os = "freebsd",
8    target_os = "openbsd"
9))]
10use crate::plugins::paths;
11use libloading::Library;
12use serde::{Deserialize, Serialize};
13use std::cell::Cell;
14use std::collections::HashMap;
15use std::ffi::{CStr, CString, c_char, c_void};
16use std::fmt;
17use std::path::{Path, PathBuf};
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU32, Ordering};
20use std::time::{Duration, Instant};
21
22#[derive(Clone, Debug, PartialEq)]
23pub struct ClapParameterInfo {
24    pub id: u32,
25    pub name: String,
26    pub module: String,
27    pub min_value: f64,
28    pub max_value: f64,
29    pub default_value: f64,
30}
31
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
33pub struct ClapPluginState {
34    pub bytes: Vec<u8>,
35}
36
37type AudioPortLayout = (Vec<usize>, usize);
38
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct ClapMidiOutputEvent {
41    pub port: usize,
42    pub event: MidiEvent,
43}
44
45#[derive(Clone, Copy, Debug, Default)]
46pub struct ClapTransportInfo {
47    pub transport_sample: usize,
48    pub playing: bool,
49    pub loop_enabled: bool,
50    pub loop_range_samples: Option<(usize, usize)>,
51    pub bpm: f64,
52    pub tsig_num: u16,
53    pub tsig_denom: u16,
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct ClapGuiInfo {
58    pub api: String,
59    pub supports_embedded: bool,
60}
61
62#[derive(Clone, Copy, Debug)]
63struct PendingParamValue {
64    param_id: u32,
65    value: f64,
66}
67
68#[derive(Clone, Copy, Debug)]
69pub struct ClapParamUpdate {
70    pub param_id: u32,
71    pub value: f64,
72}
73
74#[derive(Clone, Copy, Debug)]
75enum PendingParamEvent {
76    Value {
77        param_id: u32,
78        value: f64,
79        frame: u32,
80    },
81    GestureBegin {
82        param_id: u32,
83        frame: u32,
84    },
85    GestureEnd {
86        param_id: u32,
87        frame: u32,
88    },
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
92pub struct ClapPluginInfo {
93    pub name: String,
94    pub path: String,
95    pub capabilities: Option<ClapPluginCapabilities>,
96}
97
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
99pub struct ClapPluginCapabilities {
100    pub has_gui: bool,
101    pub gui_apis: Vec<String>,
102    pub supports_embedded: bool,
103    pub supports_floating: bool,
104    pub has_params: bool,
105    pub has_state: bool,
106    pub audio_inputs: usize,
107    pub audio_outputs: usize,
108    pub midi_inputs: usize,
109    pub midi_outputs: usize,
110}
111
112#[derive(Clone)]
113pub struct ClapProcessor {
114    path: String,
115    plugin_id: String,
116    name: String,
117    sample_rate: f64,
118    audio_inputs: Vec<Arc<AudioIO>>,
119    audio_outputs: Vec<Arc<AudioIO>>,
120    input_port_channels: Vec<usize>,
121    output_port_channels: Vec<usize>,
122    midi_input_ports: usize,
123    midi_output_ports: usize,
124    main_audio_inputs: usize,
125    main_audio_outputs: usize,
126    host_runtime: Arc<HostRuntime>,
127    plugin_handle: Arc<PluginHandle>,
128    param_infos: Arc<Vec<ClapParameterInfo>>,
129    param_values: Arc<UnsafeMutex<HashMap<u32, f64>>>,
130    pending_param_events: Arc<UnsafeMutex<Vec<PendingParamEvent>>>,
131    pending_param_events_ui: Arc<UnsafeMutex<Vec<PendingParamEvent>>>,
132    process_lock: Arc<UnsafeMutex<()>>,
133}
134
135impl fmt::Debug for ClapProcessor {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.debug_struct("ClapProcessor")
138            .field("path", &self.path)
139            .field("plugin_id", &self.plugin_id)
140            .field("name", &self.name)
141            .field("audio_inputs", &self.audio_inputs.len())
142            .field("audio_outputs", &self.audio_outputs.len())
143            .field("input_port_channels", &self.input_port_channels)
144            .field("output_port_channels", &self.output_port_channels)
145            .field("midi_input_ports", &self.midi_input_ports)
146            .field("midi_output_ports", &self.midi_output_ports)
147            .field("main_audio_inputs", &self.main_audio_inputs)
148            .field("main_audio_outputs", &self.main_audio_outputs)
149            .finish()
150    }
151}
152
153impl ClapProcessor {
154    pub fn new(
155        sample_rate: f64,
156        buffer_size: usize,
157        plugin_spec: &str,
158        input_count: usize,
159        output_count: usize,
160    ) -> Result<Self, String> {
161        let _thread_scope = HostThreadScope::enter_main();
162        let (plugin_path, plugin_id) = split_plugin_spec(plugin_spec);
163        let name = Path::new(plugin_path)
164            .file_stem()
165            .map(|s| s.to_string_lossy().to_string())
166            .unwrap_or_else(|| plugin_spec.to_string());
167        let host_runtime = Arc::new(HostRuntime::new()?);
168        let plugin_handle = Arc::new(PluginHandle::load(
169            plugin_path,
170            plugin_id,
171            host_runtime.clone(),
172            sample_rate,
173            buffer_size as u32,
174        )?);
175        let (input_layout_opt, output_layout_opt) = plugin_handle.audio_port_channels();
176        let input_port_channels_opt = input_layout_opt.as_ref().map(|(c, _)| c.clone());
177        let output_port_channels_opt = output_layout_opt.as_ref().map(|(c, _)| c.clone());
178        let discovered_inputs = input_layout_opt.as_ref().map(|(c, _)| c.len());
179        let discovered_outputs = output_layout_opt.as_ref().map(|(c, _)| c.len());
180        let (discovered_midi_inputs, discovered_midi_outputs) = plugin_handle.note_port_layout();
181        let resolved_inputs = discovered_inputs.unwrap_or(input_count);
182        let resolved_outputs = discovered_outputs.unwrap_or(output_count);
183        let main_audio_inputs = input_layout_opt
184            .as_ref()
185            .map(|(_, main)| *main)
186            .unwrap_or(input_count);
187        let main_audio_outputs = output_layout_opt
188            .as_ref()
189            .map(|(_, main)| *main)
190            .unwrap_or(output_count);
191        let audio_inputs = (0..resolved_inputs)
192            .map(|_| Arc::new(AudioIO::new(buffer_size)))
193            .collect();
194        let audio_outputs = (0..resolved_outputs)
195            .map(|_| Arc::new(AudioIO::new(buffer_size)))
196            .collect();
197        let param_infos = Arc::new(plugin_handle.parameter_infos());
198        let param_values = Arc::new(UnsafeMutex::new(
199            plugin_handle.parameter_values(&param_infos),
200        ));
201        Ok(Self {
202            path: plugin_spec.to_string(),
203            plugin_id: plugin_handle.plugin_id().to_string(),
204            name,
205            sample_rate,
206            audio_inputs,
207            audio_outputs,
208            input_port_channels: input_port_channels_opt
209                .unwrap_or_else(|| vec![1; resolved_inputs]),
210            output_port_channels: output_port_channels_opt
211                .unwrap_or_else(|| vec![1; resolved_outputs]),
212            midi_input_ports: discovered_midi_inputs.unwrap_or(0),
213            midi_output_ports: discovered_midi_outputs.unwrap_or(0),
214            main_audio_inputs,
215            main_audio_outputs,
216            host_runtime,
217            plugin_handle,
218            param_infos,
219            param_values,
220            pending_param_events: Arc::new(UnsafeMutex::new(Vec::new())),
221            pending_param_events_ui: Arc::new(UnsafeMutex::new(Vec::new())),
222            process_lock: Arc::new(UnsafeMutex::new(())),
223        })
224    }
225
226    pub fn setup_audio_ports(&self) {
227        for port in &self.audio_inputs {
228            port.setup();
229        }
230        for port in &self.audio_outputs {
231            port.setup();
232        }
233    }
234
235    pub fn process_with_audio_io(&self, frames: usize) {
236        let _ = self.process_with_midi(frames, &[], ClapTransportInfo::default());
237    }
238
239    pub fn process_with_midi(
240        &self,
241        frames: usize,
242        midi_in: &[MidiEvent],
243        transport: ClapTransportInfo,
244    ) -> Vec<ClapMidiOutputEvent> {
245        // CLAP processors are not guaranteed to be re-entrant. Serialize
246        // processing per instance to avoid concurrent mutation of plugin state.
247        let _process_guard = self.process_lock.lock();
248        let started = Instant::now();
249        for port in &self.audio_inputs {
250            if port.ready() {
251                port.process();
252            }
253        }
254        let (processed, processed_midi) = match self.process_native(frames, midi_in, transport) {
255            Ok(ok) => ok,
256            Err(err) => {
257                tracing::warn!(
258                    "CLAP process failed for '{}' ({}): {}",
259                    self.name,
260                    self.path,
261                    err
262                );
263                (false, Vec::new())
264            }
265        };
266        let elapsed = started.elapsed();
267        if elapsed > Duration::from_millis(20) {
268            tracing::warn!(
269                "Slow CLAP process '{}' ({}) took {:.3} ms for {} frames",
270                self.name,
271                self.path,
272                elapsed.as_secs_f64() * 1000.0,
273                frames
274            );
275        }
276        if !processed {
277            for out in &self.audio_outputs {
278                let out_buf = out.buffer.lock();
279                out_buf.fill(0.0);
280                *out.finished.lock() = true;
281            }
282        }
283        processed_midi
284    }
285
286    pub fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
287        self.param_infos.as_ref().clone()
288    }
289
290    pub fn parameter_values(&self) -> HashMap<u32, f64> {
291        self.param_values.lock().clone()
292    }
293
294    pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
295        self.set_parameter_at(param_id, value, 0)
296    }
297
298    pub fn set_parameter_at(&self, param_id: u32, value: f64, frame: u32) -> Result<(), String> {
299        let _thread_scope = HostThreadScope::enter_main();
300        let Some(info) = self.param_infos.iter().find(|p| p.id == param_id) else {
301            return Err(format!("Unknown CLAP parameter id: {param_id}"));
302        };
303        let clamped = value.clamp(info.min_value, info.max_value);
304        self.pending_param_events
305            .lock()
306            .push(PendingParamEvent::Value {
307                param_id,
308                value: clamped,
309                frame,
310            });
311        self.pending_param_events_ui
312            .lock()
313            .push(PendingParamEvent::Value {
314                param_id,
315                value: clamped,
316                frame,
317            });
318        self.param_values.lock().insert(param_id, clamped);
319        Ok(())
320    }
321
322    pub fn begin_parameter_edit(&self, param_id: u32) -> Result<(), String> {
323        self.begin_parameter_edit_at(param_id, 0)
324    }
325
326    pub fn begin_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
327        let _thread_scope = HostThreadScope::enter_main();
328        if !self.param_infos.iter().any(|p| p.id == param_id) {
329            return Err(format!("Unknown CLAP parameter id: {param_id}"));
330        }
331        self.pending_param_events
332            .lock()
333            .push(PendingParamEvent::GestureBegin { param_id, frame });
334        self.pending_param_events_ui
335            .lock()
336            .push(PendingParamEvent::GestureBegin { param_id, frame });
337        Ok(())
338    }
339
340    pub fn end_parameter_edit(&self, param_id: u32) -> Result<(), String> {
341        self.end_parameter_edit_at(param_id, 0)
342    }
343
344    pub fn end_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
345        let _thread_scope = HostThreadScope::enter_main();
346        if !self.param_infos.iter().any(|p| p.id == param_id) {
347            return Err(format!("Unknown CLAP parameter id: {param_id}"));
348        }
349        self.pending_param_events
350            .lock()
351            .push(PendingParamEvent::GestureEnd { param_id, frame });
352        self.pending_param_events_ui
353            .lock()
354            .push(PendingParamEvent::GestureEnd { param_id, frame });
355        Ok(())
356    }
357
358    pub fn snapshot_state(&self) -> Result<ClapPluginState, String> {
359        let _thread_scope = HostThreadScope::enter_main();
360        self.plugin_handle.snapshot_state()
361    }
362
363    pub fn restore_state(&self, state: &ClapPluginState) -> Result<(), String> {
364        let _thread_scope = HostThreadScope::enter_main();
365        self.plugin_handle.restore_state(state)
366    }
367
368    pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
369        &self.audio_inputs
370    }
371
372    pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
373        &self.audio_outputs
374    }
375
376    pub fn main_audio_input_count(&self) -> usize {
377        self.main_audio_inputs
378    }
379
380    pub fn main_audio_output_count(&self) -> usize {
381        self.main_audio_outputs
382    }
383
384    pub fn midi_input_count(&self) -> usize {
385        self.midi_input_ports
386    }
387
388    pub fn midi_output_count(&self) -> usize {
389        self.midi_output_ports
390    }
391
392    pub fn path(&self) -> &str {
393        &self.path
394    }
395
396    pub fn plugin_id(&self) -> &str {
397        &self.plugin_id
398    }
399
400    pub fn name(&self) -> &str {
401        &self.name
402    }
403
404    pub fn ui_begin_session(&self) {
405        self.host_runtime.begin_ui_session();
406    }
407
408    pub fn ui_end_session(&self) {
409        self.host_runtime.end_ui_session();
410    }
411
412    pub fn ui_should_close(&self) -> bool {
413        self.host_runtime.ui_should_close()
414    }
415
416    pub fn ui_take_due_timers(&self) -> Vec<u32> {
417        self.host_runtime.ui_take_due_timers()
418    }
419
420    pub fn ui_take_param_updates(&self) -> Vec<ClapParamUpdate> {
421        let pending_ui_events = std::mem::take(self.pending_param_events_ui.lock());
422        if pending_ui_events.is_empty() && !self.host_runtime.ui_take_param_flush_requested() {
423            return Vec::new();
424        }
425        let _thread_scope = HostThreadScope::enter_main();
426        let updates = self.plugin_handle.flush_params(&pending_ui_events);
427        if updates.is_empty() {
428            return Vec::new();
429        }
430        let values = &mut *self.param_values.lock();
431        let mut out = Vec::with_capacity(updates.len());
432        for update in updates {
433            values.insert(update.param_id, update.value);
434            out.push(ClapParamUpdate {
435                param_id: update.param_id,
436                value: update.value,
437            });
438        }
439        out
440    }
441
442    pub fn ui_take_state_update(&self) -> Option<ClapPluginState> {
443        if !self.host_runtime.ui_take_state_dirty_requested() {
444            return None;
445        }
446        let _thread_scope = HostThreadScope::enter_main();
447        self.plugin_handle.snapshot_state().ok()
448    }
449
450    pub fn gui_info(&self) -> Result<ClapGuiInfo, String> {
451        let _thread_scope = HostThreadScope::enter_main();
452        self.plugin_handle.gui_info()
453    }
454
455    pub fn gui_create(&self, api: &str, is_floating: bool) -> Result<(), String> {
456        let _thread_scope = HostThreadScope::enter_main();
457        self.plugin_handle.gui_create(api, is_floating)
458    }
459
460    pub fn gui_get_size(&self) -> Result<(u32, u32), String> {
461        let _thread_scope = HostThreadScope::enter_main();
462        self.plugin_handle.gui_get_size()
463    }
464
465    pub fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
466        let _thread_scope = HostThreadScope::enter_main();
467        self.plugin_handle.gui_set_parent_x11(window)
468    }
469
470    pub fn gui_show(&self) -> Result<(), String> {
471        let _thread_scope = HostThreadScope::enter_main();
472        self.plugin_handle.gui_show()
473    }
474
475    pub fn gui_hide(&self) {
476        let _thread_scope = HostThreadScope::enter_main();
477        self.plugin_handle.gui_hide();
478    }
479
480    pub fn gui_destroy(&self) {
481        let _thread_scope = HostThreadScope::enter_main();
482        self.plugin_handle.gui_destroy();
483    }
484
485    pub fn gui_on_main_thread(&self) {
486        let _thread_scope = HostThreadScope::enter_main();
487        self.plugin_handle.on_main_thread();
488    }
489
490    pub fn gui_on_timer(&self, timer_id: u32) {
491        let _thread_scope = HostThreadScope::enter_main();
492        self.plugin_handle.gui_on_timer(timer_id);
493    }
494
495    pub fn note_names(&self) -> std::collections::HashMap<u8, String> {
496        self.plugin_handle.get_note_names()
497    }
498
499    pub fn run_host_callbacks_main_thread(&self) {
500        let host_flags = self.host_runtime.take_callback_flags();
501        if host_flags.restart {
502            let _thread_scope = HostThreadScope::enter_main();
503            self.plugin_handle.reset();
504        }
505        if host_flags.callback {
506            let _thread_scope = HostThreadScope::enter_main();
507            self.plugin_handle.on_main_thread();
508        }
509        if host_flags.process {
510            // Host already continuously schedules process blocks.
511        }
512    }
513
514    fn process_native(
515        &self,
516        frames: usize,
517        midi_in: &[MidiEvent],
518        transport: ClapTransportInfo,
519    ) -> Result<(bool, Vec<ClapMidiOutputEvent>), String> {
520        if frames == 0 {
521            return Ok((true, Vec::new()));
522        }
523
524        let mut in_channel_ptrs: Vec<Vec<*mut f32>> = Vec::with_capacity(self.audio_inputs.len());
525        let mut out_channel_ptrs: Vec<Vec<*mut f32>> = Vec::with_capacity(self.audio_outputs.len());
526        let mut in_channel_scratch: Vec<Vec<f32>> = Vec::new();
527        let mut out_channel_scratch: Vec<Vec<f32>> = Vec::new();
528        let mut out_channel_scratch_ranges: Vec<(usize, usize)> =
529            Vec::with_capacity(self.audio_outputs.len());
530        let mut in_buffers = Vec::with_capacity(self.audio_inputs.len());
531        let mut out_buffers = Vec::with_capacity(self.audio_outputs.len());
532
533        for (port_idx, input) in self.audio_inputs.iter().enumerate() {
534            let buf = input.buffer.lock();
535            let channel_count = self
536                .input_port_channels
537                .get(port_idx)
538                .copied()
539                .unwrap_or(1)
540                .max(1);
541            let mut ptrs = Vec::with_capacity(channel_count);
542            ptrs.push(buf.as_ptr() as *mut f32);
543            for _ in 1..channel_count {
544                in_channel_scratch.push(buf.to_vec());
545                let idx = in_channel_scratch.len().saturating_sub(1);
546                ptrs.push(in_channel_scratch[idx].as_mut_ptr());
547            }
548            in_channel_ptrs.push(ptrs);
549            in_buffers.push(buf);
550        }
551        for (port_idx, output) in self.audio_outputs.iter().enumerate() {
552            let buf = output.buffer.lock();
553            let channel_count = self
554                .output_port_channels
555                .get(port_idx)
556                .copied()
557                .unwrap_or(1)
558                .max(1);
559            let mut ptrs = Vec::with_capacity(channel_count);
560            ptrs.push(buf.as_mut_ptr());
561            let scratch_start = out_channel_scratch.len();
562            for _ in 1..channel_count {
563                out_channel_scratch.push(vec![0.0; frames]);
564                let idx = out_channel_scratch.len().saturating_sub(1);
565                ptrs.push(out_channel_scratch[idx].as_mut_ptr());
566            }
567            let scratch_end = out_channel_scratch.len();
568            out_channel_scratch_ranges.push((scratch_start, scratch_end));
569            out_channel_ptrs.push(ptrs);
570            out_buffers.push(buf);
571        }
572
573        let mut in_audio = Vec::with_capacity(self.audio_inputs.len());
574        let mut out_audio = Vec::with_capacity(self.audio_outputs.len());
575
576        for ptrs in &mut in_channel_ptrs {
577            in_audio.push(ClapAudioBuffer {
578                data32: ptrs.as_mut_ptr(),
579                data64: std::ptr::null_mut(),
580                channel_count: ptrs.len() as u32,
581                latency: 0,
582                constant_mask: 0,
583            });
584        }
585        for ptrs in &mut out_channel_ptrs {
586            out_audio.push(ClapAudioBuffer {
587                data32: ptrs.as_mut_ptr(),
588                data64: std::ptr::null_mut(),
589                channel_count: ptrs.len() as u32,
590                latency: 0,
591                constant_mask: 0,
592            });
593        }
594
595        let pending_params = std::mem::take(self.pending_param_events.lock());
596        let (in_events, in_ctx) = input_events_from(
597            midi_in,
598            &pending_params,
599            self.sample_rate,
600            transport,
601            self.midi_input_ports > 0,
602        );
603        let out_cap = midi_in
604            .len()
605            .saturating_add(self.midi_output_ports.saturating_mul(64));
606        let (mut out_events, mut out_ctx) = output_events_ctx(out_cap);
607
608        let mut process = ClapProcess {
609            steady_time: -1,
610            frames_count: frames as u32,
611            transport: std::ptr::null(),
612            audio_inputs: in_audio.as_mut_ptr(),
613            audio_outputs: out_audio.as_mut_ptr(),
614            audio_inputs_count: in_audio.len() as u32,
615            audio_outputs_count: out_audio.len() as u32,
616            in_events: &in_events,
617            out_events: &mut out_events,
618        };
619
620        let _thread_scope = HostThreadScope::enter_audio();
621        let result = self.plugin_handle.process(&mut process);
622        drop(in_ctx);
623        for output in &self.audio_outputs {
624            *output.finished.lock() = true;
625        }
626        let processed = result?;
627        if processed {
628            // Downmix multi-channel CLAP output ports into track output buffers.
629            for (port_idx, out_buf) in out_buffers.iter_mut().enumerate() {
630                let Some((scratch_start, scratch_end)) = out_channel_scratch_ranges.get(port_idx)
631                else {
632                    continue;
633                };
634                let scratch_count = scratch_end.saturating_sub(*scratch_start);
635                if scratch_count == 0 {
636                    continue;
637                }
638                let ch_count = scratch_count + 1;
639                for scratch in &out_channel_scratch[*scratch_start..*scratch_end] {
640                    for (dst, src) in out_buf.iter_mut().zip(scratch.iter().take(frames)) {
641                        *dst += *src;
642                    }
643                }
644                let inv = 1.0_f32 / ch_count as f32;
645                for sample in out_buf.iter_mut().take(frames) {
646                    *sample *= inv;
647                }
648            }
649            for update in &out_ctx.param_values {
650                self.param_values
651                    .lock()
652                    .insert(update.param_id, update.value);
653            }
654            Ok((true, std::mem::take(&mut out_ctx.midi_events)))
655        } else {
656            Ok((false, Vec::new()))
657        }
658    }
659}
660
661#[repr(C)]
662#[derive(Clone, Copy)]
663struct ClapVersion {
664    major: u32,
665    minor: u32,
666    revision: u32,
667}
668
669const CLAP_VERSION: ClapVersion = ClapVersion {
670    major: 1,
671    minor: 2,
672    revision: 0,
673};
674
675#[repr(C)]
676struct ClapHost {
677    clap_version: ClapVersion,
678    host_data: *mut c_void,
679    name: *const c_char,
680    vendor: *const c_char,
681    url: *const c_char,
682    version: *const c_char,
683    get_extension: Option<unsafe extern "C" fn(*const ClapHost, *const c_char) -> *const c_void>,
684    request_restart: Option<unsafe extern "C" fn(*const ClapHost)>,
685    request_process: Option<unsafe extern "C" fn(*const ClapHost)>,
686    request_callback: Option<unsafe extern "C" fn(*const ClapHost)>,
687}
688
689#[repr(C)]
690struct ClapPluginEntry {
691    clap_version: ClapVersion,
692    init: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
693    deinit: Option<unsafe extern "C" fn()>,
694    get_factory: Option<unsafe extern "C" fn(*const c_char) -> *const c_void>,
695}
696
697#[repr(C)]
698struct ClapPluginFactory {
699    get_plugin_count: Option<unsafe extern "C" fn(*const ClapPluginFactory) -> u32>,
700    get_plugin_descriptor:
701        Option<unsafe extern "C" fn(*const ClapPluginFactory, u32) -> *const ClapPluginDescriptor>,
702    create_plugin: Option<
703        unsafe extern "C" fn(
704            *const ClapPluginFactory,
705            *const ClapHost,
706            *const c_char,
707        ) -> *const ClapPlugin,
708    >,
709}
710
711#[repr(C)]
712struct ClapPluginDescriptor {
713    clap_version: ClapVersion,
714    id: *const c_char,
715    name: *const c_char,
716    vendor: *const c_char,
717    url: *const c_char,
718    manual_url: *const c_char,
719    support_url: *const c_char,
720    version: *const c_char,
721    description: *const c_char,
722    features: *const *const c_char,
723}
724
725#[repr(C)]
726struct ClapPlugin {
727    desc: *const ClapPluginDescriptor,
728    plugin_data: *mut c_void,
729    init: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
730    destroy: Option<unsafe extern "C" fn(*const ClapPlugin)>,
731    activate: Option<unsafe extern "C" fn(*const ClapPlugin, f64, u32, u32) -> bool>,
732    deactivate: Option<unsafe extern "C" fn(*const ClapPlugin)>,
733    start_processing: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
734    stop_processing: Option<unsafe extern "C" fn(*const ClapPlugin)>,
735    reset: Option<unsafe extern "C" fn(*const ClapPlugin)>,
736    process: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapProcess) -> i32>,
737    get_extension: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char) -> *const c_void>,
738    on_main_thread: Option<unsafe extern "C" fn(*const ClapPlugin)>,
739}
740
741#[repr(C)]
742struct ClapInputEvents {
743    ctx: *const c_void,
744    size: Option<unsafe extern "C" fn(*const ClapInputEvents) -> u32>,
745    get: Option<unsafe extern "C" fn(*const ClapInputEvents, u32) -> *const ClapEventHeader>,
746}
747
748#[repr(C)]
749struct ClapOutputEvents {
750    ctx: *mut c_void,
751    try_push: Option<unsafe extern "C" fn(*const ClapOutputEvents, *const ClapEventHeader) -> bool>,
752}
753
754#[repr(C)]
755struct ClapEventHeader {
756    size: u32,
757    time: u32,
758    space_id: u16,
759    type_: u16,
760    flags: u32,
761}
762
763const CLAP_CORE_EVENT_SPACE_ID: u16 = 0;
764const CLAP_EVENT_NOTE_ON: u16 = 0;
765const CLAP_EVENT_NOTE_OFF: u16 = 1;
766const CLAP_EVENT_MIDI: u16 = 10;
767const CLAP_EVENT_PARAM_VALUE: u16 = 5;
768const CLAP_EVENT_PARAM_GESTURE_BEGIN: u16 = 6;
769const CLAP_EVENT_PARAM_GESTURE_END: u16 = 7;
770const CLAP_EVENT_TRANSPORT: u16 = 9;
771const CLAP_TRANSPORT_HAS_TEMPO: u32 = 1 << 0;
772const CLAP_TRANSPORT_HAS_BEATS_TIMELINE: u32 = 1 << 1;
773const CLAP_TRANSPORT_HAS_SECONDS_TIMELINE: u32 = 1 << 2;
774const CLAP_TRANSPORT_HAS_TIME_SIGNATURE: u32 = 1 << 3;
775const CLAP_TRANSPORT_IS_PLAYING: u32 = 1 << 4;
776const CLAP_TRANSPORT_IS_LOOP_ACTIVE: u32 = 1 << 6;
777const CLAP_BEATTIME_FACTOR: i64 = 1_i64 << 31;
778const CLAP_SECTIME_FACTOR: i64 = 1_i64 << 31;
779
780#[repr(C)]
781struct ClapEventMidi {
782    header: ClapEventHeader,
783    port_index: u16,
784    data: [u8; 3],
785}
786
787#[repr(C)]
788struct ClapEventNote {
789    header: ClapEventHeader,
790    note_id: i32,
791    port_index: i16,
792    channel: i16,
793    key: i16,
794    velocity: f64,
795}
796
797#[repr(C)]
798struct ClapEventParamValue {
799    header: ClapEventHeader,
800    param_id: u32,
801    cookie: *mut c_void,
802    note_id: i32,
803    port_index: i16,
804    channel: i16,
805    key: i16,
806    value: f64,
807}
808
809#[repr(C)]
810struct ClapEventParamGesture {
811    header: ClapEventHeader,
812    param_id: u32,
813}
814
815#[repr(C)]
816struct ClapEventTransport {
817    header: ClapEventHeader,
818    flags: u32,
819    song_pos_beats: i64,
820    song_pos_seconds: i64,
821    tempo: f64,
822    tempo_inc: f64,
823    loop_start_beats: i64,
824    loop_end_beats: i64,
825    loop_start_seconds: i64,
826    loop_end_seconds: i64,
827    bar_start: i64,
828    bar_number: i32,
829    tsig_num: u16,
830    tsig_denom: u16,
831}
832
833#[repr(C)]
834struct ClapParamInfoRaw {
835    id: u32,
836    flags: u32,
837    cookie: *mut c_void,
838    name: [c_char; 256],
839    module: [c_char; 1024],
840    min_value: f64,
841    max_value: f64,
842    default_value: f64,
843}
844
845#[repr(C)]
846struct ClapPluginParams {
847    count: Option<unsafe extern "C" fn(*const ClapPlugin) -> u32>,
848    get_info: Option<unsafe extern "C" fn(*const ClapPlugin, u32, *mut ClapParamInfoRaw) -> bool>,
849    get_value: Option<unsafe extern "C" fn(*const ClapPlugin, u32, *mut f64) -> bool>,
850    value_to_text:
851        Option<unsafe extern "C" fn(*const ClapPlugin, u32, f64, *mut c_char, u32) -> bool>,
852    text_to_value:
853        Option<unsafe extern "C" fn(*const ClapPlugin, u32, *const c_char, *mut f64) -> bool>,
854    flush: Option<
855        unsafe extern "C" fn(*const ClapPlugin, *const ClapInputEvents, *const ClapOutputEvents),
856    >,
857}
858
859#[repr(C)]
860struct ClapPluginStateExt {
861    save: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapOStream) -> bool>,
862    load: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapIStream) -> bool>,
863}
864
865#[repr(C)]
866struct ClapAudioPortInfoRaw {
867    id: u32,
868    name: [c_char; 256],
869    flags: u32,
870    channel_count: u32,
871    port_type: *const c_char,
872    in_place_pair: u32,
873}
874
875#[repr(C)]
876struct ClapPluginAudioPorts {
877    count: Option<unsafe extern "C" fn(*const ClapPlugin, bool) -> u32>,
878    get: Option<
879        unsafe extern "C" fn(*const ClapPlugin, u32, bool, *mut ClapAudioPortInfoRaw) -> bool,
880    >,
881}
882
883#[repr(C)]
884struct ClapNotePortInfoRaw {
885    id: u16,
886    supported_dialects: u32,
887    preferred_dialect: u32,
888    name: [c_char; 256],
889}
890
891#[repr(C)]
892struct ClapPluginNotePorts {
893    count: Option<unsafe extern "C" fn(*const ClapPlugin, bool) -> u32>,
894    get: Option<
895        unsafe extern "C" fn(*const ClapPlugin, u32, bool, *mut ClapNotePortInfoRaw) -> bool,
896    >,
897}
898
899#[repr(C)]
900struct ClapPluginGui {
901    is_api_supported: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char, bool) -> bool>,
902    get_preferred_api:
903        Option<unsafe extern "C" fn(*const ClapPlugin, *mut *const c_char, *mut bool) -> bool>,
904    create: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char, bool) -> bool>,
905    destroy: Option<unsafe extern "C" fn(*const ClapPlugin)>,
906    set_scale: Option<unsafe extern "C" fn(*const ClapPlugin, f64) -> bool>,
907    get_size: Option<unsafe extern "C" fn(*const ClapPlugin, *mut u32, *mut u32) -> bool>,
908    can_resize: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
909    get_resize_hints: Option<unsafe extern "C" fn(*const ClapPlugin, *mut c_void) -> bool>,
910    adjust_size: Option<unsafe extern "C" fn(*const ClapPlugin, *mut u32, *mut u32) -> bool>,
911    set_size: Option<unsafe extern "C" fn(*const ClapPlugin, u32, u32) -> bool>,
912    set_parent: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapWindow) -> bool>,
913    set_transient: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapWindow) -> bool>,
914    suggest_title: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char)>,
915    show: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
916    hide: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
917}
918
919#[repr(C)]
920union ClapWindowHandle {
921    x11: usize,
922    native: *mut c_void,
923    cocoa: *mut c_void,
924}
925
926#[repr(C)]
927struct ClapWindow {
928    api: *const c_char,
929    handle: ClapWindowHandle,
930}
931
932#[repr(C)]
933struct ClapPluginTimerSupport {
934    on_timer: Option<unsafe extern "C" fn(*const ClapPlugin, u32)>,
935}
936
937#[repr(C)]
938struct ClapHostThreadCheck {
939    is_main_thread: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
940    is_audio_thread: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
941}
942
943#[repr(C)]
944struct ClapHostLatency {
945    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
946}
947
948#[repr(C)]
949struct ClapHostTail {
950    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
951}
952
953#[repr(C)]
954struct ClapHostTimerSupport {
955    register_timer: Option<unsafe extern "C" fn(*const ClapHost, u32, *mut u32) -> bool>,
956    unregister_timer: Option<unsafe extern "C" fn(*const ClapHost, u32) -> bool>,
957}
958
959#[repr(C)]
960struct ClapHostGui {
961    resize_hints_changed: Option<unsafe extern "C" fn(*const ClapHost)>,
962    request_resize: Option<unsafe extern "C" fn(*const ClapHost, u32, u32) -> bool>,
963    request_show: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
964    request_hide: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
965    closed: Option<unsafe extern "C" fn(*const ClapHost, bool)>,
966}
967
968#[repr(C)]
969struct ClapHostParams {
970    rescan: Option<unsafe extern "C" fn(*const ClapHost, u32)>,
971    clear: Option<unsafe extern "C" fn(*const ClapHost, u32, u32)>,
972    request_flush: Option<unsafe extern "C" fn(*const ClapHost)>,
973}
974
975#[repr(C)]
976struct ClapHostState {
977    mark_dirty: Option<unsafe extern "C" fn(*const ClapHost)>,
978}
979
980#[repr(C)]
981struct ClapNoteName {
982    name: [c_char; 256],
983    port: i16,
984    key: i16,
985    channel: i16,
986}
987
988#[repr(C)]
989struct ClapPluginNoteName {
990    count: Option<unsafe extern "C" fn(*const ClapPlugin) -> u32>,
991    get: Option<unsafe extern "C" fn(*const ClapPlugin, u32, *mut ClapNoteName) -> bool>,
992}
993
994#[repr(C)]
995struct ClapHostNoteName {
996    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
997}
998
999#[repr(C)]
1000struct ClapOStream {
1001    ctx: *mut c_void,
1002    write: Option<unsafe extern "C" fn(*const ClapOStream, *const c_void, u64) -> i64>,
1003}
1004
1005#[repr(C)]
1006struct ClapIStream {
1007    ctx: *mut c_void,
1008    read: Option<unsafe extern "C" fn(*const ClapIStream, *mut c_void, u64) -> i64>,
1009}
1010
1011#[repr(C)]
1012struct ClapAudioBuffer {
1013    data32: *mut *mut f32,
1014    data64: *mut *mut f64,
1015    channel_count: u32,
1016    latency: u32,
1017    constant_mask: u64,
1018}
1019
1020#[repr(C)]
1021struct ClapProcess {
1022    steady_time: i64,
1023    frames_count: u32,
1024    transport: *const c_void,
1025    audio_inputs: *mut ClapAudioBuffer,
1026    audio_outputs: *mut ClapAudioBuffer,
1027    audio_inputs_count: u32,
1028    audio_outputs_count: u32,
1029    in_events: *const ClapInputEvents,
1030    out_events: *mut ClapOutputEvents,
1031}
1032
1033enum ClapInputEvent {
1034    Note(ClapEventNote),
1035    Midi(ClapEventMidi),
1036    ParamValue(ClapEventParamValue),
1037    ParamGesture(ClapEventParamGesture),
1038    Transport(ClapEventTransport),
1039}
1040
1041impl ClapInputEvent {
1042    fn header_ptr(&self) -> *const ClapEventHeader {
1043        match self {
1044            Self::Note(e) => &e.header as *const ClapEventHeader,
1045            Self::Midi(e) => &e.header as *const ClapEventHeader,
1046            Self::ParamValue(e) => &e.header as *const ClapEventHeader,
1047            Self::ParamGesture(e) => &e.header as *const ClapEventHeader,
1048            Self::Transport(e) => &e.header as *const ClapEventHeader,
1049        }
1050    }
1051}
1052
1053struct ClapInputEventsCtx {
1054    events: Vec<ClapInputEvent>,
1055}
1056
1057struct ClapOutputEventsCtx {
1058    midi_events: Vec<ClapMidiOutputEvent>,
1059    param_values: Vec<PendingParamValue>,
1060}
1061
1062struct ClapIStreamCtx<'a> {
1063    bytes: &'a [u8],
1064    offset: usize,
1065}
1066
1067#[derive(Default, Clone, Copy)]
1068struct HostCallbackFlags {
1069    restart: bool,
1070    process: bool,
1071    callback: bool,
1072}
1073
1074#[derive(Clone, Copy)]
1075struct HostTimer {
1076    id: u32,
1077    period: Duration,
1078    next_tick: Instant,
1079}
1080
1081struct HostRuntimeState {
1082    callback_flags: UnsafeMutex<HostCallbackFlags>,
1083    timers: UnsafeMutex<Vec<HostTimer>>,
1084    ui_should_close: AtomicU32,
1085    ui_active: AtomicU32,
1086    param_flush_requested: AtomicU32,
1087    state_dirty_requested: AtomicU32,
1088    note_names_dirty: AtomicU32,
1089}
1090
1091thread_local! {
1092    static CLAP_HOST_MAIN_THREAD: Cell<bool> = const { Cell::new(true) };
1093    static CLAP_HOST_AUDIO_THREAD: Cell<bool> = const { Cell::new(false) };
1094}
1095
1096struct HostThreadScope {
1097    main: bool,
1098    prev: bool,
1099}
1100
1101impl HostThreadScope {
1102    fn enter_main() -> Self {
1103        let prev = CLAP_HOST_MAIN_THREAD.with(|flag| {
1104            let prev = flag.get();
1105            flag.set(true);
1106            prev
1107        });
1108        Self { main: true, prev }
1109    }
1110
1111    fn enter_audio() -> Self {
1112        let prev = CLAP_HOST_AUDIO_THREAD.with(|flag| {
1113            let prev = flag.get();
1114            flag.set(true);
1115            prev
1116        });
1117        Self { main: false, prev }
1118    }
1119}
1120
1121impl Drop for HostThreadScope {
1122    fn drop(&mut self) {
1123        if self.main {
1124            CLAP_HOST_MAIN_THREAD.with(|flag| flag.set(self.prev));
1125        } else {
1126            CLAP_HOST_AUDIO_THREAD.with(|flag| flag.set(self.prev));
1127        }
1128    }
1129}
1130
1131struct HostRuntime {
1132    state: Box<HostRuntimeState>,
1133    host: ClapHost,
1134}
1135
1136impl HostRuntime {
1137    fn new() -> Result<Self, String> {
1138        let mut state = Box::new(HostRuntimeState {
1139            callback_flags: UnsafeMutex::new(HostCallbackFlags::default()),
1140            timers: UnsafeMutex::new(Vec::new()),
1141            ui_should_close: AtomicU32::new(0),
1142            ui_active: AtomicU32::new(0),
1143            param_flush_requested: AtomicU32::new(0),
1144            state_dirty_requested: AtomicU32::new(0),
1145            note_names_dirty: AtomicU32::new(0),
1146        });
1147        let host = ClapHost {
1148            clap_version: CLAP_VERSION,
1149            host_data: (&mut *state as *mut HostRuntimeState).cast::<c_void>(),
1150            name: c"Maolan".as_ptr(),
1151            vendor: c"Maolan".as_ptr(),
1152            url: c"https://example.invalid".as_ptr(),
1153            version: c"0.0.1".as_ptr(),
1154            get_extension: Some(host_get_extension),
1155            request_restart: Some(host_request_restart),
1156            request_process: Some(host_request_process),
1157            request_callback: Some(host_request_callback),
1158        };
1159        Ok(Self { state, host })
1160    }
1161
1162    fn take_callback_flags(&self) -> HostCallbackFlags {
1163        let flags = self.state.callback_flags.lock();
1164        let out = *flags;
1165        *flags = HostCallbackFlags::default();
1166        out
1167    }
1168
1169    fn begin_ui_session(&self) {
1170        self.state.ui_should_close.store(0, Ordering::Release);
1171        self.state.ui_active.store(1, Ordering::Release);
1172        self.state.param_flush_requested.store(0, Ordering::Release);
1173        self.state.state_dirty_requested.store(0, Ordering::Release);
1174        self.state.timers.lock().clear();
1175    }
1176
1177    fn end_ui_session(&self) {
1178        self.state.ui_active.store(0, Ordering::Release);
1179        self.state.ui_should_close.store(0, Ordering::Release);
1180        self.state.param_flush_requested.store(0, Ordering::Release);
1181        self.state.state_dirty_requested.store(0, Ordering::Release);
1182        self.state.timers.lock().clear();
1183    }
1184
1185    fn ui_should_close(&self) -> bool {
1186        self.state.ui_should_close.load(Ordering::Acquire) != 0
1187    }
1188
1189    fn ui_take_due_timers(&self) -> Vec<u32> {
1190        let now = Instant::now();
1191        let timers = &mut *self.state.timers.lock();
1192        let mut due = Vec::new();
1193        for timer in timers.iter_mut() {
1194            if now >= timer.next_tick {
1195                due.push(timer.id);
1196                timer.next_tick = now + timer.period;
1197            }
1198        }
1199        due
1200    }
1201
1202    fn ui_take_param_flush_requested(&self) -> bool {
1203        self.state.param_flush_requested.swap(0, Ordering::AcqRel) != 0
1204    }
1205
1206    fn ui_take_state_dirty_requested(&self) -> bool {
1207        self.state.state_dirty_requested.swap(0, Ordering::AcqRel) != 0
1208    }
1209}
1210
1211// SAFETY: HostRuntime owns stable CString storage and a CLAP host struct that
1212// contains raw pointers into that owned storage. The data is immutable after
1213// construction and safe to share/move across threads.
1214unsafe impl Send for HostRuntime {}
1215// SAFETY: See Send rationale above; HostRuntime has no interior mutation.
1216unsafe impl Sync for HostRuntime {}
1217
1218struct PluginHandle {
1219    _library: Library,
1220    entry: *const ClapPluginEntry,
1221    plugin: *const ClapPlugin,
1222    plugin_id: String,
1223}
1224
1225// SAFETY: PluginHandle only stores pointers/libraries managed by the CLAP ABI.
1226// Access to plugin processing is synchronized by the engine track scheduling.
1227unsafe impl Send for PluginHandle {}
1228// SAFETY: Shared references do not mutate PluginHandle fields directly.
1229unsafe impl Sync for PluginHandle {}
1230
1231impl PluginHandle {
1232    fn load(
1233        plugin_path: &str,
1234        plugin_id: Option<&str>,
1235        host_runtime: Arc<HostRuntime>,
1236        sample_rate: f64,
1237        frames: u32,
1238    ) -> Result<Self, String> {
1239        let factory_id = c"clap.plugin-factory";
1240
1241        // SAFETY: We keep `library` alive for at least as long as plugin and entry pointers.
1242        let library = unsafe { Library::new(plugin_path) }.map_err(|e| e.to_string())?;
1243        // SAFETY: Symbol name and type follow CLAP ABI (`clap_entry` global variable).
1244        let entry_ptr = unsafe {
1245            let sym = library
1246                .get::<*const ClapPluginEntry>(b"clap_entry\0")
1247                .map_err(|e| e.to_string())?;
1248            *sym
1249        };
1250        if entry_ptr.is_null() {
1251            return Err("CLAP entry symbol is null".to_string());
1252        }
1253        // SAFETY: entry pointer comes from validated CLAP symbol.
1254        let entry = unsafe { &*entry_ptr };
1255        let init = entry
1256            .init
1257            .ok_or_else(|| "CLAP entry missing init()".to_string())?;
1258        let host_ptr = &host_runtime.host as *const ClapHost;
1259        // SAFETY: Valid host pointer for plugin bundle.
1260        if unsafe { !init(host_ptr) } {
1261            return Err(format!("CLAP entry init failed for {plugin_path}"));
1262        }
1263        let get_factory = entry
1264            .get_factory
1265            .ok_or_else(|| "CLAP entry missing get_factory()".to_string())?;
1266        // SAFETY: Factory id is a static NUL-terminated C string.
1267        let factory = unsafe { get_factory(factory_id.as_ptr()) } as *const ClapPluginFactory;
1268        if factory.is_null() {
1269            return Err("CLAP plugin factory not found".to_string());
1270        }
1271        // SAFETY: factory pointer was validated above.
1272        let factory_ref = unsafe { &*factory };
1273        let get_count = factory_ref
1274            .get_plugin_count
1275            .ok_or_else(|| "CLAP factory missing get_plugin_count()".to_string())?;
1276        let get_desc = factory_ref
1277            .get_plugin_descriptor
1278            .ok_or_else(|| "CLAP factory missing get_plugin_descriptor()".to_string())?;
1279        let create = factory_ref
1280            .create_plugin
1281            .ok_or_else(|| "CLAP factory missing create_plugin()".to_string())?;
1282
1283        // SAFETY: factory function pointers are valid CLAP ABI function pointers.
1284        let count = unsafe { get_count(factory) };
1285        if count == 0 {
1286            return Err("CLAP factory returned zero plugins".to_string());
1287        }
1288        let mut selected_id = None::<CString>;
1289        for i in 0..count {
1290            // SAFETY: i < count.
1291            let desc = unsafe { get_desc(factory, i) };
1292            if desc.is_null() {
1293                continue;
1294            }
1295            // SAFETY: descriptor pointer comes from factory.
1296            let desc = unsafe { &*desc };
1297            if desc.id.is_null() {
1298                continue;
1299            }
1300            // SAFETY: descriptor id is NUL-terminated per CLAP ABI.
1301            let id = unsafe { CStr::from_ptr(desc.id) };
1302            let id_str = id.to_string_lossy();
1303            if plugin_id.is_none() || plugin_id == Some(id_str.as_ref()) {
1304                selected_id = Some(
1305                    CString::new(id_str.as_ref()).map_err(|e| format!("Invalid plugin id: {e}"))?,
1306                );
1307                break;
1308            }
1309        }
1310        let selected_id = selected_id.ok_or_else(|| {
1311            if let Some(id) = plugin_id {
1312                format!("CLAP descriptor id not found in bundle: {id}")
1313            } else {
1314                "CLAP descriptor not found".to_string()
1315            }
1316        })?;
1317        // SAFETY: valid host pointer and plugin id.
1318        let plugin = unsafe { create(factory, &host_runtime.host, selected_id.as_ptr()) };
1319        if plugin.is_null() {
1320            return Err("CLAP factory create_plugin failed".to_string());
1321        }
1322        // SAFETY: plugin pointer validated above.
1323        let plugin_ref = unsafe { &*plugin };
1324        let plugin_init = plugin_ref
1325            .init
1326            .ok_or_else(|| "CLAP plugin missing init()".to_string())?;
1327        // SAFETY: plugin pointer and function pointer follow CLAP ABI.
1328        if unsafe { !plugin_init(plugin) } {
1329            return Err("CLAP plugin init() failed".to_string());
1330        }
1331        if let Some(activate) = plugin_ref.activate {
1332            // SAFETY: plugin pointer and arguments are valid for current engine buffer config.
1333            if unsafe { !activate(plugin, sample_rate, frames.max(1), frames.max(1)) } {
1334                return Err("CLAP plugin activate() failed".to_string());
1335            }
1336        }
1337        if let Some(start_processing) = plugin_ref.start_processing {
1338            // SAFETY: plugin activated above.
1339            if unsafe { !start_processing(plugin) } {
1340                return Err("CLAP plugin start_processing() failed".to_string());
1341            }
1342        }
1343        let plugin_id_str = selected_id.to_string_lossy().into_owned();
1344        Ok(Self {
1345            _library: library,
1346            entry: entry_ptr,
1347            plugin,
1348            plugin_id: plugin_id_str,
1349        })
1350    }
1351
1352    fn plugin_id(&self) -> &str {
1353        &self.plugin_id
1354    }
1355
1356    fn process(&self, process: &mut ClapProcess) -> Result<bool, String> {
1357        // SAFETY: plugin pointer is valid for lifetime of self.
1358        let plugin = unsafe { &*self.plugin };
1359        let Some(process_fn) = plugin.process else {
1360            return Ok(false);
1361        };
1362        // SAFETY: process struct references live buffers for the duration of call.
1363        let _status = unsafe { process_fn(self.plugin, process as *const _) };
1364        Ok(true)
1365    }
1366
1367    fn reset(&self) {
1368        // SAFETY: plugin pointer valid during self lifetime.
1369        let plugin = unsafe { &*self.plugin };
1370        if let Some(reset) = plugin.reset {
1371            // SAFETY: function pointer follows CLAP ABI.
1372            unsafe { reset(self.plugin) };
1373        }
1374    }
1375
1376    fn on_main_thread(&self) {
1377        // SAFETY: plugin pointer valid during self lifetime.
1378        let plugin = unsafe { &*self.plugin };
1379        if let Some(on_main_thread) = plugin.on_main_thread {
1380            // SAFETY: function pointer follows CLAP ABI.
1381            unsafe { on_main_thread(self.plugin) };
1382        }
1383    }
1384
1385    fn params_ext(&self) -> Option<&ClapPluginParams> {
1386        let ext_id = c"clap.params";
1387        // SAFETY: plugin pointer is valid while self is alive.
1388        let plugin = unsafe { &*self.plugin };
1389        let get_extension = plugin.get_extension?;
1390        // SAFETY: extension id is a valid static C string.
1391        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1392        if ext_ptr.is_null() {
1393            return None;
1394        }
1395        // SAFETY: CLAP guarantees extension pointer layout for requested extension id.
1396        Some(unsafe { &*(ext_ptr as *const ClapPluginParams) })
1397    }
1398
1399    fn state_ext(&self) -> Option<&ClapPluginStateExt> {
1400        let ext_id = c"clap.state";
1401        // SAFETY: plugin pointer is valid while self is alive.
1402        let plugin = unsafe { &*self.plugin };
1403        let get_extension = plugin.get_extension?;
1404        // SAFETY: extension id is valid static C string.
1405        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1406        if ext_ptr.is_null() {
1407            return None;
1408        }
1409        // SAFETY: extension pointer layout follows clap.state ABI.
1410        Some(unsafe { &*(ext_ptr as *const ClapPluginStateExt) })
1411    }
1412
1413    fn audio_ports_ext(&self) -> Option<&ClapPluginAudioPorts> {
1414        let ext_id = c"clap.audio-ports";
1415        // SAFETY: plugin pointer is valid while self is alive.
1416        let plugin = unsafe { &*self.plugin };
1417        let get_extension = plugin.get_extension?;
1418        // SAFETY: extension id is valid static C string.
1419        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1420        if ext_ptr.is_null() {
1421            return None;
1422        }
1423        // SAFETY: extension pointer layout follows clap.audio-ports ABI.
1424        Some(unsafe { &*(ext_ptr as *const ClapPluginAudioPorts) })
1425    }
1426
1427    fn note_ports_ext(&self) -> Option<&ClapPluginNotePorts> {
1428        let ext_id = c"clap.note-ports";
1429        // SAFETY: plugin pointer is valid while self is alive.
1430        let plugin = unsafe { &*self.plugin };
1431        let get_extension = plugin.get_extension?;
1432        // SAFETY: extension id is valid static C string.
1433        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1434        if ext_ptr.is_null() {
1435            return None;
1436        }
1437        // SAFETY: extension pointer layout follows clap.note-ports ABI.
1438        Some(unsafe { &*(ext_ptr as *const ClapPluginNotePorts) })
1439    }
1440
1441    fn note_name_ext(&self) -> Option<&ClapPluginNoteName> {
1442        let ext_id = c"clap.note-name";
1443        let plugin = unsafe { &*self.plugin };
1444        let get_extension = plugin.get_extension?;
1445        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1446        if ext_ptr.is_null() {
1447            return None;
1448        }
1449        Some(unsafe { &*(ext_ptr as *const ClapPluginNoteName) })
1450    }
1451
1452    fn get_note_names(&self) -> std::collections::HashMap<u8, String> {
1453        let mut result = std::collections::HashMap::new();
1454        let Some(ext) = self.note_name_ext() else {
1455            return result;
1456        };
1457        let Some(count_fn) = ext.count else {
1458            return result;
1459        };
1460        let Some(get_fn) = ext.get else {
1461            return result;
1462        };
1463        let count = unsafe { count_fn(self.plugin) };
1464        for i in 0..count {
1465            let mut nn = ClapNoteName {
1466                name: [0; 256],
1467                port: -1,
1468                key: -1,
1469                channel: -1,
1470            };
1471            if unsafe { get_fn(self.plugin, i, &mut nn) } {
1472                let name = unsafe {
1473                    std::ffi::CStr::from_ptr(nn.name.as_ptr())
1474                        .to_string_lossy()
1475                        .into_owned()
1476                };
1477                if nn.key >= 0 && nn.key <= 127 && !name.is_empty() {
1478                    result.insert(nn.key as u8, name);
1479                }
1480            }
1481        }
1482        result
1483    }
1484
1485    fn parameter_infos(&self) -> Vec<ClapParameterInfo> {
1486        let Some(params) = self.params_ext() else {
1487            return Vec::new();
1488        };
1489        let Some(count_fn) = params.count else {
1490            return Vec::new();
1491        };
1492        let Some(get_info_fn) = params.get_info else {
1493            return Vec::new();
1494        };
1495        // SAFETY: function pointers come from plugin extension table.
1496        let count = unsafe { count_fn(self.plugin) };
1497        let mut out = Vec::with_capacity(count as usize);
1498        for idx in 0..count {
1499            let mut info = ClapParamInfoRaw {
1500                id: 0,
1501                flags: 0,
1502                cookie: std::ptr::null_mut(),
1503                name: [0; 256],
1504                module: [0; 1024],
1505                min_value: 0.0,
1506                max_value: 1.0,
1507                default_value: 0.0,
1508            };
1509            // SAFETY: info points to valid writable struct.
1510            if unsafe { !get_info_fn(self.plugin, idx, &mut info as *mut _) } {
1511                continue;
1512            }
1513            out.push(ClapParameterInfo {
1514                id: info.id,
1515                name: c_char_buf_to_string(&info.name),
1516                module: c_char_buf_to_string(&info.module),
1517                min_value: info.min_value,
1518                max_value: info.max_value,
1519                default_value: info.default_value,
1520            });
1521        }
1522        out
1523    }
1524
1525    fn parameter_values(&self, infos: &[ClapParameterInfo]) -> HashMap<u32, f64> {
1526        let mut out = HashMap::new();
1527        let Some(params) = self.params_ext() else {
1528            for info in infos {
1529                out.insert(info.id, info.default_value);
1530            }
1531            return out;
1532        };
1533        let Some(get_value_fn) = params.get_value else {
1534            for info in infos {
1535                out.insert(info.id, info.default_value);
1536            }
1537            return out;
1538        };
1539        for info in infos {
1540            let mut value = info.default_value;
1541            // SAFETY: pointer to stack `value` is valid and param id belongs to plugin metadata.
1542            if unsafe { !get_value_fn(self.plugin, info.id, &mut value as *mut _) } {
1543                value = info.default_value;
1544            }
1545            out.insert(info.id, value);
1546        }
1547        out
1548    }
1549
1550    fn flush_params(&self, param_events: &[PendingParamEvent]) -> Vec<PendingParamValue> {
1551        let Some(params) = self.params_ext() else {
1552            return Vec::new();
1553        };
1554        let Some(flush_fn) = params.flush else {
1555            return Vec::new();
1556        };
1557        let (in_events, _in_ctx) = param_input_events_from(param_events);
1558        let out_cap = param_events.len().max(32);
1559        let (out_events, mut out_ctx) = output_events_ctx(out_cap);
1560        // SAFETY: input/output event wrappers stay valid for duration of flush callback.
1561        unsafe {
1562            flush_fn(self.plugin, &in_events, &out_events);
1563        }
1564        std::mem::take(&mut out_ctx.param_values)
1565    }
1566
1567    fn snapshot_state(&self) -> Result<ClapPluginState, String> {
1568        let Some(state_ext) = self.state_ext() else {
1569            return Ok(ClapPluginState { bytes: Vec::new() });
1570        };
1571        let Some(save_fn) = state_ext.save else {
1572            return Ok(ClapPluginState { bytes: Vec::new() });
1573        };
1574        let mut bytes = Vec::<u8>::new();
1575        let mut stream = ClapOStream {
1576            ctx: (&mut bytes as *mut Vec<u8>).cast::<c_void>(),
1577            write: Some(clap_ostream_write),
1578        };
1579        // SAFETY: stream callbacks reference `bytes` for duration of call.
1580        if unsafe {
1581            !save_fn(
1582                self.plugin,
1583                &mut stream as *mut ClapOStream as *const ClapOStream,
1584            )
1585        } {
1586            return Err("CLAP state save failed".to_string());
1587        }
1588        Ok(ClapPluginState { bytes })
1589    }
1590
1591    fn restore_state(&self, state: &ClapPluginState) -> Result<(), String> {
1592        let Some(state_ext) = self.state_ext() else {
1593            return Ok(());
1594        };
1595        let Some(load_fn) = state_ext.load else {
1596            return Ok(());
1597        };
1598        let mut ctx = ClapIStreamCtx {
1599            bytes: &state.bytes,
1600            offset: 0,
1601        };
1602        let mut stream = ClapIStream {
1603            ctx: (&mut ctx as *mut ClapIStreamCtx).cast::<c_void>(),
1604            read: Some(clap_istream_read),
1605        };
1606        // SAFETY: stream callbacks reference `ctx` for duration of call.
1607        if unsafe {
1608            !load_fn(
1609                self.plugin,
1610                &mut stream as *mut ClapIStream as *const ClapIStream,
1611            )
1612        } {
1613            return Err("CLAP state load failed".to_string());
1614        }
1615        Ok(())
1616    }
1617
1618    const CLAP_AUDIO_PORT_IS_MAIN: u32 = 1;
1619
1620    fn audio_port_channels(&self) -> (Option<AudioPortLayout>, Option<AudioPortLayout>) {
1621        let Some(ext) = self.audio_ports_ext() else {
1622            return (None, None);
1623        };
1624        let Some(count_fn) = ext.count else {
1625            return (None, None);
1626        };
1627        let Some(get_fn) = ext.get else {
1628            return (None, None);
1629        };
1630
1631        let read_ports = |is_input: bool| -> AudioPortLayout {
1632            let mut channels = Vec::new();
1633            let mut main_count = 0;
1634            let count = unsafe { count_fn(self.plugin, is_input) } as usize;
1635            channels.reserve(count);
1636            for idx in 0..count {
1637                let mut info = ClapAudioPortInfoRaw {
1638                    id: 0,
1639                    name: [0; 256],
1640                    flags: 0,
1641                    channel_count: 1,
1642                    port_type: std::ptr::null(),
1643                    in_place_pair: u32::MAX,
1644                };
1645                if unsafe { get_fn(self.plugin, idx as u32, is_input, &mut info as *mut _) } {
1646                    channels.push((info.channel_count as usize).max(1));
1647                    if info.flags & Self::CLAP_AUDIO_PORT_IS_MAIN != 0 {
1648                        main_count += 1;
1649                    }
1650                }
1651            }
1652            (channels, main_count)
1653        };
1654        (Some(read_ports(true)), Some(read_ports(false)))
1655    }
1656
1657    fn note_port_layout(&self) -> (Option<usize>, Option<usize>) {
1658        let Some(ext) = self.note_ports_ext() else {
1659            return (None, None);
1660        };
1661        let Some(count_fn) = ext.count else {
1662            return (None, None);
1663        };
1664        // SAFETY: function pointer comes from plugin extension table.
1665        let in_count = unsafe { count_fn(self.plugin, true) } as usize;
1666        // SAFETY: function pointer comes from plugin extension table.
1667        let out_count = unsafe { count_fn(self.plugin, false) } as usize;
1668        (Some(in_count), Some(out_count))
1669    }
1670
1671    fn gui_ext(&self) -> Option<&ClapPluginGui> {
1672        let ext_id = c"clap.gui";
1673        let plugin = unsafe { &*self.plugin };
1674        let get_extension = plugin.get_extension?;
1675        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1676        if ext_ptr.is_null() {
1677            return None;
1678        }
1679        Some(unsafe { &*(ext_ptr as *const ClapPluginGui) })
1680    }
1681
1682    fn gui_timer_support_ext(&self) -> Option<&ClapPluginTimerSupport> {
1683        let ext_id = c"clap.timer-support";
1684        let plugin = unsafe { &*self.plugin };
1685        let get_extension = plugin.get_extension?;
1686        let ext_ptr = unsafe { get_extension(self.plugin, ext_id.as_ptr()) };
1687        if ext_ptr.is_null() {
1688            return None;
1689        }
1690        Some(unsafe { &*(ext_ptr as *const ClapPluginTimerSupport) })
1691    }
1692
1693    fn gui_info(&self) -> Result<ClapGuiInfo, String> {
1694        let gui = self
1695            .gui_ext()
1696            .ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
1697        let is_api_supported = gui
1698            .is_api_supported
1699            .ok_or_else(|| "CLAP gui.is_api_supported is unavailable".to_string())?;
1700        for (api, supports_embedded) in [
1701            ("x11", true),
1702            ("cocoa", true),
1703            ("x11", false),
1704            ("cocoa", false),
1705        ] {
1706            let api_c = CString::new(api).map_err(|e| e.to_string())?;
1707            if unsafe { is_api_supported(self.plugin, api_c.as_ptr(), !supports_embedded) } {
1708                return Ok(ClapGuiInfo {
1709                    api: api.to_string(),
1710                    supports_embedded,
1711                });
1712            }
1713        }
1714        Err("No supported CLAP GUI API found".to_string())
1715    }
1716
1717    fn gui_create(&self, api: &str, is_floating: bool) -> Result<(), String> {
1718        let gui = self
1719            .gui_ext()
1720            .ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
1721        let create = gui
1722            .create
1723            .ok_or_else(|| "CLAP gui.create is unavailable".to_string())?;
1724        let api_c = CString::new(api).map_err(|e| e.to_string())?;
1725        if unsafe { !create(self.plugin, api_c.as_ptr(), is_floating) } {
1726            return Err("CLAP gui.create failed".to_string());
1727        }
1728        Ok(())
1729    }
1730
1731    fn gui_get_size(&self) -> Result<(u32, u32), String> {
1732        let gui = self
1733            .gui_ext()
1734            .ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
1735        let get_size = gui
1736            .get_size
1737            .ok_or_else(|| "CLAP gui.get_size is unavailable".to_string())?;
1738        let mut width = 0;
1739        let mut height = 0;
1740        if unsafe { !get_size(self.plugin, &mut width, &mut height) } {
1741            return Err("CLAP gui.get_size failed".to_string());
1742        }
1743        Ok((width, height))
1744    }
1745
1746    fn gui_set_parent_x11(&self, window: usize) -> Result<(), String> {
1747        let gui = self
1748            .gui_ext()
1749            .ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
1750        let set_parent = gui
1751            .set_parent
1752            .ok_or_else(|| "CLAP gui.set_parent is unavailable".to_string())?;
1753        let clap_window = ClapWindow {
1754            api: c"x11".as_ptr(),
1755            handle: ClapWindowHandle { x11: window },
1756        };
1757        if unsafe { !set_parent(self.plugin, &clap_window) } {
1758            return Err("CLAP gui.set_parent failed".to_string());
1759        }
1760        Ok(())
1761    }
1762
1763    fn gui_show(&self) -> Result<(), String> {
1764        let gui = self
1765            .gui_ext()
1766            .ok_or_else(|| "CLAP plugin does not expose clap.gui".to_string())?;
1767        let show = gui
1768            .show
1769            .ok_or_else(|| "CLAP gui.show is unavailable".to_string())?;
1770        if unsafe { !show(self.plugin) } {
1771            return Err("CLAP gui.show failed".to_string());
1772        }
1773        Ok(())
1774    }
1775
1776    fn gui_hide(&self) {
1777        if let Some(gui) = self.gui_ext()
1778            && let Some(hide) = gui.hide
1779        {
1780            unsafe { hide(self.plugin) };
1781        }
1782    }
1783
1784    fn gui_destroy(&self) {
1785        if let Some(gui) = self.gui_ext()
1786            && let Some(destroy) = gui.destroy
1787        {
1788            unsafe { destroy(self.plugin) };
1789        }
1790    }
1791
1792    fn gui_on_timer(&self, timer_id: u32) {
1793        if let Some(timer_ext) = self.gui_timer_support_ext()
1794            && let Some(on_timer) = timer_ext.on_timer
1795        {
1796            unsafe { on_timer(self.plugin, timer_id) };
1797        }
1798    }
1799}
1800
1801impl Drop for PluginHandle {
1802    fn drop(&mut self) {
1803        // SAFETY: pointers were obtained from valid CLAP entry and plugin factory.
1804        unsafe {
1805            if !self.plugin.is_null() {
1806                let plugin = &*self.plugin;
1807                if let Some(stop_processing) = plugin.stop_processing {
1808                    stop_processing(self.plugin);
1809                }
1810                if let Some(deactivate) = plugin.deactivate {
1811                    deactivate(self.plugin);
1812                }
1813                if let Some(destroy) = plugin.destroy {
1814                    destroy(self.plugin);
1815                }
1816            }
1817            if !self.entry.is_null() {
1818                let entry = &*self.entry;
1819                if let Some(deinit) = entry.deinit {
1820                    deinit();
1821                }
1822            }
1823        }
1824    }
1825}
1826
1827static HOST_THREAD_CHECK_EXT: ClapHostThreadCheck = ClapHostThreadCheck {
1828    is_main_thread: Some(host_is_main_thread),
1829    is_audio_thread: Some(host_is_audio_thread),
1830};
1831static HOST_LATENCY_EXT: ClapHostLatency = ClapHostLatency {
1832    changed: Some(host_latency_changed),
1833};
1834static HOST_TAIL_EXT: ClapHostTail = ClapHostTail {
1835    changed: Some(host_tail_changed),
1836};
1837static HOST_TIMER_EXT: ClapHostTimerSupport = ClapHostTimerSupport {
1838    register_timer: Some(host_timer_register),
1839    unregister_timer: Some(host_timer_unregister),
1840};
1841static HOST_GUI_EXT: ClapHostGui = ClapHostGui {
1842    resize_hints_changed: Some(host_gui_resize_hints_changed),
1843    request_resize: Some(host_gui_request_resize),
1844    request_show: Some(host_gui_request_show),
1845    request_hide: Some(host_gui_request_hide),
1846    closed: Some(host_gui_closed),
1847};
1848static HOST_PARAMS_EXT: ClapHostParams = ClapHostParams {
1849    rescan: Some(host_params_rescan),
1850    clear: Some(host_params_clear),
1851    request_flush: Some(host_params_request_flush),
1852};
1853static HOST_STATE_EXT: ClapHostState = ClapHostState {
1854    mark_dirty: Some(host_state_mark_dirty),
1855};
1856static HOST_NOTE_NAME_EXT: ClapHostNoteName = ClapHostNoteName {
1857    changed: Some(host_note_name_changed),
1858};
1859static NEXT_TIMER_ID: AtomicU32 = AtomicU32::new(1);
1860
1861fn host_runtime_state(host: *const ClapHost) -> Option<&'static HostRuntimeState> {
1862    if host.is_null() {
1863        return None;
1864    }
1865    let state_ptr = unsafe { (*host).host_data as *const HostRuntimeState };
1866    if state_ptr.is_null() {
1867        return None;
1868    }
1869    Some(unsafe { &*state_ptr })
1870}
1871
1872unsafe extern "C" fn host_get_extension(
1873    _host: *const ClapHost,
1874    _extension_id: *const c_char,
1875) -> *const c_void {
1876    if _extension_id.is_null() {
1877        return std::ptr::null();
1878    }
1879    // SAFETY: extension id is expected to be a valid NUL-terminated string.
1880    let id = unsafe { CStr::from_ptr(_extension_id) }.to_string_lossy();
1881    match id.as_ref() {
1882        "clap.host.thread-check" => {
1883            (&HOST_THREAD_CHECK_EXT as *const ClapHostThreadCheck).cast::<c_void>()
1884        }
1885        "clap.host.latency" => (&HOST_LATENCY_EXT as *const ClapHostLatency).cast::<c_void>(),
1886        "clap.host.tail" => (&HOST_TAIL_EXT as *const ClapHostTail).cast::<c_void>(),
1887        "clap.host.timer-support" => {
1888            (&HOST_TIMER_EXT as *const ClapHostTimerSupport).cast::<c_void>()
1889        }
1890        "clap.host.gui" => host_runtime_state(_host)
1891            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
1892            .map(|_| (&HOST_GUI_EXT as *const ClapHostGui).cast::<c_void>())
1893            .unwrap_or(std::ptr::null()),
1894        "clap.host.params" => host_runtime_state(_host)
1895            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
1896            .map(|_| (&HOST_PARAMS_EXT as *const ClapHostParams).cast::<c_void>())
1897            .unwrap_or(std::ptr::null()),
1898        "clap.host.state" => host_runtime_state(_host)
1899            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
1900            .map(|_| (&HOST_STATE_EXT as *const ClapHostState).cast::<c_void>())
1901            .unwrap_or(std::ptr::null()),
1902        "clap.host.note-name" => (&HOST_NOTE_NAME_EXT as *const ClapHostNoteName).cast::<c_void>(),
1903        _ => std::ptr::null(),
1904    }
1905}
1906
1907unsafe extern "C" fn host_request_process(_host: *const ClapHost) {
1908    if let Some(state) = host_runtime_state(_host) {
1909        state.callback_flags.lock().process = true;
1910    }
1911}
1912
1913unsafe extern "C" fn host_request_callback(_host: *const ClapHost) {
1914    if let Some(state) = host_runtime_state(_host) {
1915        state.callback_flags.lock().callback = true;
1916    }
1917}
1918
1919unsafe extern "C" fn host_request_restart(_host: *const ClapHost) {
1920    if let Some(state) = host_runtime_state(_host) {
1921        state.callback_flags.lock().restart = true;
1922    }
1923}
1924
1925unsafe extern "C" fn host_note_name_changed(_host: *const ClapHost) {
1926    if let Some(state) = host_runtime_state(_host) {
1927        state.note_names_dirty.store(1, Ordering::Release);
1928    }
1929}
1930
1931unsafe extern "C" fn host_is_main_thread(_host: *const ClapHost) -> bool {
1932    CLAP_HOST_MAIN_THREAD.with(Cell::get)
1933}
1934
1935unsafe extern "C" fn host_is_audio_thread(_host: *const ClapHost) -> bool {
1936    CLAP_HOST_AUDIO_THREAD.with(Cell::get)
1937}
1938
1939unsafe extern "C" fn host_latency_changed(_host: *const ClapHost) {}
1940
1941unsafe extern "C" fn host_tail_changed(_host: *const ClapHost) {}
1942
1943unsafe extern "C" fn host_timer_register(
1944    _host: *const ClapHost,
1945    _period_ms: u32,
1946    timer_id: *mut u32,
1947) -> bool {
1948    if timer_id.is_null() {
1949        return false;
1950    }
1951    let id = NEXT_TIMER_ID.fetch_add(1, Ordering::Relaxed);
1952    if let Some(state) = host_runtime_state(_host) {
1953        let period_ms = _period_ms.max(1);
1954        state.timers.lock().push(HostTimer {
1955            id,
1956            period: Duration::from_millis(period_ms as u64),
1957            next_tick: Instant::now() + Duration::from_millis(period_ms as u64),
1958        });
1959    }
1960    // SAFETY: timer_id points to writable u32 provided by plugin.
1961    unsafe {
1962        *timer_id = id;
1963    }
1964    true
1965}
1966
1967unsafe extern "C" fn host_timer_unregister(_host: *const ClapHost, _timer_id: u32) -> bool {
1968    if let Some(state) = host_runtime_state(_host) {
1969        state.timers.lock().retain(|timer| timer.id != _timer_id);
1970    }
1971    true
1972}
1973
1974unsafe extern "C" fn host_gui_resize_hints_changed(_host: *const ClapHost) {}
1975
1976unsafe extern "C" fn host_gui_request_resize(
1977    _host: *const ClapHost,
1978    _width: u32,
1979    _height: u32,
1980) -> bool {
1981    true
1982}
1983
1984unsafe extern "C" fn host_gui_request_show(_host: *const ClapHost) -> bool {
1985    true
1986}
1987
1988unsafe extern "C" fn host_gui_request_hide(_host: *const ClapHost) -> bool {
1989    if let Some(state) = host_runtime_state(_host) {
1990        if state.ui_active.load(Ordering::Acquire) != 0 {
1991            state.ui_should_close.store(1, Ordering::Release);
1992        }
1993        true
1994    } else {
1995        false
1996    }
1997}
1998
1999unsafe extern "C" fn host_gui_closed(_host: *const ClapHost, _was_destroyed: bool) {
2000    if let Some(state) = host_runtime_state(_host)
2001        && state.ui_active.load(Ordering::Acquire) != 0
2002    {
2003        state.ui_should_close.store(1, Ordering::Release);
2004    }
2005}
2006
2007unsafe extern "C" fn host_params_rescan(_host: *const ClapHost, _flags: u32) {}
2008
2009unsafe extern "C" fn host_params_clear(_host: *const ClapHost, _param_id: u32, _flags: u32) {}
2010
2011unsafe extern "C" fn host_params_request_flush(_host: *const ClapHost) {
2012    if let Some(state) = host_runtime_state(_host) {
2013        state.param_flush_requested.store(1, Ordering::Release);
2014        state.callback_flags.lock().callback = true;
2015    }
2016}
2017
2018unsafe extern "C" fn host_state_mark_dirty(_host: *const ClapHost) {
2019    if let Some(state) = host_runtime_state(_host) {
2020        state.state_dirty_requested.store(1, Ordering::Release);
2021        state.callback_flags.lock().callback = true;
2022    }
2023}
2024
2025unsafe extern "C" fn input_events_size(_list: *const ClapInputEvents) -> u32 {
2026    if _list.is_null() {
2027        return 0;
2028    }
2029    // SAFETY: ctx points to ClapInputEventsCtx owned by process_native.
2030    let ctx = unsafe { (*_list).ctx as *const ClapInputEventsCtx };
2031    if ctx.is_null() {
2032        return 0;
2033    }
2034    // SAFETY: ctx is valid during process callback lifetime.
2035    unsafe { (*ctx).events.len() as u32 }
2036}
2037
2038unsafe extern "C" fn input_events_get(
2039    _list: *const ClapInputEvents,
2040    _index: u32,
2041) -> *const ClapEventHeader {
2042    if _list.is_null() {
2043        return std::ptr::null();
2044    }
2045    // SAFETY: ctx points to ClapInputEventsCtx owned by process_native.
2046    let ctx = unsafe { (*_list).ctx as *const ClapInputEventsCtx };
2047    if ctx.is_null() {
2048        return std::ptr::null();
2049    }
2050    // SAFETY: ctx is valid during process callback lifetime.
2051    let events = unsafe { &(*ctx).events };
2052    let Some(event) = events.get(_index as usize) else {
2053        return std::ptr::null();
2054    };
2055    event.header_ptr()
2056}
2057
2058unsafe extern "C" fn output_events_try_push(
2059    _list: *const ClapOutputEvents,
2060    _event: *const ClapEventHeader,
2061) -> bool {
2062    if _list.is_null() || _event.is_null() {
2063        return false;
2064    }
2065    // SAFETY: ctx points to ClapOutputEventsCtx owned by process_native.
2066    let ctx = unsafe { (*_list).ctx as *mut ClapOutputEventsCtx };
2067    if ctx.is_null() {
2068        return false;
2069    }
2070    // SAFETY: event pointer is valid for callback lifetime.
2071    let header = unsafe { &*_event };
2072    if header.space_id != CLAP_CORE_EVENT_SPACE_ID {
2073        return false;
2074    }
2075    match header.type_ {
2076        CLAP_EVENT_MIDI => {
2077            if (header.size as usize) < std::mem::size_of::<ClapEventMidi>() {
2078                return false;
2079            }
2080            // SAFETY: validated type/size above.
2081            let midi = unsafe { &*(_event as *const ClapEventMidi) };
2082            // SAFETY: ctx pointer is valid and uniquely owned during processing.
2083            unsafe {
2084                (*ctx).midi_events.push(ClapMidiOutputEvent {
2085                    port: midi.port_index as usize,
2086                    event: MidiEvent::new(header.time, midi.data.to_vec()),
2087                });
2088            }
2089            true
2090        }
2091        CLAP_EVENT_PARAM_VALUE => {
2092            if (header.size as usize) < std::mem::size_of::<ClapEventParamValue>() {
2093                return false;
2094            }
2095            // SAFETY: validated type/size above.
2096            let param = unsafe { &*(_event as *const ClapEventParamValue) };
2097            // SAFETY: ctx pointer is valid and uniquely owned during processing.
2098            unsafe {
2099                (*ctx).param_values.push(PendingParamValue {
2100                    param_id: param.param_id,
2101                    value: param.value,
2102                });
2103            }
2104            true
2105        }
2106        _ => false,
2107    }
2108}
2109
2110fn input_events_from(
2111    midi_events: &[MidiEvent],
2112    param_events: &[PendingParamEvent],
2113    sample_rate: f64,
2114    transport: ClapTransportInfo,
2115    has_note_ports: bool,
2116) -> (ClapInputEvents, Box<ClapInputEventsCtx>) {
2117    let mut events = Vec::with_capacity(midi_events.len() + param_events.len() + 1);
2118    let bpm = transport.bpm.max(1.0);
2119    let sample_rate = sample_rate.max(1.0);
2120    let seconds = transport.transport_sample as f64 / sample_rate;
2121    let song_pos_seconds = (seconds * CLAP_SECTIME_FACTOR as f64) as i64;
2122    let beats = seconds * (bpm / 60.0);
2123    let song_pos_beats = (beats * CLAP_BEATTIME_FACTOR as f64) as i64;
2124    let mut flags = CLAP_TRANSPORT_HAS_TEMPO
2125        | CLAP_TRANSPORT_HAS_BEATS_TIMELINE
2126        | CLAP_TRANSPORT_HAS_SECONDS_TIMELINE
2127        | CLAP_TRANSPORT_HAS_TIME_SIGNATURE;
2128    if transport.playing {
2129        flags |= CLAP_TRANSPORT_IS_PLAYING;
2130    }
2131    let (loop_start_seconds, loop_end_seconds, loop_start_beats, loop_end_beats) =
2132        if transport.loop_enabled {
2133            if let Some((loop_start, loop_end)) = transport.loop_range_samples {
2134                flags |= CLAP_TRANSPORT_IS_LOOP_ACTIVE;
2135                let ls_sec = loop_start as f64 / sample_rate;
2136                let le_sec = loop_end as f64 / sample_rate;
2137                let ls_beats = ls_sec * (bpm / 60.0);
2138                let le_beats = le_sec * (bpm / 60.0);
2139                (
2140                    (ls_sec * CLAP_SECTIME_FACTOR as f64) as i64,
2141                    (le_sec * CLAP_SECTIME_FACTOR as f64) as i64,
2142                    (ls_beats * CLAP_BEATTIME_FACTOR as f64) as i64,
2143                    (le_beats * CLAP_BEATTIME_FACTOR as f64) as i64,
2144                )
2145            } else {
2146                (0, 0, 0, 0)
2147            }
2148        } else {
2149            (0, 0, 0, 0)
2150        };
2151    let ts_num = transport.tsig_num.max(1);
2152    let ts_denom = transport.tsig_denom.max(1);
2153    let beats_per_bar = ts_num as f64 * (4.0 / ts_denom as f64);
2154    let bar_number = if beats_per_bar > 0.0 {
2155        (beats / beats_per_bar).floor().max(0.0) as i32
2156    } else {
2157        0
2158    };
2159    let bar_start_beats = (bar_number as f64 * beats_per_bar * CLAP_BEATTIME_FACTOR as f64) as i64;
2160    events.push(ClapInputEvent::Transport(ClapEventTransport {
2161        header: ClapEventHeader {
2162            size: std::mem::size_of::<ClapEventTransport>() as u32,
2163            time: 0,
2164            space_id: CLAP_CORE_EVENT_SPACE_ID,
2165            type_: CLAP_EVENT_TRANSPORT,
2166            flags: 0,
2167        },
2168        flags,
2169        song_pos_beats,
2170        song_pos_seconds,
2171        tempo: bpm,
2172        tempo_inc: 0.0,
2173        loop_start_beats,
2174        loop_end_beats,
2175        loop_start_seconds,
2176        loop_end_seconds,
2177        bar_start: bar_start_beats,
2178        bar_number,
2179        tsig_num: ts_num,
2180        tsig_denom: ts_denom,
2181    }));
2182    for event in midi_events {
2183        if event.data.is_empty() {
2184            continue;
2185        }
2186        let mut data = [0_u8; 3];
2187        let bytes = event.data.len().min(3);
2188        data[..bytes].copy_from_slice(&event.data[..bytes]);
2189        let status = data[0];
2190        let is_note_on = (0x90..=0x9F).contains(&status);
2191        let is_note_off = (0x80..=0x8F).contains(&status);
2192        if has_note_ports && (is_note_on || is_note_off) {
2193            let channel = (status & 0x0F) as i16;
2194            let key = data.get(1).copied().unwrap_or(0).min(127) as i16;
2195            let velocity_byte = data.get(2).copied().unwrap_or(0);
2196            let velocity = if is_note_on && velocity_byte == 0 {
2197                // Note-on with velocity 0 is conventionally note-off.
2198                events.push(ClapInputEvent::Note(ClapEventNote {
2199                    header: ClapEventHeader {
2200                        size: std::mem::size_of::<ClapEventNote>() as u32,
2201                        time: event.frame,
2202                        space_id: CLAP_CORE_EVENT_SPACE_ID,
2203                        type_: CLAP_EVENT_NOTE_OFF,
2204                        flags: 0,
2205                    },
2206                    note_id: -1,
2207                    port_index: 0,
2208                    channel,
2209                    key,
2210                    velocity: 0.0,
2211                }));
2212                continue;
2213            } else {
2214                (velocity_byte as f64 / 127.0).clamp(0.0, 1.0)
2215            };
2216            events.push(ClapInputEvent::Note(ClapEventNote {
2217                header: ClapEventHeader {
2218                    size: std::mem::size_of::<ClapEventNote>() as u32,
2219                    time: event.frame,
2220                    space_id: CLAP_CORE_EVENT_SPACE_ID,
2221                    type_: if is_note_on {
2222                        CLAP_EVENT_NOTE_ON
2223                    } else {
2224                        CLAP_EVENT_NOTE_OFF
2225                    },
2226                    flags: 0,
2227                },
2228                note_id: -1,
2229                port_index: 0,
2230                channel,
2231                key,
2232                velocity,
2233            }));
2234        } else {
2235            events.push(ClapInputEvent::Midi(ClapEventMidi {
2236                header: ClapEventHeader {
2237                    size: std::mem::size_of::<ClapEventMidi>() as u32,
2238                    time: event.frame,
2239                    space_id: CLAP_CORE_EVENT_SPACE_ID,
2240                    type_: CLAP_EVENT_MIDI,
2241                    flags: 0,
2242                },
2243                port_index: 0,
2244                data,
2245            }));
2246        }
2247    }
2248    for param in param_events {
2249        match *param {
2250            PendingParamEvent::Value {
2251                param_id,
2252                value,
2253                frame,
2254            } => events.push(ClapInputEvent::ParamValue(ClapEventParamValue {
2255                header: ClapEventHeader {
2256                    size: std::mem::size_of::<ClapEventParamValue>() as u32,
2257                    time: frame,
2258                    space_id: CLAP_CORE_EVENT_SPACE_ID,
2259                    type_: CLAP_EVENT_PARAM_VALUE,
2260                    flags: 0,
2261                },
2262                param_id,
2263                cookie: std::ptr::null_mut(),
2264                note_id: -1,
2265                port_index: -1,
2266                channel: -1,
2267                key: -1,
2268                value,
2269            })),
2270            PendingParamEvent::GestureBegin { param_id, frame } => {
2271                events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
2272                    header: ClapEventHeader {
2273                        size: std::mem::size_of::<ClapEventParamGesture>() as u32,
2274                        time: frame,
2275                        space_id: CLAP_CORE_EVENT_SPACE_ID,
2276                        type_: CLAP_EVENT_PARAM_GESTURE_BEGIN,
2277                        flags: 0,
2278                    },
2279                    param_id,
2280                }))
2281            }
2282            PendingParamEvent::GestureEnd { param_id, frame } => {
2283                events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
2284                    header: ClapEventHeader {
2285                        size: std::mem::size_of::<ClapEventParamGesture>() as u32,
2286                        time: frame,
2287                        space_id: CLAP_CORE_EVENT_SPACE_ID,
2288                        type_: CLAP_EVENT_PARAM_GESTURE_END,
2289                        flags: 0,
2290                    },
2291                    param_id,
2292                }))
2293            }
2294        }
2295    }
2296    events.sort_by_key(|event| match event {
2297        ClapInputEvent::Note(e) => e.header.time,
2298        ClapInputEvent::Midi(e) => e.header.time,
2299        ClapInputEvent::ParamValue(e) => e.header.time,
2300        ClapInputEvent::ParamGesture(e) => e.header.time,
2301        ClapInputEvent::Transport(e) => e.header.time,
2302    });
2303    let mut ctx = Box::new(ClapInputEventsCtx { events });
2304    let list = ClapInputEvents {
2305        ctx: (&mut *ctx as *mut ClapInputEventsCtx).cast::<c_void>(),
2306        size: Some(input_events_size),
2307        get: Some(input_events_get),
2308    };
2309    (list, ctx)
2310}
2311
2312fn param_input_events_from(
2313    param_events: &[PendingParamEvent],
2314) -> (ClapInputEvents, Box<ClapInputEventsCtx>) {
2315    let mut events = Vec::with_capacity(param_events.len());
2316    for param in param_events {
2317        match *param {
2318            PendingParamEvent::Value {
2319                param_id,
2320                value,
2321                frame,
2322            } => events.push(ClapInputEvent::ParamValue(ClapEventParamValue {
2323                header: ClapEventHeader {
2324                    size: std::mem::size_of::<ClapEventParamValue>() as u32,
2325                    time: frame,
2326                    space_id: CLAP_CORE_EVENT_SPACE_ID,
2327                    type_: CLAP_EVENT_PARAM_VALUE,
2328                    flags: 0,
2329                },
2330                param_id,
2331                cookie: std::ptr::null_mut(),
2332                note_id: -1,
2333                port_index: -1,
2334                channel: -1,
2335                key: -1,
2336                value,
2337            })),
2338            PendingParamEvent::GestureBegin { param_id, frame } => {
2339                events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
2340                    header: ClapEventHeader {
2341                        size: std::mem::size_of::<ClapEventParamGesture>() as u32,
2342                        time: frame,
2343                        space_id: CLAP_CORE_EVENT_SPACE_ID,
2344                        type_: CLAP_EVENT_PARAM_GESTURE_BEGIN,
2345                        flags: 0,
2346                    },
2347                    param_id,
2348                }))
2349            }
2350            PendingParamEvent::GestureEnd { param_id, frame } => {
2351                events.push(ClapInputEvent::ParamGesture(ClapEventParamGesture {
2352                    header: ClapEventHeader {
2353                        size: std::mem::size_of::<ClapEventParamGesture>() as u32,
2354                        time: frame,
2355                        space_id: CLAP_CORE_EVENT_SPACE_ID,
2356                        type_: CLAP_EVENT_PARAM_GESTURE_END,
2357                        flags: 0,
2358                    },
2359                    param_id,
2360                }))
2361            }
2362        }
2363    }
2364    events.sort_by_key(|event| match event {
2365        ClapInputEvent::Note(e) => e.header.time,
2366        ClapInputEvent::Midi(e) => e.header.time,
2367        ClapInputEvent::ParamValue(e) => e.header.time,
2368        ClapInputEvent::ParamGesture(e) => e.header.time,
2369        ClapInputEvent::Transport(e) => e.header.time,
2370    });
2371    let mut ctx = Box::new(ClapInputEventsCtx { events });
2372    let list = ClapInputEvents {
2373        ctx: (&mut *ctx as *mut ClapInputEventsCtx).cast::<c_void>(),
2374        size: Some(input_events_size),
2375        get: Some(input_events_get),
2376    };
2377    (list, ctx)
2378}
2379
2380fn output_events_ctx(capacity: usize) -> (ClapOutputEvents, Box<ClapOutputEventsCtx>) {
2381    let mut ctx = Box::new(ClapOutputEventsCtx {
2382        midi_events: Vec::with_capacity(capacity),
2383        param_values: Vec::with_capacity(capacity / 2),
2384    });
2385    let list = ClapOutputEvents {
2386        ctx: (&mut *ctx as *mut ClapOutputEventsCtx).cast::<c_void>(),
2387        try_push: Some(output_events_try_push),
2388    };
2389    (list, ctx)
2390}
2391
2392fn c_char_buf_to_string<const N: usize>(buf: &[c_char; N]) -> String {
2393    let bytes = buf
2394        .iter()
2395        .take_while(|&&b| b != 0)
2396        .map(|&b| b as u8)
2397        .collect::<Vec<u8>>();
2398    String::from_utf8_lossy(&bytes).to_string()
2399}
2400
2401fn split_plugin_spec(spec: &str) -> (&str, Option<&str>) {
2402    if let Some((path, id)) = spec.split_once("::")
2403        && !id.trim().is_empty()
2404    {
2405        return (path, Some(id.trim()));
2406    }
2407    (spec, None)
2408}
2409
2410unsafe extern "C" fn clap_ostream_write(
2411    stream: *const ClapOStream,
2412    buffer: *const c_void,
2413    size: u64,
2414) -> i64 {
2415    if stream.is_null() || buffer.is_null() {
2416        return -1;
2417    }
2418    // SAFETY: ctx is initialized by snapshot_state and valid during callback.
2419    let ctx = unsafe { (*stream).ctx as *mut Vec<u8> };
2420    if ctx.is_null() {
2421        return -1;
2422    }
2423    let n = (size as usize).min(isize::MAX as usize);
2424    // SAFETY: source pointer is valid for `n` bytes per caller contract.
2425    let src = unsafe { std::slice::from_raw_parts(buffer.cast::<u8>(), n) };
2426    // SAFETY: ctx points to writable Vec<u8>.
2427    unsafe {
2428        (*ctx).extend_from_slice(src);
2429    }
2430    n as i64
2431}
2432
2433unsafe extern "C" fn clap_istream_read(
2434    stream: *const ClapIStream,
2435    buffer: *mut c_void,
2436    size: u64,
2437) -> i64 {
2438    if stream.is_null() || buffer.is_null() {
2439        return -1;
2440    }
2441    // SAFETY: ctx is initialized by restore_state and valid during callback.
2442    let ctx = unsafe { (*stream).ctx as *mut ClapIStreamCtx<'_> };
2443    if ctx.is_null() {
2444        return -1;
2445    }
2446    // SAFETY: ctx points to valid read context.
2447    let ctx = unsafe { &mut *ctx };
2448    let remaining = ctx.bytes.len().saturating_sub(ctx.offset);
2449    if remaining == 0 {
2450        return 0;
2451    }
2452    let n = remaining.min(size as usize);
2453    // SAFETY: destination pointer is valid for `n` bytes per caller contract.
2454    let dst = unsafe { std::slice::from_raw_parts_mut(buffer.cast::<u8>(), n) };
2455    dst.copy_from_slice(&ctx.bytes[ctx.offset..ctx.offset + n]);
2456    ctx.offset += n;
2457    n as i64
2458}
2459
2460pub fn list_plugins() -> Vec<ClapPluginInfo> {
2461    list_plugins_with_capabilities(false)
2462}
2463
2464pub fn is_supported_clap_binary(path: &Path) -> bool {
2465    path.extension().is_some_and(|ext| {
2466        ext.eq_ignore_ascii_case("clap")
2467            || ext.eq_ignore_ascii_case("so")
2468            || ext.eq_ignore_ascii_case("dylib")
2469            || ext.eq_ignore_ascii_case("dll")
2470    })
2471}
2472
2473pub fn list_plugins_with_capabilities(scan_capabilities: bool) -> Vec<ClapPluginInfo> {
2474    let mut roots = default_clap_search_roots();
2475
2476    if let Ok(extra) = std::env::var("CLAP_PATH") {
2477        for p in std::env::split_paths(&extra) {
2478            if !p.as_os_str().is_empty() {
2479                roots.push(p);
2480            }
2481        }
2482    }
2483
2484    let mut out = Vec::new();
2485    for root in roots {
2486        collect_clap_plugins(&root, &mut out, scan_capabilities);
2487    }
2488
2489    out.sort_by_key(|a| a.name.to_lowercase());
2490    out.dedup_by(|a, b| a.path.eq_ignore_ascii_case(&b.path));
2491    out
2492}
2493
2494fn collect_clap_plugins(root: &Path, out: &mut Vec<ClapPluginInfo>, scan_capabilities: bool) {
2495    let Ok(entries) = std::fs::read_dir(root) else {
2496        return;
2497    };
2498    for entry in entries.flatten() {
2499        let path = entry.path();
2500        let Ok(ft) = entry.file_type() else {
2501            continue;
2502        };
2503        if ft.is_dir() {
2504            if path
2505                .file_name()
2506                .and_then(|name| name.to_str())
2507                .is_some_and(|name| {
2508                    matches!(
2509                        name,
2510                        "deps" | "build" | "incremental" | ".fingerprint" | "examples"
2511                    )
2512                })
2513            {
2514                continue;
2515            }
2516            collect_clap_plugins(&path, out, scan_capabilities);
2517            continue;
2518        }
2519
2520        if is_supported_clap_binary(&path) {
2521            let infos = scan_bundle_descriptors(&path, scan_capabilities);
2522            if infos.is_empty() {
2523                let name = path
2524                    .file_stem()
2525                    .map(|s| s.to_string_lossy().to_string())
2526                    .unwrap_or_else(|| path.to_string_lossy().to_string());
2527                out.push(ClapPluginInfo {
2528                    name,
2529                    path: path.to_string_lossy().to_string(),
2530                    capabilities: None,
2531                });
2532            } else {
2533                out.extend(infos);
2534            }
2535        }
2536    }
2537}
2538
2539fn scan_bundle_descriptors(path: &Path, scan_capabilities: bool) -> Vec<ClapPluginInfo> {
2540    let path_str = path.to_string_lossy().to_string();
2541    let factory_id = c"clap.plugin-factory";
2542    let host_runtime = match HostRuntime::new() {
2543        Ok(runtime) => runtime,
2544        Err(_) => return Vec::new(),
2545    };
2546    // SAFETY: path points to plugin module file.
2547    let library = match unsafe { Library::new(path) } {
2548        Ok(lib) => lib,
2549        Err(_) => return Vec::new(),
2550    };
2551    // SAFETY: symbol is CLAP entry pointer.
2552    let entry_ptr = unsafe {
2553        match library.get::<*const ClapPluginEntry>(b"clap_entry\0") {
2554            Ok(sym) => *sym,
2555            Err(_) => return Vec::new(),
2556        }
2557    };
2558    if entry_ptr.is_null() {
2559        return Vec::new();
2560    }
2561    // SAFETY: entry pointer validated above.
2562    let entry = unsafe { &*entry_ptr };
2563    let Some(init) = entry.init else {
2564        return Vec::new();
2565    };
2566    let host_ptr = &host_runtime.host;
2567    // SAFETY: valid host pointer.
2568    if unsafe { !init(host_ptr) } {
2569        return Vec::new();
2570    }
2571    let mut out = Vec::new();
2572    if let Some(get_factory) = entry.get_factory {
2573        // SAFETY: static factory id.
2574        let factory = unsafe { get_factory(factory_id.as_ptr()) } as *const ClapPluginFactory;
2575        if !factory.is_null() {
2576            // SAFETY: factory pointer validated above.
2577            let factory_ref = unsafe { &*factory };
2578            if let (Some(get_count), Some(get_desc)) = (
2579                factory_ref.get_plugin_count,
2580                factory_ref.get_plugin_descriptor,
2581            ) {
2582                // SAFETY: function pointer from plugin.
2583                let count = unsafe { get_count(factory) };
2584                for i in 0..count {
2585                    // SAFETY: i < count.
2586                    let desc = unsafe { get_desc(factory, i) };
2587                    if desc.is_null() {
2588                        continue;
2589                    }
2590                    // SAFETY: descriptor pointer from plugin factory.
2591                    let desc = unsafe { &*desc };
2592                    if desc.id.is_null() || desc.name.is_null() {
2593                        continue;
2594                    }
2595                    // SAFETY: CLAP descriptor strings are NUL-terminated.
2596                    let id = unsafe { CStr::from_ptr(desc.id) }
2597                        .to_string_lossy()
2598                        .to_string();
2599                    // SAFETY: CLAP descriptor strings are NUL-terminated.
2600                    let name = unsafe { CStr::from_ptr(desc.name) }
2601                        .to_string_lossy()
2602                        .to_string();
2603
2604                    let capabilities = if scan_capabilities {
2605                        scan_plugin_capabilities(factory_ref, factory, &host_runtime.host, &id)
2606                    } else {
2607                        None
2608                    };
2609
2610                    out.push(ClapPluginInfo {
2611                        name,
2612                        path: format!("{path_str}::{id}"),
2613                        capabilities,
2614                    });
2615                }
2616            }
2617        }
2618    }
2619    // SAFETY: deinit belongs to entry and is valid after init.
2620    if let Some(deinit) = entry.deinit {
2621        unsafe { deinit() };
2622    }
2623    out
2624}
2625
2626fn scan_plugin_capabilities(
2627    factory: &ClapPluginFactory,
2628    factory_ptr: *const ClapPluginFactory,
2629    host: &ClapHost,
2630    plugin_id: &str,
2631) -> Option<ClapPluginCapabilities> {
2632    let create = factory.create_plugin?;
2633
2634    let id_cstring = CString::new(plugin_id).ok()?;
2635    // SAFETY: valid factory, host, and id pointers.
2636    let plugin = unsafe { create(factory_ptr, host, id_cstring.as_ptr()) };
2637    if plugin.is_null() {
2638        return None;
2639    }
2640
2641    // SAFETY: plugin pointer validated above.
2642    let plugin_ref = unsafe { &*plugin };
2643    let plugin_init = plugin_ref.init?;
2644
2645    // SAFETY: plugin pointer and function pointer follow CLAP ABI.
2646    if unsafe { !plugin_init(plugin) } {
2647        return None;
2648    }
2649
2650    let mut capabilities = ClapPluginCapabilities {
2651        has_gui: false,
2652        gui_apis: Vec::new(),
2653        supports_embedded: false,
2654        supports_floating: false,
2655        has_params: false,
2656        has_state: false,
2657        audio_inputs: 0,
2658        audio_outputs: 0,
2659        midi_inputs: 0,
2660        midi_outputs: 0,
2661    };
2662
2663    // Check for extensions
2664    if let Some(get_extension) = plugin_ref.get_extension {
2665        // Check GUI extension
2666        let gui_ext_id = c"clap.gui";
2667        // SAFETY: extension id is valid static C string.
2668        let gui_ptr = unsafe { get_extension(plugin, gui_ext_id.as_ptr()) };
2669        if !gui_ptr.is_null() {
2670            capabilities.has_gui = true;
2671            // SAFETY: CLAP guarantees extension pointer layout for requested extension id.
2672            let gui = unsafe { &*(gui_ptr as *const ClapPluginGui) };
2673
2674            // Check which GUI APIs are supported
2675            if let Some(is_api_supported) = gui.is_api_supported {
2676                for api in ["x11", "cocoa"] {
2677                    if let Ok(api_cstr) = CString::new(api) {
2678                        // Check embedded mode
2679                        // SAFETY: valid plugin and API string pointers.
2680                        if unsafe { is_api_supported(plugin, api_cstr.as_ptr(), false) } {
2681                            capabilities.gui_apis.push(format!("{} (embedded)", api));
2682                            capabilities.supports_embedded = true;
2683                        }
2684                        // Check floating mode
2685                        // SAFETY: valid plugin and API string pointers.
2686                        if unsafe { is_api_supported(plugin, api_cstr.as_ptr(), true) } {
2687                            if !capabilities.supports_embedded {
2688                                capabilities.gui_apis.push(format!("{} (floating)", api));
2689                            }
2690                            capabilities.supports_floating = true;
2691                        }
2692                    }
2693                }
2694            }
2695        }
2696
2697        // Check params extension
2698        let params_ext_id = c"clap.params";
2699        // SAFETY: extension id is valid static C string.
2700        let params_ptr = unsafe { get_extension(plugin, params_ext_id.as_ptr()) };
2701        capabilities.has_params = !params_ptr.is_null();
2702
2703        // Check state extension
2704        let state_ext_id = c"clap.state";
2705        // SAFETY: extension id is valid static C string.
2706        let state_ptr = unsafe { get_extension(plugin, state_ext_id.as_ptr()) };
2707        capabilities.has_state = !state_ptr.is_null();
2708
2709        // Check audio-ports extension
2710        let audio_ports_ext_id = c"clap.audio-ports";
2711        // SAFETY: extension id is valid static C string.
2712        let audio_ports_ptr = unsafe { get_extension(plugin, audio_ports_ext_id.as_ptr()) };
2713        if !audio_ports_ptr.is_null() {
2714            // SAFETY: CLAP guarantees extension pointer layout for requested extension id.
2715            let audio_ports = unsafe { &*(audio_ports_ptr as *const ClapPluginAudioPorts) };
2716            if let Some(count_fn) = audio_ports.count {
2717                // SAFETY: function pointer comes from plugin extension table.
2718                capabilities.audio_inputs = unsafe { count_fn(plugin, true) } as usize;
2719                // SAFETY: function pointer comes from plugin extension table.
2720                capabilities.audio_outputs = unsafe { count_fn(plugin, false) } as usize;
2721            }
2722        }
2723
2724        // Check note-ports extension
2725        let note_ports_ext_id = c"clap.note-ports";
2726        // SAFETY: extension id is valid static C string.
2727        let note_ports_ptr = unsafe { get_extension(plugin, note_ports_ext_id.as_ptr()) };
2728        if !note_ports_ptr.is_null() {
2729            // SAFETY: CLAP guarantees extension pointer layout for requested extension id.
2730            let note_ports = unsafe { &*(note_ports_ptr as *const ClapPluginNotePorts) };
2731            if let Some(count_fn) = note_ports.count {
2732                // SAFETY: function pointer comes from plugin extension table.
2733                capabilities.midi_inputs = unsafe { count_fn(plugin, true) } as usize;
2734                // SAFETY: function pointer comes from plugin extension table.
2735                capabilities.midi_outputs = unsafe { count_fn(plugin, false) } as usize;
2736            }
2737        }
2738    }
2739
2740    // Clean up plugin instance
2741    if let Some(destroy) = plugin_ref.destroy {
2742        // SAFETY: plugin pointer is valid.
2743        unsafe { destroy(plugin) };
2744    }
2745
2746    Some(capabilities)
2747}
2748
2749fn default_clap_search_roots() -> Vec<PathBuf> {
2750    let mut roots = Vec::new();
2751
2752    #[cfg(target_os = "macos")]
2753    {
2754        paths::push_macos_audio_plugin_roots(&mut roots, "CLAP");
2755    }
2756
2757    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
2758    {
2759        paths::push_unix_plugin_roots(&mut roots, "clap");
2760    }
2761
2762    roots
2763}
2764
2765#[cfg(test)]
2766mod tests {
2767    use super::collect_clap_plugins;
2768    use std::fs;
2769    use std::path::PathBuf;
2770    use std::time::{SystemTime, UNIX_EPOCH};
2771
2772    #[cfg(unix)]
2773    fn make_symlink(src: &PathBuf, dst: &PathBuf) {
2774        std::os::unix::fs::symlink(src, dst).expect("should create symlink");
2775    }
2776
2777    #[cfg(unix)]
2778    #[test]
2779    fn collect_clap_plugins_includes_symlinked_clap_files() {
2780        let nanos = SystemTime::now()
2781            .duration_since(UNIX_EPOCH)
2782            .expect("time should be valid")
2783            .as_nanos();
2784        let root = std::env::temp_dir().join(format!(
2785            "maolan-clap-symlink-test-{}-{nanos}",
2786            std::process::id()
2787        ));
2788        fs::create_dir_all(&root).expect("should create temp dir");
2789
2790        let target_file = root.join("librural_modeler.so");
2791        fs::write(&target_file, b"not a real clap binary").expect("should create target file");
2792        let clap_link = root.join("RuralModeler.clap");
2793        make_symlink(&PathBuf::from("librural_modeler.so"), &clap_link);
2794
2795        let mut out = Vec::new();
2796        collect_clap_plugins(&root, &mut out, false);
2797
2798        assert!(
2799            out.iter()
2800                .any(|info| info.path == clap_link.to_string_lossy()),
2801            "scanner should include symlinked .clap files"
2802        );
2803
2804        fs::remove_dir_all(&root).expect("should remove temp dir");
2805    }
2806}