Skip to main content

maolan_engine/plugins/
clap.rs

1use crate::midi::io::MidiEvent;
2use crate::mutex::UnsafeMutex;
3#[cfg(any(
4    target_os = "macos",
5    target_os = "linux",
6    target_os = "freebsd",
7    target_os = "openbsd"
8))]
9use crate::plugins::paths;
10use libloading::Library;
11use serde::{Deserialize, Serialize};
12use std::ffi::{CStr, CString, c_char, c_void};
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU32, Ordering};
15
16#[derive(Clone, Debug, PartialEq)]
17pub struct ClapParameterInfo {
18    pub id: u32,
19    pub name: String,
20    pub module: String,
21    pub min_value: f64,
22    pub max_value: f64,
23    pub default_value: f64,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27pub struct ClapPluginState {
28    pub bytes: Vec<u8>,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct ClapMidiOutputEvent {
33    pub port: usize,
34    pub event: MidiEvent,
35}
36
37#[derive(Clone, Copy, Debug, Default)]
38pub struct ClapTransportInfo {
39    pub transport_sample: usize,
40    pub playing: bool,
41    pub loop_enabled: bool,
42    pub loop_range_samples: Option<(usize, usize)>,
43    pub bpm: f64,
44    pub tsig_num: u16,
45    pub tsig_denom: u16,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct ClapGuiInfo {
50    pub api: String,
51    pub supports_embedded: bool,
52}
53
54#[derive(Clone, Copy, Debug)]
55pub struct ClapParamUpdate {
56    pub param_id: u32,
57    pub value: f64,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
61pub struct ClapPluginInfo {
62    pub name: String,
63    pub path: String,
64    pub capabilities: Option<ClapPluginCapabilities>,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
68pub struct ClapPluginCapabilities {
69    pub has_gui: bool,
70    pub gui_apis: Vec<String>,
71    pub supports_embedded: bool,
72    pub supports_floating: bool,
73    pub has_params: bool,
74    pub has_state: bool,
75    pub audio_inputs: usize,
76    pub audio_outputs: usize,
77    pub midi_inputs: usize,
78    pub midi_outputs: usize,
79}
80
81#[repr(C)]
82#[derive(Clone, Copy)]
83struct ClapVersion {
84    major: u32,
85    minor: u32,
86    revision: u32,
87}
88
89const CLAP_VERSION: ClapVersion = ClapVersion {
90    major: 1,
91    minor: 2,
92    revision: 0,
93};
94
95#[repr(C)]
96struct ClapHost {
97    clap_version: ClapVersion,
98    host_data: *mut c_void,
99    name: *const c_char,
100    vendor: *const c_char,
101    url: *const c_char,
102    version: *const c_char,
103    get_extension: Option<unsafe extern "C" fn(*const ClapHost, *const c_char) -> *const c_void>,
104    request_restart: Option<unsafe extern "C" fn(*const ClapHost)>,
105    request_process: Option<unsafe extern "C" fn(*const ClapHost)>,
106    request_callback: Option<unsafe extern "C" fn(*const ClapHost)>,
107}
108
109#[repr(C)]
110struct ClapPluginEntry {
111    clap_version: ClapVersion,
112    init: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
113    deinit: Option<unsafe extern "C" fn()>,
114    get_factory: Option<unsafe extern "C" fn(*const c_char) -> *const c_void>,
115}
116
117#[repr(C)]
118struct ClapPluginFactory {
119    get_plugin_count: Option<unsafe extern "C" fn(*const ClapPluginFactory) -> u32>,
120    get_plugin_descriptor:
121        Option<unsafe extern "C" fn(*const ClapPluginFactory, u32) -> *const ClapPluginDescriptor>,
122    create_plugin: Option<
123        unsafe extern "C" fn(
124            *const ClapPluginFactory,
125            *const ClapHost,
126            *const c_char,
127        ) -> *const ClapPlugin,
128    >,
129}
130
131#[repr(C)]
132struct ClapPluginDescriptor {
133    clap_version: ClapVersion,
134    id: *const c_char,
135    name: *const c_char,
136    vendor: *const c_char,
137    url: *const c_char,
138    manual_url: *const c_char,
139    support_url: *const c_char,
140    version: *const c_char,
141    description: *const c_char,
142    features: *const *const c_char,
143}
144
145#[repr(C)]
146struct ClapPlugin {
147    desc: *const ClapPluginDescriptor,
148    plugin_data: *mut c_void,
149    init: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
150    destroy: Option<unsafe extern "C" fn(*const ClapPlugin)>,
151    activate: Option<unsafe extern "C" fn(*const ClapPlugin, f64, u32, u32) -> bool>,
152    deactivate: Option<unsafe extern "C" fn(*const ClapPlugin)>,
153    start_processing: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
154    stop_processing: Option<unsafe extern "C" fn(*const ClapPlugin)>,
155    reset: Option<unsafe extern "C" fn(*const ClapPlugin)>,
156    process: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapProcess) -> i32>,
157    get_extension: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char) -> *const c_void>,
158    on_main_thread: Option<unsafe extern "C" fn(*const ClapPlugin)>,
159}
160
161#[repr(C)]
162struct ClapInputEvents {
163    ctx: *const c_void,
164    size: Option<unsafe extern "C" fn(*const ClapInputEvents) -> u32>,
165    get: Option<unsafe extern "C" fn(*const ClapInputEvents, u32) -> *const ClapEventHeader>,
166}
167
168#[repr(C)]
169struct ClapOutputEvents {
170    ctx: *mut c_void,
171    try_push: Option<unsafe extern "C" fn(*const ClapOutputEvents, *const ClapEventHeader) -> bool>,
172}
173
174#[repr(C)]
175struct ClapEventHeader {
176    size: u32,
177    time: u32,
178    space_id: u16,
179    type_: u16,
180    flags: u32,
181}
182
183#[repr(C)]
184struct ClapAudioPortInfoRaw {
185    id: u32,
186    name: [c_char; 256],
187    flags: u32,
188    channel_count: u32,
189    port_type: *const c_char,
190    in_place_pair: u32,
191}
192
193#[repr(C)]
194struct ClapPluginAudioPorts {
195    count: Option<unsafe extern "C" fn(*const ClapPlugin, bool) -> u32>,
196    get: Option<
197        unsafe extern "C" fn(*const ClapPlugin, u32, bool, *mut ClapAudioPortInfoRaw) -> bool,
198    >,
199}
200
201#[repr(C)]
202struct ClapNotePortInfoRaw {
203    id: u16,
204    supported_dialects: u32,
205    preferred_dialect: u32,
206    name: [c_char; 256],
207}
208
209#[repr(C)]
210struct ClapPluginNotePorts {
211    count: Option<unsafe extern "C" fn(*const ClapPlugin, bool) -> u32>,
212    get: Option<
213        unsafe extern "C" fn(*const ClapPlugin, u32, bool, *mut ClapNotePortInfoRaw) -> bool,
214    >,
215}
216
217#[repr(C)]
218struct ClapPluginGui {
219    is_api_supported: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char, bool) -> bool>,
220    get_preferred_api:
221        Option<unsafe extern "C" fn(*const ClapPlugin, *mut *const c_char, *mut bool) -> bool>,
222    create: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char, bool) -> bool>,
223    destroy: Option<unsafe extern "C" fn(*const ClapPlugin)>,
224    set_scale: Option<unsafe extern "C" fn(*const ClapPlugin, f64) -> bool>,
225    get_size: Option<unsafe extern "C" fn(*const ClapPlugin, *mut u32, *mut u32) -> bool>,
226    can_resize: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
227    get_resize_hints: Option<unsafe extern "C" fn(*const ClapPlugin, *mut c_void) -> bool>,
228    adjust_size: Option<unsafe extern "C" fn(*const ClapPlugin, *mut u32, *mut u32) -> bool>,
229    set_size: Option<unsafe extern "C" fn(*const ClapPlugin, u32, u32) -> bool>,
230    set_parent: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapWindow) -> bool>,
231    set_transient: Option<unsafe extern "C" fn(*const ClapPlugin, *const ClapWindow) -> bool>,
232    suggest_title: Option<unsafe extern "C" fn(*const ClapPlugin, *const c_char)>,
233    show: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
234    hide: Option<unsafe extern "C" fn(*const ClapPlugin) -> bool>,
235}
236
237#[repr(C)]
238union ClapWindowHandle {
239    x11: usize,
240    native: *mut c_void,
241    cocoa: *mut c_void,
242}
243
244#[repr(C)]
245struct ClapWindow {
246    api: *const c_char,
247    handle: ClapWindowHandle,
248}
249
250#[repr(C)]
251struct ClapHostThreadCheck {
252    is_main_thread: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
253    is_audio_thread: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
254}
255
256#[repr(C)]
257struct ClapHostLatency {
258    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
259}
260
261#[repr(C)]
262struct ClapHostTail {
263    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
264}
265
266#[repr(C)]
267struct ClapHostTimerSupport {
268    register_timer: Option<unsafe extern "C" fn(*const ClapHost, u32, *mut u32) -> bool>,
269    unregister_timer: Option<unsafe extern "C" fn(*const ClapHost, u32) -> bool>,
270}
271
272#[repr(C)]
273struct ClapHostGui {
274    resize_hints_changed: Option<unsafe extern "C" fn(*const ClapHost)>,
275    request_resize: Option<unsafe extern "C" fn(*const ClapHost, u32, u32) -> bool>,
276    request_show: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
277    request_hide: Option<unsafe extern "C" fn(*const ClapHost) -> bool>,
278    closed: Option<unsafe extern "C" fn(*const ClapHost, bool)>,
279}
280
281#[repr(C)]
282struct ClapHostParams {
283    rescan: Option<unsafe extern "C" fn(*const ClapHost, u32)>,
284    clear: Option<unsafe extern "C" fn(*const ClapHost, u32, u32)>,
285    request_flush: Option<unsafe extern "C" fn(*const ClapHost)>,
286}
287
288#[repr(C)]
289struct ClapHostState {
290    mark_dirty: Option<unsafe extern "C" fn(*const ClapHost)>,
291}
292
293#[repr(C)]
294struct ClapHostAudioPorts {
295    is_rescan_flag_supported: Option<unsafe extern "C" fn(*const ClapHost, flag: u32) -> bool>,
296    rescan: Option<unsafe extern "C" fn(*const ClapHost, flags: u32)>,
297}
298
299#[repr(C)]
300struct ClapHostNotePorts {
301    supported_dialects: Option<unsafe extern "C" fn(*const ClapHost) -> u32>,
302    rescan: Option<unsafe extern "C" fn(*const ClapHost, flags: u32)>,
303}
304
305#[repr(C)]
306struct ClapHostNoteName {
307    changed: Option<unsafe extern "C" fn(*const ClapHost)>,
308}
309
310#[repr(C)]
311struct ClapAudioBuffer {
312    data32: *mut *mut f32,
313    data64: *mut *mut f64,
314    channel_count: u32,
315    latency: u32,
316    constant_mask: u64,
317}
318
319#[repr(C)]
320struct ClapProcess {
321    steady_time: i64,
322    frames_count: u32,
323    transport: *const c_void,
324    audio_inputs: *mut ClapAudioBuffer,
325    audio_outputs: *mut ClapAudioBuffer,
326    audio_inputs_count: u32,
327    audio_outputs_count: u32,
328    in_events: *const ClapInputEvents,
329    out_events: *mut ClapOutputEvents,
330}
331
332#[derive(Default, Clone, Copy)]
333struct HostCallbackFlags {
334    restart: bool,
335    process: bool,
336    callback: bool,
337}
338
339#[derive(Clone, Copy)]
340struct HostTimer {
341    id: u32,
342}
343
344struct HostRuntimeState {
345    callback_flags: UnsafeMutex<HostCallbackFlags>,
346    timers: UnsafeMutex<Vec<HostTimer>>,
347    ui_should_close: AtomicU32,
348    ui_active: AtomicU32,
349    param_flush_requested: AtomicU32,
350    state_dirty_requested: AtomicU32,
351    note_names_dirty: AtomicU32,
352    audio_ports_rescan_requested: AtomicU32,
353    note_ports_rescan_requested: AtomicU32,
354}
355
356static HOST_THREAD_CHECK_EXT: ClapHostThreadCheck = ClapHostThreadCheck {
357    is_main_thread: Some(host_is_main_thread),
358    is_audio_thread: Some(host_is_audio_thread),
359};
360static HOST_LATENCY_EXT: ClapHostLatency = ClapHostLatency {
361    changed: Some(host_latency_changed),
362};
363static HOST_TAIL_EXT: ClapHostTail = ClapHostTail {
364    changed: Some(host_tail_changed),
365};
366static HOST_TIMER_EXT: ClapHostTimerSupport = ClapHostTimerSupport {
367    register_timer: Some(host_timer_register),
368    unregister_timer: Some(host_timer_unregister),
369};
370static HOST_GUI_EXT: ClapHostGui = ClapHostGui {
371    resize_hints_changed: Some(host_gui_resize_hints_changed),
372    request_resize: Some(host_gui_request_resize),
373    request_show: Some(host_gui_request_show),
374    request_hide: Some(host_gui_request_hide),
375    closed: Some(host_gui_closed),
376};
377static HOST_PARAMS_EXT: ClapHostParams = ClapHostParams {
378    rescan: Some(host_params_rescan),
379    clear: Some(host_params_clear),
380    request_flush: Some(host_params_request_flush),
381};
382static HOST_STATE_EXT: ClapHostState = ClapHostState {
383    mark_dirty: Some(host_state_mark_dirty),
384};
385static HOST_NOTE_NAME_EXT: ClapHostNoteName = ClapHostNoteName {
386    changed: Some(host_note_name_changed),
387};
388static HOST_AUDIO_PORTS_EXT: ClapHostAudioPorts = ClapHostAudioPorts {
389    is_rescan_flag_supported: Some(host_audio_ports_is_rescan_flag_supported),
390    rescan: Some(host_audio_ports_rescan),
391};
392static HOST_NOTE_PORTS_EXT: ClapHostNotePorts = ClapHostNotePorts {
393    supported_dialects: Some(host_note_ports_supported_dialects),
394    rescan: Some(host_note_ports_rescan),
395};
396static NEXT_TIMER_ID: AtomicU32 = AtomicU32::new(1);
397
398fn host_runtime_state(host: *const ClapHost) -> Option<&'static HostRuntimeState> {
399    if host.is_null() {
400        return None;
401    }
402    let state_ptr = unsafe { (*host).host_data as *const HostRuntimeState };
403    if state_ptr.is_null() {
404        return None;
405    }
406    Some(unsafe { &*state_ptr })
407}
408
409unsafe extern "C" fn host_get_extension(
410    _host: *const ClapHost,
411    _extension_id: *const c_char,
412) -> *const c_void {
413    if _extension_id.is_null() {
414        return std::ptr::null();
415    }
416
417    let id = unsafe { CStr::from_ptr(_extension_id) }.to_string_lossy();
418    match id.as_ref() {
419        "clap.host.thread-check" => {
420            (&HOST_THREAD_CHECK_EXT as *const ClapHostThreadCheck).cast::<c_void>()
421        }
422        "clap.host.latency" => (&HOST_LATENCY_EXT as *const ClapHostLatency).cast::<c_void>(),
423        "clap.host.tail" => (&HOST_TAIL_EXT as *const ClapHostTail).cast::<c_void>(),
424        "clap.host.timer-support" => {
425            (&HOST_TIMER_EXT as *const ClapHostTimerSupport).cast::<c_void>()
426        }
427        "clap.host.gui" => host_runtime_state(_host)
428            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
429            .map(|_| (&HOST_GUI_EXT as *const ClapHostGui).cast::<c_void>())
430            .unwrap_or(std::ptr::null()),
431        "clap.host.params" => host_runtime_state(_host)
432            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
433            .map(|_| (&HOST_PARAMS_EXT as *const ClapHostParams).cast::<c_void>())
434            .unwrap_or(std::ptr::null()),
435        "clap.host.state" => host_runtime_state(_host)
436            .filter(|state| state.ui_active.load(Ordering::Acquire) != 0)
437            .map(|_| (&HOST_STATE_EXT as *const ClapHostState).cast::<c_void>())
438            .unwrap_or(std::ptr::null()),
439        "clap.host.note-name" => (&HOST_NOTE_NAME_EXT as *const ClapHostNoteName).cast::<c_void>(),
440        "clap.host.audio-ports" => {
441            (&HOST_AUDIO_PORTS_EXT as *const ClapHostAudioPorts).cast::<c_void>()
442        }
443        "clap.host.note-ports" => {
444            (&HOST_NOTE_PORTS_EXT as *const ClapHostNotePorts).cast::<c_void>()
445        }
446        _ => std::ptr::null(),
447    }
448}
449
450unsafe extern "C" fn host_request_process(_host: *const ClapHost) {
451    if let Some(state) = host_runtime_state(_host) {
452        state.callback_flags.lock().process = true;
453    }
454}
455
456unsafe extern "C" fn host_request_callback(_host: *const ClapHost) {
457    if let Some(state) = host_runtime_state(_host) {
458        state.callback_flags.lock().callback = true;
459    }
460}
461
462unsafe extern "C" fn host_request_restart(_host: *const ClapHost) {
463    if let Some(state) = host_runtime_state(_host) {
464        state.callback_flags.lock().restart = true;
465    }
466}
467
468unsafe extern "C" fn host_audio_ports_is_rescan_flag_supported(
469    _host: *const ClapHost,
470    _flag: u32,
471) -> bool {
472    true
473}
474
475unsafe extern "C" fn host_audio_ports_rescan(_host: *const ClapHost, _flags: u32) {
476    if let Some(state) = host_runtime_state(_host) {
477        state
478            .audio_ports_rescan_requested
479            .store(1, Ordering::Release);
480    }
481}
482
483unsafe extern "C" fn host_note_ports_rescan(_host: *const ClapHost, _flags: u32) {
484    if let Some(state) = host_runtime_state(_host) {
485        state
486            .note_ports_rescan_requested
487            .store(1, Ordering::Release);
488    }
489}
490
491unsafe extern "C" fn host_note_ports_supported_dialects(_host: *const ClapHost) -> u32 {
492    let _ = _host;
493    0x1F
494}
495
496unsafe extern "C" fn host_note_name_changed(_host: *const ClapHost) {
497    if let Some(state) = host_runtime_state(_host) {
498        state.note_names_dirty.store(1, Ordering::Release);
499    }
500}
501
502unsafe extern "C" fn host_is_main_thread(_host: *const ClapHost) -> bool {
503    true
504}
505
506unsafe extern "C" fn host_is_audio_thread(_host: *const ClapHost) -> bool {
507    false
508}
509
510unsafe extern "C" fn host_latency_changed(_host: *const ClapHost) {}
511
512unsafe extern "C" fn host_tail_changed(_host: *const ClapHost) {}
513
514unsafe extern "C" fn host_timer_register(
515    _host: *const ClapHost,
516    _period_ms: u32,
517    timer_id: *mut u32,
518) -> bool {
519    if timer_id.is_null() {
520        return false;
521    }
522    let id = NEXT_TIMER_ID.fetch_add(1, Ordering::Relaxed);
523    if let Some(state) = host_runtime_state(_host) {
524        state.timers.lock().push(HostTimer { id });
525    }
526
527    unsafe {
528        *timer_id = id;
529    }
530    true
531}
532
533unsafe extern "C" fn host_timer_unregister(_host: *const ClapHost, _timer_id: u32) -> bool {
534    if let Some(state) = host_runtime_state(_host) {
535        state.timers.lock().retain(|timer| timer.id != _timer_id);
536    }
537    true
538}
539
540unsafe extern "C" fn host_gui_resize_hints_changed(_host: *const ClapHost) {}
541
542unsafe extern "C" fn host_gui_request_resize(
543    _host: *const ClapHost,
544    _width: u32,
545    _height: u32,
546) -> bool {
547    true
548}
549
550unsafe extern "C" fn host_gui_request_show(_host: *const ClapHost) -> bool {
551    true
552}
553
554unsafe extern "C" fn host_gui_request_hide(_host: *const ClapHost) -> bool {
555    if let Some(state) = host_runtime_state(_host) {
556        if state.ui_active.load(Ordering::Acquire) != 0 {
557            state.ui_should_close.store(1, Ordering::Release);
558        }
559        true
560    } else {
561        false
562    }
563}
564
565unsafe extern "C" fn host_gui_closed(_host: *const ClapHost, _was_destroyed: bool) {
566    if let Some(state) = host_runtime_state(_host)
567        && state.ui_active.load(Ordering::Acquire) != 0
568    {
569        state.ui_should_close.store(1, Ordering::Release);
570    }
571}
572
573unsafe extern "C" fn host_params_rescan(_host: *const ClapHost, _flags: u32) {}
574
575unsafe extern "C" fn host_params_clear(_host: *const ClapHost, _param_id: u32, _flags: u32) {}
576
577unsafe extern "C" fn host_params_request_flush(_host: *const ClapHost) {
578    if let Some(state) = host_runtime_state(_host) {
579        state.param_flush_requested.store(1, Ordering::Release);
580        state.callback_flags.lock().callback = true;
581    }
582}
583
584unsafe extern "C" fn host_state_mark_dirty(_host: *const ClapHost) {
585    if let Some(state) = host_runtime_state(_host) {
586        state.state_dirty_requested.store(1, Ordering::Release);
587        state.callback_flags.lock().callback = true;
588    }
589}
590
591pub fn list_plugins() -> Vec<ClapPluginInfo> {
592    list_plugins_with_capabilities(false)
593}
594
595pub fn is_supported_clap_binary(path: &Path) -> bool {
596    path.extension()
597        .is_some_and(|ext| ext.eq_ignore_ascii_case("clap"))
598}
599
600pub fn list_plugins_with_capabilities(scan_capabilities: bool) -> Vec<ClapPluginInfo> {
601    let mut roots = default_clap_search_roots();
602
603    if let Ok(extra) = std::env::var("CLAP_PATH") {
604        for p in std::env::split_paths(&extra) {
605            if !p.as_os_str().is_empty() {
606                roots.push(p);
607            }
608        }
609    }
610
611    let mut out = Vec::new();
612    for root in roots {
613        collect_clap_plugins(&root, &mut out, scan_capabilities);
614    }
615
616    out.sort_by_key(|a| a.name.to_lowercase());
617    out.dedup_by(|a, b| a.path.eq_ignore_ascii_case(&b.path));
618    out
619}
620
621fn collect_clap_plugins(root: &Path, out: &mut Vec<ClapPluginInfo>, scan_capabilities: bool) {
622    let Ok(entries) = std::fs::read_dir(root) else {
623        return;
624    };
625    for entry in entries.flatten() {
626        let path = entry.path();
627        let Ok(ft) = entry.file_type() else {
628            continue;
629        };
630        if ft.is_dir() {
631            if path
632                .file_name()
633                .and_then(|name| name.to_str())
634                .is_some_and(|name| {
635                    matches!(
636                        name,
637                        "deps" | "build" | "incremental" | ".fingerprint" | "examples"
638                    )
639                })
640            {
641                continue;
642            }
643            collect_clap_plugins(&path, out, scan_capabilities);
644            continue;
645        }
646
647        if is_supported_clap_binary(&path) {
648            let infos = scan_bundle_descriptors(&path, scan_capabilities);
649            if infos.is_empty() {
650                let name = path
651                    .file_stem()
652                    .map(|s| s.to_string_lossy().to_string())
653                    .unwrap_or_else(|| path.to_string_lossy().to_string());
654                out.push(ClapPluginInfo {
655                    name,
656                    path: path.to_string_lossy().to_string(),
657                    capabilities: None,
658                });
659            } else {
660                out.extend(infos);
661            }
662        }
663    }
664}
665
666fn scan_bundle_descriptors(path: &Path, scan_capabilities: bool) -> Vec<ClapPluginInfo> {
667    let path_str = path.to_string_lossy().to_string();
668    let factory_id = c"clap.plugin-factory";
669    let mut host_runtime_state = Box::new(HostRuntimeState {
670        callback_flags: UnsafeMutex::new(HostCallbackFlags::default()),
671        timers: UnsafeMutex::new(Vec::new()),
672        ui_should_close: AtomicU32::new(0),
673        ui_active: AtomicU32::new(0),
674        param_flush_requested: AtomicU32::new(0),
675        state_dirty_requested: AtomicU32::new(0),
676        note_names_dirty: AtomicU32::new(0),
677        audio_ports_rescan_requested: AtomicU32::new(0),
678        note_ports_rescan_requested: AtomicU32::new(0),
679    });
680    let host_runtime = ClapHost {
681        clap_version: CLAP_VERSION,
682        host_data: (&mut *host_runtime_state as *mut HostRuntimeState).cast::<c_void>(),
683        name: c"Maolan".as_ptr(),
684        vendor: c"Maolan".as_ptr(),
685        url: c"https://example.invalid".as_ptr(),
686        version: c"0.0.1".as_ptr(),
687        get_extension: Some(host_get_extension),
688        request_restart: Some(host_request_restart),
689        request_process: Some(host_request_process),
690        request_callback: Some(host_request_callback),
691    };
692
693    let library = match unsafe { Library::new(path) } {
694        Ok(lib) => lib,
695        Err(_) => return Vec::new(),
696    };
697
698    let entry_ptr = unsafe {
699        match library.get::<*const ClapPluginEntry>(b"clap_entry\0") {
700            Ok(sym) => *sym,
701            Err(_) => return Vec::new(),
702        }
703    };
704    if entry_ptr.is_null() {
705        return Vec::new();
706    }
707
708    let entry = unsafe { &*entry_ptr };
709    let Some(init) = entry.init else {
710        return Vec::new();
711    };
712    let host_ptr = &host_runtime;
713
714    if unsafe { !init(host_ptr) } {
715        return Vec::new();
716    }
717    let mut out = Vec::new();
718    if let Some(get_factory) = entry.get_factory {
719        let factory = unsafe { get_factory(factory_id.as_ptr()) } as *const ClapPluginFactory;
720        if !factory.is_null() {
721            let factory_ref = unsafe { &*factory };
722            if let (Some(get_count), Some(get_desc)) = (
723                factory_ref.get_plugin_count,
724                factory_ref.get_plugin_descriptor,
725            ) {
726                let count = unsafe { get_count(factory) };
727                for i in 0..count {
728                    let desc = unsafe { get_desc(factory, i) };
729                    if desc.is_null() {
730                        continue;
731                    }
732
733                    let desc = unsafe { &*desc };
734                    if desc.id.is_null() || desc.name.is_null() {
735                        continue;
736                    }
737
738                    let id = unsafe { CStr::from_ptr(desc.id) }
739                        .to_string_lossy()
740                        .to_string();
741
742                    let name = unsafe { CStr::from_ptr(desc.name) }
743                        .to_string_lossy()
744                        .to_string();
745
746                    let capabilities = if scan_capabilities {
747                        scan_plugin_capabilities(factory_ref, factory, &host_runtime, &id)
748                    } else {
749                        None
750                    };
751
752                    out.push(ClapPluginInfo {
753                        name,
754                        path: format!("{path_str}::{id}"),
755                        capabilities,
756                    });
757                }
758            }
759        }
760    }
761
762    if let Some(deinit) = entry.deinit {
763        unsafe { deinit() };
764    }
765    out
766}
767
768fn scan_plugin_capabilities(
769    factory: &ClapPluginFactory,
770    factory_ptr: *const ClapPluginFactory,
771    host: &ClapHost,
772    plugin_id: &str,
773) -> Option<ClapPluginCapabilities> {
774    let create = factory.create_plugin?;
775
776    let id_cstring = CString::new(plugin_id).ok()?;
777
778    let plugin = unsafe { create(factory_ptr, host, id_cstring.as_ptr()) };
779    if plugin.is_null() {
780        return None;
781    }
782
783    let plugin_ref = unsafe { &*plugin };
784    let plugin_init = plugin_ref.init?;
785
786    if unsafe { !plugin_init(plugin) } {
787        return None;
788    }
789
790    let mut capabilities = ClapPluginCapabilities {
791        has_gui: false,
792        gui_apis: Vec::new(),
793        supports_embedded: false,
794        supports_floating: false,
795        has_params: false,
796        has_state: false,
797        audio_inputs: 0,
798        audio_outputs: 0,
799        midi_inputs: 0,
800        midi_outputs: 0,
801    };
802
803    if let Some(get_extension) = plugin_ref.get_extension {
804        let gui_ext_id = c"clap.gui";
805
806        let gui_ptr = unsafe { get_extension(plugin, gui_ext_id.as_ptr()) };
807        if !gui_ptr.is_null() {
808            capabilities.has_gui = true;
809
810            let gui = unsafe { &*(gui_ptr as *const ClapPluginGui) };
811
812            if let Some(is_api_supported) = gui.is_api_supported {
813                for api in ["x11", "cocoa"] {
814                    if let Ok(api_cstr) = CString::new(api) {
815                        if unsafe { is_api_supported(plugin, api_cstr.as_ptr(), false) } {
816                            capabilities.gui_apis.push(format!("{} (embedded)", api));
817                            capabilities.supports_embedded = true;
818                        }
819
820                        if unsafe { is_api_supported(plugin, api_cstr.as_ptr(), true) } {
821                            if !capabilities.supports_embedded {
822                                capabilities.gui_apis.push(format!("{} (floating)", api));
823                            }
824                            capabilities.supports_floating = true;
825                        }
826                    }
827                }
828            }
829        }
830
831        let params_ext_id = c"clap.params";
832
833        let params_ptr = unsafe { get_extension(plugin, params_ext_id.as_ptr()) };
834        capabilities.has_params = !params_ptr.is_null();
835
836        let state_ext_id = c"clap.state";
837
838        let state_ptr = unsafe { get_extension(plugin, state_ext_id.as_ptr()) };
839        capabilities.has_state = !state_ptr.is_null();
840
841        let audio_ports_ext_id = c"clap.audio-ports";
842
843        let audio_ports_ptr = unsafe { get_extension(plugin, audio_ports_ext_id.as_ptr()) };
844        if !audio_ports_ptr.is_null() {
845            let audio_ports = unsafe { &*(audio_ports_ptr as *const ClapPluginAudioPorts) };
846            if let Some(count_fn) = audio_ports.count {
847                capabilities.audio_inputs = unsafe { count_fn(plugin, true) } as usize;
848
849                capabilities.audio_outputs = unsafe { count_fn(plugin, false) } as usize;
850            }
851        }
852
853        let note_ports_ext_id = c"clap.note-ports";
854
855        let note_ports_ptr = unsafe { get_extension(plugin, note_ports_ext_id.as_ptr()) };
856        if !note_ports_ptr.is_null() {
857            let note_ports = unsafe { &*(note_ports_ptr as *const ClapPluginNotePorts) };
858            if let Some(count_fn) = note_ports.count {
859                capabilities.midi_inputs = unsafe { count_fn(plugin, true) } as usize;
860
861                capabilities.midi_outputs = unsafe { count_fn(plugin, false) } as usize;
862            }
863        }
864    }
865
866    if let Some(destroy) = plugin_ref.destroy {
867        unsafe { destroy(plugin) };
868    }
869
870    Some(capabilities)
871}
872
873fn default_clap_search_roots() -> Vec<PathBuf> {
874    #[cfg(target_os = "macos")]
875    {
876        let mut roots = Vec::new();
877        paths::push_macos_audio_plugin_roots(&mut roots, "CLAP");
878        roots
879    }
880    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
881    {
882        let mut roots = Vec::new();
883        paths::push_unix_plugin_roots(&mut roots, "clap");
884        roots
885    }
886    #[cfg(not(any(
887        target_os = "macos",
888        target_os = "linux",
889        target_os = "freebsd",
890        target_os = "openbsd"
891    )))]
892    {
893        Vec::new()
894    }
895}
896
897#[cfg(test)]
898mod tests {
899    #[cfg(unix)]
900    use super::collect_clap_plugins;
901    #[cfg(unix)]
902    use std::fs;
903    #[cfg(unix)]
904    use std::path::PathBuf;
905    #[cfg(unix)]
906    use std::time::{SystemTime, UNIX_EPOCH};
907
908    #[cfg(unix)]
909    fn make_symlink(src: &PathBuf, dst: &PathBuf) {
910        std::os::unix::fs::symlink(src, dst).expect("should create symlink");
911    }
912
913    #[cfg(unix)]
914    #[test]
915    fn collect_clap_plugins_includes_symlinked_clap_files() {
916        let nanos = SystemTime::now()
917            .duration_since(UNIX_EPOCH)
918            .expect("time should be valid")
919            .as_nanos();
920        let root = std::env::temp_dir().join(format!(
921            "maolan-clap-symlink-test-{}-{nanos}",
922            std::process::id()
923        ));
924        fs::create_dir_all(&root).expect("should create temp dir");
925
926        let target_file = root.join("librural_modeler.so");
927        fs::write(&target_file, b"not a real clap binary").expect("should create target file");
928        let clap_link = root.join("RuralModeler.clap");
929        make_symlink(&PathBuf::from("librural_modeler.so"), &clap_link);
930
931        let mut out = Vec::new();
932        collect_clap_plugins(&root, &mut out, false);
933
934        assert!(
935            out.iter()
936                .any(|info| info.path == clap_link.to_string_lossy()),
937            "scanner should include symlinked .clap files"
938        );
939
940        fs::remove_dir_all(&root).expect("should remove temp dir");
941    }
942}