maolan_engine/plugins/
ipc.rs1use crate::audio::io::AudioIO;
4use crate::mutex::UnsafeMutex;
5use maolan_plugin_protocol::events::EventPair;
6use maolan_plugin_protocol::protocol::*;
7use maolan_plugin_protocol::shm::ShmMapping;
8use std::path::{Path, PathBuf};
9use std::process::{Child, Command, Stdio};
10use std::sync::Arc;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::time::{Duration, Instant};
13
14static NEXT_INSTANCE_ID: AtomicU64 = AtomicU64::new(0);
15
16pub fn unique_instance_id(format: &str) -> String {
18 let n = NEXT_INSTANCE_ID.fetch_add(1, Ordering::Relaxed);
19 format!("{}-{}-{}", format, std::process::id(), n)
20}
21
22pub struct HostSpawnArgs<'a> {
24 pub host_binary: &'a Path,
25 pub format: &'a str,
26 pub plugin_spec: &'a str,
27 pub instance_id: &'a str,
28 pub extra_args: &'a [&'a str],
29}
30
31pub fn spawn_host(args: HostSpawnArgs) -> Result<(Child, ShmMapping, EventPair, String), String> {
33 let pid = std::process::id();
34 let shm_name = format!("/maolan-{pid}-{}", args.instance_id);
35
36 let mapping = ShmMapping::create(&shm_name, SHM_SIZE)
37 .map_err(|e| format!("failed to create shared memory: {e}"))?;
38 unsafe {
39 init_shm_layout(mapping.as_ptr(), mapping.size());
40 }
41
42 let mut events = EventPair::new().map_err(|e| format!("failed to create event pipes: {e}"))?;
43
44 let mut cmd = Command::new(args.host_binary);
45 cmd.arg(args.format)
46 .arg(args.plugin_spec)
47 .arg(&shm_name)
48 .arg(args.instance_id)
49 .stdin(Stdio::null())
50 .stdout(Stdio::null())
51 .stderr(Stdio::inherit());
52
53 #[cfg(unix)]
54 {
55 cmd.arg(events.host_read_fd().to_string())
56 .arg(events.host_write_fd().to_string());
57 }
58
59 for arg in args.extra_args {
60 cmd.arg(arg);
61 }
62 #[cfg(windows)]
63 {
64 cmd.arg(events.daw_to_host_name())
65 .arg(events.host_to_daw_name());
66 }
67
68 let parent_args: Vec<String> = std::env::args().collect();
69 if let Some(pos) = parent_args.iter().position(|a| a == "--log-level")
70 && pos + 1 < parent_args.len()
71 {
72 cmd.arg("--log-level").arg(&parent_args[pos + 1]);
73 }
74
75 let child = cmd
76 .spawn()
77 .map_err(|e| format!("failed to spawn {} host: {e}", args.format))?;
78
79 events.close_daw_unused();
80
81 Ok((child, mapping, events, shm_name))
82}
83
84pub 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>]) {
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(
116 mapping: &Option<ShmMapping>,
117 events: &Option<EventPair>,
118 child: &UnsafeMutex<Option<Child>>,
119 shm_name: &str,
120) {
121 if let Some(mapping) = mapping
122 && let Some(events) = events
123 {
124 let header = unsafe { header_mut(mapping.as_ptr()) };
125 header.shutdown_request.store(1, Ordering::Release);
126 let _ = events.signal_host();
127 }
128 let mut child_opt = child.lock().take();
129 if let Some(mut child) = child_opt.take() {
130 let start = Instant::now();
131 while start.elapsed() < Duration::from_secs(2) {
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 let _ = child.kill();
139 }
140 }
141 let _ = ShmMapping::unlink(shm_name);
142}
143
144pub fn find_plugin_host_binary() -> Option<PathBuf> {
151 let exe_dir = std::env::current_exe()
152 .ok()
153 .and_then(|p| p.parent().map(PathBuf::from));
154
155 if let Some(ref dir) = exe_dir {
157 let candidate = dir.join("maolan-plugin-host");
158 if candidate.exists() {
159 tracing::info!(path = %candidate.display(), "Using plugin-host from exe directory");
160 return Some(candidate);
161 }
162 }
163
164 if let Ok(manifest) = std::env::var("CARGO_MANIFEST_DIR") {
166 let engine_root = Path::new(&manifest);
167 for profile in ["debug", "release"] {
168 let candidate = engine_root
170 .parent()
171 .unwrap_or(Path::new(""))
172 .join("daw")
173 .join("target")
174 .join(profile)
175 .join("maolan-plugin-host");
176 if candidate.exists() {
177 tracing::info!(path = %candidate.display(), "Using plugin-host from daw workspace target");
178 return Some(candidate);
179 }
180
181 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("maolan-plugin-host");
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") {
199 for dir in path_var.split(':') {
200 let candidate = Path::new(dir).join("maolan-plugin-host");
201 if candidate.exists() {
202 tracing::info!(path = %candidate.display(), "Using plugin-host from PATH");
203 return Some(candidate);
204 }
205 }
206 }
207
208 tracing::error!("maolan-plugin-host binary not found");
209 None
210}
211
212pub unsafe fn copy_inputs_to_shm(inputs: &[Arc<AudioIO>], ptr: *mut u8, frames: usize) {
217 for (ch, input) in inputs.iter().enumerate() {
218 let src = input.buffer.lock();
219 let dst = unsafe { audio_channel_ptr(ptr, ch, 0) };
220 let len = frames.min(src.len());
221 unsafe {
222 std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
223 }
224 }
225}
226
227pub unsafe fn copy_outputs_from_shm(outputs: &[Arc<AudioIO>], ptr: *mut u8, frames: usize) {
232 for (ch, output) in outputs.iter().enumerate() {
233 let dst = output.buffer.lock();
234 let src = unsafe { audio_channel_ptr(ptr, ch, 1) };
235 let len = frames.min(dst.len());
236 unsafe {
237 std::ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), len);
238 }
239 *output.finished.lock() = true;
240 }
241}
242
243pub unsafe fn configure_shm_header(ptr: *mut u8, frames: usize, num_in: usize, num_out: usize) {
248 unsafe {
249 let h = header_mut(ptr);
250 h.block_size.store(frames as u32, Ordering::Release);
251 h.num_input_channels.store(num_in as u32, Ordering::Release);
252 h.num_output_channels
253 .store(num_out as u32, Ordering::Release);
254 }
255}
256
257#[macro_export]
260macro_rules! impl_ipc_processor_wrapper {
261 ($processor:ty) => {
262 impl $crate::mutex::UnsafeMutex<$processor> {
263 pub fn setup_audio_ports(&self) {
264 self.lock().setup_audio_ports();
265 }
266
267 pub fn audio_inputs(&self) -> &[std::sync::Arc<$crate::audio::io::AudioIO>] {
268 self.lock().audio_inputs()
269 }
270
271 pub fn audio_outputs(&self) -> &[std::sync::Arc<$crate::audio::io::AudioIO>] {
272 self.lock().audio_outputs()
273 }
274
275 pub fn main_audio_input_count(&self) -> usize {
276 self.lock().main_audio_input_count()
277 }
278
279 pub fn main_audio_output_count(&self) -> usize {
280 self.lock().main_audio_output_count()
281 }
282
283 pub fn midi_input_count(&self) -> usize {
284 self.lock().midi_input_count()
285 }
286
287 pub fn midi_output_count(&self) -> usize {
288 self.lock().midi_output_count()
289 }
290
291 pub fn set_bypassed(&self, bypassed: bool) {
292 self.lock().set_bypassed(bypassed);
293 }
294
295 pub fn name(&self) -> String {
296 self.lock().name().to_string()
297 }
298
299 pub fn run_host_callbacks_main_thread(&self) {
300 self.lock().run_host_callbacks_main_thread();
301 }
302
303 pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
304 self.lock().reconfigure_ports_if_needed()
305 }
306 }
307 };
308}