maolan_engine/plugins/
ipc.rs1use 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
9const 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
248pub 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
263pub 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
282pub 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}