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