maolan_engine/plugins/
ipc.rs1use 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 append_parent_log_level(&mut cmd);
66
67 let mut child = cmd
68 .spawn()
69 .map_err(|e| format!("failed to spawn {} host: {e}", args.format))?;
70 let stderr = child.stderr.take();
71
72 events.close_daw_unused();
73
74 Ok((child, mapping, events, shm_name, stderr))
75}
76
77pub fn append_parent_log_level(cmd: &mut Command) {
78 let parent_args: Vec<String> = std::env::args().collect();
79 if let Some(pos) = parent_args.iter().position(|a| a == "--log-level")
80 && pos + 1 < parent_args.len()
81 {
82 cmd.arg("--log-level").arg(&parent_args[pos + 1]);
83 }
84}
85
86pub fn wait_for_ready(header: &ShmHeader, timeout: Duration) -> bool {
87 let start = Instant::now();
88 while start.elapsed() < timeout {
89 if header.ready.load(Ordering::Acquire) != 0 {
90 return true;
91 }
92 std::thread::sleep(Duration::from_millis(5));
93 }
94 false
95}
96
97pub fn bypass_copy_inputs_to_outputs(inputs: &[Arc<AudioIO>], outputs: &[Arc<AudioIO>]) {
98 for (input, output) in inputs.iter().zip(outputs.iter()) {
99 let src = input.buffer.lock();
100 let dst = output.buffer.lock();
101 dst.fill(0.0);
102 for (d, s) in dst.iter_mut().zip(src.iter()) {
103 *d = *s;
104 }
105 *output.finished.lock() = true;
106 }
107 for output in outputs.iter().skip(inputs.len()) {
108 let dst = output.buffer.lock();
109 dst.fill(0.0);
110 *output.finished.lock() = true;
111 }
112}
113
114pub fn drop_host(
115 mapping: &Option<ShmMapping>,
116 events: &Option<EventPair>,
117 child: &UnsafeMutex<Option<Child>>,
118 shm_name: &str,
119) {
120 if let Some(mapping) = mapping
121 && let Some(events) = events
122 {
123 let header = unsafe { header_mut(mapping.as_ptr()) };
124 header.shutdown_request.store(1, Ordering::Release);
125 let _ = events.signal_host();
126 }
127 let mut child_opt = child.lock().take();
128 if let Some(mut child) = child_opt.take() {
129 let start = Instant::now();
130 while start.elapsed() < Duration::from_secs(2) {
131 if child.try_wait().map(|s| s.is_some()).unwrap_or(true) {
132 break;
133 }
134 std::thread::sleep(Duration::from_millis(10));
135 }
136 if child.try_wait().map(|s| s.is_none()).unwrap_or(false) {
137 let _ = child.kill();
138 }
139 }
140 let _ = ShmMapping::unlink(shm_name);
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
219pub unsafe fn copy_inputs_to_shm(inputs: &[Arc<AudioIO>], ptr: *mut u8, frames: usize) {
225 for (ch, input) in inputs.iter().enumerate() {
226 let src = input.buffer.lock();
227 let dst = unsafe { audio_channel_ptr(ptr, ch, 0) };
228 let len = frames.min(src.len());
229 unsafe {
230 std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
231 }
232 }
233}
234
235pub unsafe fn copy_outputs_from_shm(outputs: &[Arc<AudioIO>], ptr: *mut u8, frames: usize) {
241 for (ch, output) in outputs.iter().enumerate() {
242 let dst = output.buffer.lock();
243 let src = unsafe { audio_channel_ptr(ptr, ch, 1) };
244 let len = frames.min(dst.len());
245 unsafe {
246 std::ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), len);
247 }
248 *output.finished.lock() = true;
249 }
250}
251
252pub unsafe fn configure_shm_header(
257 ptr: *mut u8,
258 frames: usize,
259 num_in: usize,
260 num_out: usize,
261 midi_in: usize,
262 midi_out: usize,
263) {
264 unsafe {
265 let h = header_mut(ptr);
266 h.block_size.store(frames as u32, Ordering::Release);
267 h.num_input_channels.store(num_in as u32, Ordering::Release);
268 h.num_output_channels
269 .store(num_out as u32, Ordering::Release);
270 h.midi_in_port_count
271 .store(midi_in as u32, Ordering::Release);
272 h.midi_out_port_count
273 .store(midi_out as u32, Ordering::Release);
274 }
275}
276
277#[macro_export]
278macro_rules! impl_ipc_processor_wrapper {
279 ($processor:ty) => {
280 impl $crate::mutex::UnsafeMutex<$processor> {
281 pub fn setup_audio_ports(&self) {
282 self.lock().setup_audio_ports();
283 }
284
285 pub fn audio_inputs(&self) -> &[std::sync::Arc<$crate::audio::io::AudioIO>] {
286 self.lock().audio_inputs()
287 }
288
289 pub fn audio_outputs(&self) -> &[std::sync::Arc<$crate::audio::io::AudioIO>] {
290 self.lock().audio_outputs()
291 }
292
293 pub fn main_audio_input_count(&self) -> usize {
294 self.lock().main_audio_input_count()
295 }
296
297 pub fn main_audio_output_count(&self) -> usize {
298 self.lock().main_audio_output_count()
299 }
300
301 pub fn midi_input_count(&self) -> usize {
302 self.lock().midi_input_count()
303 }
304
305 pub fn midi_output_count(&self) -> usize {
306 self.lock().midi_output_count()
307 }
308
309 pub fn set_bypassed(&self, bypassed: bool) {
310 self.lock().set_bypassed(bypassed);
311 }
312
313 pub fn name(&self) -> String {
314 self.lock().name().to_string()
315 }
316
317 pub fn run_host_callbacks_main_thread(&self) {
318 self.lock().run_host_callbacks_main_thread();
319 }
320
321 pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
322 self.lock().reconfigure_ports_if_needed()
323 }
324 }
325 };
326}