Skip to main content

ferraus_plugins/
host_rt.rs

1//! The host-process side of the protocol: framing, shared-memory mapping, and dispatch to a
2//! format backend (CLAP in `ferraus-plughost`, VST3 in `ferraus-plughost-vst3`).
3
4use crate::protocol::*;
5use base64::Engine;
6use std::fs::File;
7use std::io::{Read, Write};
8use std::path::Path;
9
10/// An error a backend reports back to the client.
11#[derive(Debug, Clone)]
12pub struct HostError {
13    /// `load_failed`, `unknown_param`, `bad_value`, `no_state`, `process_failed`,
14    /// `unsupported`, `bad_request`.
15    pub code: String,
16    pub message: String,
17}
18
19impl HostError {
20    pub fn new(code: &str, message: impl Into<String>) -> Self {
21        HostError { code: code.into(), message: message.into() }
22    }
23}
24
25pub type HostResult<T> = Result<T, HostError>;
26
27/// Result of activation.
28#[derive(Debug, Clone, Copy)]
29pub struct Activation {
30    pub latency: u32,
31    pub tail: Option<u32>,
32}
33
34/// One plugin format. Every method runs on the host's single thread.
35pub trait Backend {
36    /// Program name reported in `hello`.
37    fn host_name(&self) -> &'static str;
38    fn formats(&self) -> Vec<String>;
39    fn probe(&mut self, path: &Path) -> HostResult<Vec<Descriptor>>;
40    fn load(&mut self, path: &Path, plugin_id: Option<&str>) -> HostResult<PluginInfo>;
41    fn get_params(&mut self) -> HostResult<Vec<ParamValue>>;
42    fn set_params(&mut self, params: &[ParamAssign]) -> HostResult<Vec<ParamValue>>;
43    fn list_presets(&mut self) -> HostResult<Vec<PresetInfo>>;
44    fn load_preset(&mut self, preset: &str) -> HostResult<Vec<ParamValue>>;
45    fn get_state(&mut self) -> HostResult<Vec<u8>>;
46    fn set_state(&mut self, state: &[u8]) -> HostResult<()>;
47    /// Activate and start processing. The main ports' channel counts come from
48    /// [`PluginInfo::main_in_channels`]/[`PluginInfo::main_out_channels`].
49    fn activate(&mut self, sample_rate: f64, block_size: u32) -> HostResult<Activation>;
50    /// Process one block from the main input channels into the main output channels.
51    fn process(
52        &mut self,
53        inputs: &[&[f32]],
54        outputs: &mut [&mut [f32]],
55        frames: u32,
56        events: &[Event],
57    ) -> HostResult<()>;
58    fn deactivate(&mut self);
59    /// Main channel counts, valid after `load`.
60    fn main_channels(&self) -> (u32, u32);
61}
62
63/// Move the process's stdout out of reach of plugin code: the protocol gets a private
64/// duplicate of fd 1 and fd 1 itself is pointed at stderr.
65fn take_protocol_stdout() -> File {
66    #[cfg(unix)]
67    unsafe {
68        use std::os::fd::FromRawFd;
69        let fd = libc::dup(1);
70        if fd >= 0 {
71            libc::dup2(2, 1);
72            return File::from_raw_fd(fd);
73        }
74        File::from_raw_fd(1)
75    }
76    #[cfg(windows)]
77    unsafe {
78        use std::os::windows::io::FromRawHandle;
79        let fd = libc::dup(1);
80        if fd >= 0 {
81            libc::dup2(2, 1);
82            let h = libc::get_osfhandle(fd);
83            return File::from_raw_handle(h as _);
84        }
85        let h = libc::get_osfhandle(1);
86        File::from_raw_handle(h as _)
87    }
88}
89
90struct Shm {
91    map: memmap2::MmapMut,
92    layout: ShmLayout,
93}
94
95/// Run the request loop until `quit` or end of input. Returns the process exit code.
96pub fn serve(backend: &mut dyn Backend) -> i32 {
97    let mut out = take_protocol_stdout();
98    let stdin = std::io::stdin();
99    let mut input = stdin.lock();
100    serve_on(backend, &mut input, &mut out)
101}
102
103/// The loop itself, on arbitrary streams (used by tests).
104pub fn serve_on(backend: &mut dyn Backend, input: &mut dyn Read, out: &mut dyn Write) -> i32 {
105    let mut shm: Option<Shm> = None;
106    let mut block_size = 0u32;
107    loop {
108        let frame = match read_frame(input) {
109            Ok(Some(f)) => f,
110            Ok(None) => return 0,
111            Err(e) => {
112                eprintln!("ferraus-plughost: broken control pipe: {e}");
113                return 3;
114            }
115        };
116        let req: Request = match serde_json::from_slice(&frame) {
117            Ok(r) => r,
118            Err(e) => {
119                let reply = Reply::Error { code: "bad_request".into(), message: format!("cannot parse request: {e}") };
120                if write_frame(out, &reply).is_err() {
121                    return 3;
122                }
123                continue;
124            }
125        };
126        let quit = matches!(req, Request::Quit);
127        let reply = handle(backend, req, &mut shm, &mut block_size);
128        if write_frame(out, &reply).is_err() {
129            return 3;
130        }
131        if quit {
132            return 0;
133        }
134    }
135}
136
137fn err(e: HostError) -> Reply {
138    Reply::Error { code: e.code, message: e.message }
139}
140
141fn handle(backend: &mut dyn Backend, req: Request, shm: &mut Option<Shm>, block_size: &mut u32) -> Reply {
142    let b64 = base64::engine::general_purpose::STANDARD;
143    match req {
144        Request::Hello => Reply::Hello {
145            host: backend.host_name().into(),
146            version: env!("CARGO_PKG_VERSION").into(),
147            protocol: PROTOCOL_VERSION,
148            formats: backend.formats(),
149        },
150        Request::Probe { path } => match backend.probe(Path::new(&path)) {
151            Ok(plugins) => Reply::Probe { plugins },
152            Err(e) => err(e),
153        },
154        Request::Load { path, plugin_id } => match backend.load(Path::new(&path), plugin_id.as_deref()) {
155            Ok(info) => Reply::Loaded { info },
156            Err(e) => err(e),
157        },
158        Request::SetParams { params } => match backend.set_params(&params) {
159            Ok(params) => Reply::Params { params },
160            Err(e) => err(e),
161        },
162        Request::GetParams => match backend.get_params() {
163            Ok(params) => Reply::Params { params },
164            Err(e) => err(e),
165        },
166        Request::ListPresets => match backend.list_presets() {
167            Ok(presets) => Reply::Presets { presets },
168            Err(e) => err(e),
169        },
170        Request::LoadPreset { preset } => match backend.load_preset(&preset) {
171            Ok(params) => Reply::Params { params },
172            Err(e) => err(e),
173        },
174        Request::GetState => match backend.get_state() {
175            Ok(s) => Reply::State { state_b64: b64.encode(s) },
176            Err(e) => err(e),
177        },
178        Request::SetState { state_b64 } => match b64.decode(state_b64.trim()) {
179            Ok(bytes) => match backend.set_state(&bytes) {
180                Ok(()) => Reply::Ok,
181                Err(e) => err(e),
182            },
183            Err(e) => err(HostError::new("bad_request", format!("state is not base64: {e}"))),
184        },
185        Request::Activate { sample_rate, block_size: bs, shm_path } => {
186            let map = match File::options()
187                .read(true)
188                .write(true)
189                .open(&shm_path)
190                .and_then(|f| unsafe { memmap2::MmapMut::map_mut(&f) })
191            {
192                Ok(m) => m,
193                Err(e) => {
194                    return err(HostError::new("bad_request", format!("cannot map shared memory {shm_path}: {e}")))
195                }
196            };
197            let layout = match ShmLayout::read_header(&map) {
198                Ok(l) => l,
199                Err(e) => return err(HostError::new("bad_request", e)),
200            };
201            if layout.block_size != bs {
202                return err(HostError::new("bad_request", "block size differs from the shared memory header"));
203            }
204            match backend.activate(sample_rate, bs) {
205                Ok(a) => {
206                    let (ci, co) = backend.main_channels();
207                    *shm = Some(Shm { map, layout });
208                    *block_size = bs;
209                    Reply::Activated { latency: a.latency, tail: a.tail, channels_in: ci, channels_out: co }
210                }
211                Err(e) => err(e),
212            }
213        }
214        Request::Process { slot, frames, events } => {
215            let Some(s) = shm.as_mut() else {
216                return err(HostError::new("bad_request", "process before activate"));
217            };
218            let l = s.layout;
219            if slot >= l.slots || frames > *block_size || frames > l.block_size {
220                return err(HostError::new("bad_request", "slot or frame count out of range"));
221            }
222            std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
223            let base = s.map.as_mut_ptr();
224            let n = frames as usize;
225            // SAFETY: offsets come from the validated layout, input and output ranges are
226            // disjoint, and the client does not touch this slot until we reply.
227            let ins: Vec<&[f32]> =
228                (0..l.channels_in).map(|c| unsafe { &*f32_slice_mut(base, l.input_offset(slot, c), n) }).collect();
229            let mut outs: Vec<&mut [f32]> =
230                (0..l.channels_out).map(|c| unsafe { f32_slice_mut(base, l.output_offset(slot, c), n) }).collect();
231            for o in outs.iter_mut() {
232                o.fill(0.0);
233            }
234            let r = backend.process(&ins, &mut outs, frames, &events);
235            std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst);
236            match r {
237                Ok(()) => Reply::Processed { slot },
238                Err(e) => err(e),
239            }
240        }
241        Request::Deactivate => {
242            backend.deactivate();
243            *shm = None;
244            Reply::Ok
245        }
246        Request::Quit => {
247            backend.deactivate();
248            *shm = None;
249            Reply::Ok
250        }
251    }
252}