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