Skip to main content

maolan_engine/plugins/
ipc.rs

1//! Shared IPC helpers for out-of-process plugin processors.
2
3use crate::audio::io::AudioIO;
4use crate::mutex::UnsafeMutex;
5use maolan_plugin_protocol::events::EventPair;
6use maolan_plugin_protocol::protocol::*;
7use maolan_plugin_protocol::shm::ShmMapping;
8use std::path::{Path, PathBuf};
9use std::process::{Child, Command, Stdio};
10use std::sync::Arc;
11use std::sync::atomic::Ordering;
12use std::time::{Duration, Instant};
13
14/// Arguments for spawning a plugin host subprocess.
15pub struct HostSpawnArgs<'a> {
16    pub host_binary: &'a Path,
17    pub format: &'a str,
18    pub plugin_spec: &'a str,
19    pub instance_id: &'a str,
20    pub extra_args: &'a [&'a str],
21}
22
23/// Spawn the unified `maolan-plugin-host` binary and set up SHM + event pipes.
24pub fn spawn_host(args: HostSpawnArgs) -> Result<(Child, ShmMapping, EventPair, String), String> {
25    let pid = std::process::id();
26    let shm_name = format!("/maolan-{pid}-{}", args.instance_id);
27
28    let mapping = ShmMapping::create(&shm_name, SHM_SIZE)
29        .map_err(|e| format!("failed to create shared memory: {e}"))?;
30    unsafe {
31        init_shm_layout(mapping.as_ptr(), mapping.size());
32    }
33
34    let mut events = EventPair::new().map_err(|e| format!("failed to create event pipes: {e}"))?;
35
36    let mut cmd = Command::new(args.host_binary);
37    cmd.arg(args.format)
38        .arg(args.plugin_spec)
39        .arg(&shm_name)
40        .arg(args.instance_id)
41        .stdin(Stdio::null())
42        .stdout(Stdio::null())
43        .stderr(Stdio::inherit());
44
45    for arg in args.extra_args {
46        cmd.arg(arg);
47    }
48
49    #[cfg(unix)]
50    {
51        cmd.arg(events.host_read_fd().to_string())
52            .arg(events.host_write_fd().to_string());
53    }
54    #[cfg(windows)]
55    {
56        cmd.arg(events.daw_to_host_name())
57            .arg(events.host_to_daw_name());
58    }
59
60    if std::env::args().any(|a| a == "--debug") {
61        cmd.arg("--debug");
62    }
63
64    let child = cmd
65        .spawn()
66        .map_err(|e| format!("failed to spawn {} host: {e}", args.format))?;
67
68    events.close_daw_unused();
69
70    Ok((child, mapping, events, shm_name))
71}
72
73/// Poll the SHM ready flag until it becomes non-zero or `timeout` elapses.
74pub fn wait_for_ready(header: &ShmHeader, timeout: Duration) -> bool {
75    let start = Instant::now();
76    while start.elapsed() < timeout {
77        if header.ready.load(Ordering::Acquire) != 0 {
78            return true;
79        }
80        std::thread::sleep(Duration::from_millis(5));
81    }
82    false
83}
84
85/// Copy input buffers to output buffers when the plugin host is bypassed or crashed.
86pub fn bypass_copy_inputs_to_outputs(inputs: &[Arc<AudioIO>], outputs: &[Arc<AudioIO>]) {
87    for (input, output) in inputs.iter().zip(outputs.iter()) {
88        let src = input.buffer.lock();
89        let dst = output.buffer.lock();
90        dst.fill(0.0);
91        for (d, s) in dst.iter_mut().zip(src.iter()) {
92            *d = *s;
93        }
94        *output.finished.lock() = true;
95    }
96    for output in outputs.iter().skip(inputs.len()) {
97        let dst = output.buffer.lock();
98        dst.fill(0.0);
99        *output.finished.lock() = true;
100    }
101}
102
103/// Shared shutdown logic for the `Drop` impl of all OOP processors.
104pub fn drop_host(
105    mapping: &Option<ShmMapping>,
106    events: &Option<EventPair>,
107    child: &UnsafeMutex<Option<Child>>,
108    shm_name: &str,
109) {
110    if let Some(mapping) = mapping
111        && let Some(events) = events
112    {
113        let header = unsafe { header_mut(mapping.as_ptr()) };
114        header.shutdown_request.store(1, Ordering::Release);
115        let _ = events.signal_host();
116    }
117    let mut child_opt = child.lock().take();
118    if let Some(mut child) = child_opt.take() {
119        let start = Instant::now();
120        while start.elapsed() < Duration::from_secs(2) {
121            if child.try_wait().map(|s| s.is_some()).unwrap_or(true) {
122                break;
123            }
124            std::thread::sleep(Duration::from_millis(10));
125        }
126        if child.try_wait().map(|s| s.is_none()).unwrap_or(false) {
127            let _ = child.kill();
128        }
129    }
130    let _ = ShmMapping::unlink(shm_name);
131}
132
133/// Locate the `maolan-plugin-host` binary at runtime.
134///
135/// Search order:
136/// 1. Same directory as the current executable.
137/// 2. Workspace `target/debug` or `target/release` (development).
138/// 3. `PATH` environment variable.
139pub fn find_plugin_host_binary() -> Option<PathBuf> {
140    let exe_dir = std::env::current_exe()
141        .ok()
142        .and_then(|p| p.parent().map(PathBuf::from));
143
144    // 1. Same directory as current executable.
145    if let Some(ref dir) = exe_dir {
146        let candidate = dir.join("maolan-plugin-host");
147        if candidate.exists() {
148            return Some(candidate);
149        }
150    }
151
152    // 2. Development workspace paths.
153    if let Ok(manifest) = std::env::var("CARGO_MANIFEST_DIR") {
154        let engine_root = Path::new(&manifest);
155        for profile in ["debug", "release"] {
156            let candidate = engine_root
157                .parent()
158                .unwrap_or(Path::new(""))
159                .join("daw")
160                .join("target")
161                .join(profile)
162                .join("maolan-plugin-host");
163            if candidate.exists() {
164                return Some(candidate);
165            }
166        }
167    }
168
169    // 3. PATH.
170    if let Ok(path_var) = std::env::var("PATH") {
171        for dir in path_var.split(':') {
172            let candidate = Path::new(dir).join("maolan-plugin-host");
173            if candidate.exists() {
174                return Some(candidate);
175            }
176        }
177    }
178
179    None
180}
181
182/// Copy input AudioIO buffers to shared memory (bus 0).
183///
184/// # Safety
185/// `ptr` must be a valid pointer to the start of the plugin-host SHM region.
186pub unsafe fn copy_inputs_to_shm(inputs: &[Arc<AudioIO>], ptr: *mut u8, frames: usize) {
187    for (ch, input) in inputs.iter().enumerate() {
188        let src = input.buffer.lock();
189        let dst = unsafe { audio_channel_ptr(ptr, ch, 0) };
190        let len = frames.min(src.len());
191        unsafe {
192            std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
193        }
194    }
195}
196
197/// Copy output shared memory (bus 1) back to AudioIO buffers.
198///
199/// # Safety
200/// `ptr` must be a valid pointer to the start of the plugin-host SHM region.
201pub unsafe fn copy_outputs_from_shm(outputs: &[Arc<AudioIO>], ptr: *mut u8, frames: usize) {
202    for (ch, output) in outputs.iter().enumerate() {
203        let dst = output.buffer.lock();
204        let src = unsafe { audio_channel_ptr(ptr, ch, 1) };
205        let len = frames.min(dst.len());
206        unsafe {
207            std::ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), len);
208        }
209        *output.finished.lock() = true;
210    }
211}
212
213/// Set the standard SHM header fields for a processing block.
214///
215/// # Safety
216/// `ptr` must be a valid pointer to the start of the plugin-host SHM region.
217pub unsafe fn configure_shm_header(ptr: *mut u8, frames: usize, num_in: usize, num_out: usize) {
218    unsafe {
219        let h = header_mut(ptr);
220        h.block_size.store(frames as u32, Ordering::Release);
221        h.num_input_channels.store(num_in as u32, Ordering::Release);
222        h.num_output_channels
223            .store(num_out as u32, Ordering::Release);
224    }
225}
226
227/// Generate `UnsafeMutex<Processor>` forwarding methods that are identical
228/// across all out-of-process plugin formats (CLAP, VST3, LV2).
229#[macro_export]
230macro_rules! impl_ipc_processor_wrapper {
231    ($processor:ty) => {
232        impl $crate::mutex::UnsafeMutex<$processor> {
233            pub fn setup_audio_ports(&self) {
234                self.lock().setup_audio_ports();
235            }
236
237            pub fn audio_inputs(&self) -> &[std::sync::Arc<$crate::audio::io::AudioIO>] {
238                self.lock().audio_inputs()
239            }
240
241            pub fn audio_outputs(&self) -> &[std::sync::Arc<$crate::audio::io::AudioIO>] {
242                self.lock().audio_outputs()
243            }
244
245            pub fn main_audio_input_count(&self) -> usize {
246                self.lock().main_audio_input_count()
247            }
248
249            pub fn main_audio_output_count(&self) -> usize {
250                self.lock().main_audio_output_count()
251            }
252
253            pub fn midi_input_count(&self) -> usize {
254                self.lock().midi_input_count()
255            }
256
257            pub fn midi_output_count(&self) -> usize {
258                self.lock().midi_output_count()
259            }
260
261            pub fn set_bypassed(&self, bypassed: bool) {
262                self.lock().set_bypassed(bypassed);
263            }
264
265            pub fn name(&self) -> String {
266                self.lock().name().to_string()
267            }
268
269            pub fn run_host_callbacks_main_thread(&self) {
270                self.lock().run_host_callbacks_main_thread();
271            }
272
273            pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
274                self.lock().reconfigure_ports_if_needed()
275            }
276        }
277    };
278}