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