Skip to main content

maolan_engine/plugins/
ipc.rs

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