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}