Skip to main content

maolan_engine/plugins/
ipc.rs

1use crate::audio::io::AudioIO;
2use maolan_plugin_protocol::events::EventPair;
3use maolan_plugin_protocol::protocol::*;
4use maolan_plugin_protocol::shm::ShmMapping;
5use std::path::{Path, PathBuf};
6use std::process::{Child, ChildStderr, Command, Stdio};
7use std::sync::Arc;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::time::{Duration, Instant};
10
11static NEXT_INSTANCE_ID: AtomicU64 = AtomicU64::new(0);
12
13pub fn unique_instance_id(format: &str) -> String {
14    let n = NEXT_INSTANCE_ID.fetch_add(1, Ordering::Relaxed);
15    format!("{}-{}-{}", format, std::process::id(), n)
16}
17
18pub struct HostSpawnArgs<'a> {
19    pub host_binary: &'a Path,
20    pub format: &'a str,
21    pub plugin_spec: &'a str,
22    pub instance_id: &'a str,
23    pub extra_args: &'a [&'a str],
24}
25
26pub fn spawn_host(
27    args: HostSpawnArgs,
28) -> Result<(Child, ShmMapping, EventPair, String, Option<ChildStderr>), String> {
29    let pid = std::process::id();
30    let shm_name = format!("/maolan-{pid}-{}", args.instance_id);
31
32    let mapping = ShmMapping::create(&shm_name, SHM_SIZE)
33        .map_err(|e| format!("failed to create shared memory: {e}"))?;
34    unsafe {
35        init_shm_layout(mapping.as_ptr(), mapping.size());
36    }
37
38    let mut events = EventPair::new().map_err(|e| format!("failed to create event pipes: {e}"))?;
39
40    let mut cmd = Command::new(args.host_binary);
41    cmd.arg(args.format)
42        .arg(args.plugin_spec)
43        .arg(&shm_name)
44        .arg(args.instance_id)
45        .stdin(Stdio::null())
46        .stdout(Stdio::null())
47        .stderr(Stdio::piped());
48
49    #[cfg(unix)]
50    {
51        cmd.arg(events.host_read_fd().to_string())
52            .arg(events.host_write_fd().to_string());
53    }
54
55    for arg in args.extra_args {
56        cmd.arg(arg);
57    }
58    #[cfg(windows)]
59    {
60        cmd.arg(events.daw_to_host_name())
61            .arg(events.host_to_daw_name());
62    }
63
64    append_parent_log_level(&mut cmd);
65
66    let mut child = cmd
67        .spawn()
68        .map_err(|e| format!("failed to spawn {} host: {e}", args.format))?;
69    let stderr = child.stderr.take();
70
71    events.close_daw_unused();
72
73    Ok((child, mapping, events, shm_name, stderr))
74}
75
76pub fn append_parent_log_level(cmd: &mut Command) {
77    let parent_args: Vec<String> = std::env::args().collect();
78    if let Some(pos) = parent_args.iter().position(|a| a == "--log-level")
79        && pos + 1 < parent_args.len()
80    {
81        cmd.arg("--log-level").arg(&parent_args[pos + 1]);
82    }
83}
84
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
96pub fn bypass_copy_inputs_to_outputs(inputs: &[Arc<AudioIO>], outputs: &[Arc<AudioIO>]) {
97    for (input, output) in inputs.iter().zip(outputs.iter()) {
98        let src = input.buffer.lock();
99        let dst = output.buffer.lock();
100        dst.fill(0.0);
101        for (d, s) in dst.iter_mut().zip(src.iter()) {
102            *d = *s;
103        }
104        *output.finished.lock() = true;
105    }
106    for output in outputs.iter().skip(inputs.len()) {
107        let dst = output.buffer.lock();
108        dst.fill(0.0);
109        *output.finished.lock() = true;
110    }
111}
112
113pub fn drop_host(
114    mapping: Option<ShmMapping>,
115    events: Option<EventPair>,
116    child: Option<Child>,
117    shm_name: String,
118) {
119    if let Some(ref mapping) = mapping
120        && let Some(ref events) = events
121    {
122        let header = unsafe { header_mut(mapping.as_ptr()) };
123        header.shutdown_request.store(1, Ordering::Release);
124        let _ = events.signal_host();
125    }
126
127    std::thread::spawn(move || {
128        tracing::info!(%shm_name, "drop_host: waiting for plugin host process to exit");
129        if let Some(mut child) = child {
130            let start = Instant::now();
131            while start.elapsed() < Duration::from_secs(5) {
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                tracing::warn!(%shm_name, "drop_host: plugin host did not exit in time, killing");
139                let _ = child.kill();
140                let _ = child.wait();
141            }
142        }
143        drop(mapping);
144        drop(events);
145        let _ = ShmMapping::unlink(&shm_name);
146        tracing::info!(%shm_name, "drop_host: cleanup complete");
147    });
148}
149
150pub fn find_plugin_host_binary() -> Option<PathBuf> {
151    let host_name = if cfg!(windows) {
152        "maolan-plugin-host.exe"
153    } else {
154        "maolan-plugin-host"
155    };
156
157    if let Ok(override_path) = std::env::var("MAOLAN_PLUGIN_HOST") {
158        let candidate = PathBuf::from(override_path);
159        if candidate.exists() {
160            tracing::info!(path = %candidate.display(), "Using plugin-host from MAOLAN_PLUGIN_HOST");
161            return Some(candidate);
162        }
163        tracing::warn!(path = %candidate.display(), "MAOLAN_PLUGIN_HOST points to a missing file");
164    }
165
166    let exe_dir = std::env::current_exe()
167        .ok()
168        .and_then(|p| p.parent().map(PathBuf::from));
169
170    if let Some(ref dir) = exe_dir {
171        let candidate = dir.join(host_name);
172        if candidate.exists() {
173            tracing::info!(path = %candidate.display(), "Using plugin-host from exe directory");
174            return Some(candidate);
175        }
176    }
177
178    if let Ok(manifest) = std::env::var("CARGO_MANIFEST_DIR") {
179        let engine_root = Path::new(&manifest);
180        for profile in ["debug", "release"] {
181            let candidate = engine_root
182                .parent()
183                .unwrap_or(Path::new(""))
184                .join("daw")
185                .join("target")
186                .join(profile)
187                .join(host_name);
188            if candidate.exists() {
189                tracing::info!(path = %candidate.display(), "Using plugin-host from daw workspace target");
190                return Some(candidate);
191            }
192
193            let candidate = engine_root
194                .parent()
195                .unwrap_or(Path::new(""))
196                .join("daw")
197                .join("plugin-host")
198                .join("target")
199                .join(profile)
200                .join(host_name);
201            if candidate.exists() {
202                tracing::info!(path = %candidate.display(), "Using plugin-host from plugin-host crate target");
203                return Some(candidate);
204            }
205        }
206    }
207
208    if let Ok(path_var) = std::env::var("PATH") {
209        #[cfg(windows)]
210        let path_sep = ';';
211        #[cfg(not(windows))]
212        let path_sep = ':';
213        for dir in path_var.split(path_sep) {
214            let candidate = Path::new(dir).join(host_name);
215            if candidate.exists() {
216                tracing::info!(path = %candidate.display(), "Using plugin-host from PATH");
217                return Some(candidate);
218            }
219        }
220    }
221
222    tracing::error!("maolan-plugin-host binary not found");
223    None
224}
225
226/// # Safety
227///
228/// `ptr` must point to a valid, initialized shared-memory layout with enough
229/// space for the configured number of input channels and `frames` samples.
230/// `frames` must not exceed the block size reserved in that layout.
231pub unsafe fn copy_inputs_to_shm(inputs: &[Arc<AudioIO>], ptr: *mut u8, frames: usize) {
232    for (ch, input) in inputs.iter().enumerate() {
233        let src = input.buffer.lock();
234        let dst = unsafe { audio_channel_ptr(ptr, ch, 0) };
235        let len = frames.min(src.len());
236        unsafe {
237            std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
238        }
239    }
240}
241
242/// # Safety
243///
244/// `ptr` must point to a valid, initialized shared-memory layout with enough
245/// space for the configured number of output channels and `frames` samples.
246/// Each output buffer must be writable and at least `frames` elements long.
247pub unsafe fn copy_outputs_from_shm(outputs: &[Arc<AudioIO>], ptr: *mut u8, frames: usize) {
248    for (ch, output) in outputs.iter().enumerate() {
249        let dst = output.buffer.lock();
250        let src = unsafe { audio_channel_ptr(ptr, ch, 1) };
251        let len = frames.min(dst.len());
252        unsafe {
253            std::ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), len);
254        }
255        *output.finished.lock() = true;
256    }
257}
258
259/// # Safety
260///
261/// `ptr` must point to a valid, initialized shared-memory layout whose header
262/// can safely be written to.
263pub unsafe fn configure_shm_header(
264    ptr: *mut u8,
265    frames: usize,
266    num_in: usize,
267    num_out: usize,
268    midi_in: usize,
269    midi_out: usize,
270) {
271    unsafe {
272        let h = header_mut(ptr);
273        h.block_size.store(frames as u32, Ordering::Release);
274        h.num_input_channels.store(num_in as u32, Ordering::Release);
275        h.num_output_channels
276            .store(num_out as u32, Ordering::Release);
277        h.midi_in_port_count
278            .store(midi_in as u32, Ordering::Release);
279        h.midi_out_port_count
280            .store(midi_out as u32, Ordering::Release);
281    }
282}
283
284#[macro_export]
285macro_rules! impl_ipc_processor_wrapper {
286    ($processor:ty) => {
287        impl $crate::mutex::UnsafeMutex<$processor> {
288            pub fn setup_audio_ports(&self) {
289                self.lock().setup_audio_ports();
290            }
291
292            pub fn audio_inputs(&self) -> &[std::sync::Arc<$crate::audio::io::AudioIO>] {
293                self.lock().audio_inputs()
294            }
295
296            pub fn audio_outputs(&self) -> &[std::sync::Arc<$crate::audio::io::AudioIO>] {
297                self.lock().audio_outputs()
298            }
299
300            pub fn main_audio_input_count(&self) -> usize {
301                self.lock().main_audio_input_count()
302            }
303
304            pub fn main_audio_output_count(&self) -> usize {
305                self.lock().main_audio_output_count()
306            }
307
308            pub fn midi_input_count(&self) -> usize {
309                self.lock().midi_input_count()
310            }
311
312            pub fn midi_output_count(&self) -> usize {
313                self.lock().midi_output_count()
314            }
315
316            pub fn set_bypassed(&self, bypassed: bool) {
317                self.lock().set_bypassed(bypassed);
318            }
319
320            pub fn name(&self) -> String {
321                self.lock().name().to_string()
322            }
323
324            pub fn run_host_callbacks_main_thread(&self) {
325                self.lock().run_host_callbacks_main_thread();
326            }
327
328            pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
329                self.lock().reconfigure_ports_if_needed()
330            }
331        }
332    };
333}