Skip to main content

maolan_engine/plugins/
ipc.rs

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