maolan_engine/plugins/
ipc.rs1use 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
226pub 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
242pub 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
259pub 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}