Skip to main content

truce_rack_core/
wrapper.rs

1//! `catch_unwind` helpers for FFI boundaries between rack and
2//! plugin code.
3//!
4//! Plugin libraries are written by third parties. A panic in a
5//! plugin's `process` (or any other callback) would unwind
6//! across the `extern "C"` boundary back into the host —
7//! undefined behaviour on most toolchains, abort on others.
8//! These helpers catch the unwind, log a short diagnostic, and
9//! return a fallback value the wrapper can hand back to its
10//! caller.
11//!
12//! Mirrors `truce_core::wrapper`. The plugin-side framework
13//! catches panics going *out* of plugin code; the host-side
14//! framework catches panics going *in* from plugin code. Same
15//! helper shape, opposite direction.
16
17use crate::error::Error;
18use std::any::type_name;
19use std::panic::{AssertUnwindSafe, catch_unwind};
20
21/// Run a per-audio-block callback body under [`catch_unwind`]
22/// with no fallback value — caller only cares whether the body
23/// panicked.
24///
25/// Returns `true` on a clean exit, `false` on panic. Wrappers
26/// should zero output buffers on `false` so the host doesn't
27/// hear garbage from whatever was in those slots.
28#[must_use]
29pub fn run_audio_block<P>(format: &str, body: impl FnOnce()) -> bool {
30    let result = catch_unwind(AssertUnwindSafe(body));
31    if let Err(payload) = result {
32        eprintln!(
33            "[truce-rack {format}] panic in process() for plugin {}: {}",
34            type_name::<P>(),
35            extract_panic_msg(&payload),
36        );
37        return false;
38    }
39    true
40}
41
42/// Run a per-audio-block callback body under [`catch_unwind`]
43/// with a fallback return value. Returns the body's value on
44/// clean exit, `fallback` on panic.
45pub fn run_audio_block_with<P, R>(format: &str, fallback: R, body: impl FnOnce() -> R) -> R {
46    match catch_unwind(AssertUnwindSafe(body)) {
47        Ok(value) => value,
48        Err(payload) => {
49            eprintln!(
50                "[truce-rack {format}] panic in process() for plugin {}: {}",
51                type_name::<P>(),
52                extract_panic_msg(&payload),
53            );
54            fallback
55        }
56    }
57}
58
59/// Run an `extern "C"` plugin callback body (state save / load,
60/// parameter formatting, GUI handler) under [`catch_unwind`]
61/// with a fallback return value. `action` is logged for
62/// debuggability ("`save_state`", "`load_state`", "`format_value`", …).
63pub fn run_extern_callback_with<P, R>(
64    format: &str,
65    action: &str,
66    fallback: R,
67    body: impl FnOnce() -> R,
68) -> R {
69    match catch_unwind(AssertUnwindSafe(body)) {
70        Ok(value) => value,
71        Err(payload) => {
72            eprintln!(
73                "[truce-rack {format}] panic in {action} for plugin {}: {}",
74                type_name::<P>(),
75                extract_panic_msg(&payload),
76            );
77            fallback
78        }
79    }
80}
81
82/// Convenience: run a plugin callback under [`catch_unwind`]
83/// and convert a panic into an [`Error::Panic`]. Used by the
84/// `PluginCore` / `Plugin` impl shims when the callback's
85/// natural error type is `crate::Result<T>`.
86///
87/// Distinct from [`run_extern_callback_with`] in that this one
88/// is *inside* the rack-side Rust trait surface, not at the
89/// raw C ABI edge — but the catch-and-log pattern is the same.
90///
91/// # Errors
92/// `body`'s error propagates on a clean failure; a panic in
93/// `body` returns [`Error::Panic`] with the supplied `action`
94/// string.
95pub fn run_callable<P, T>(
96    action: &'static str,
97    body: impl FnOnce() -> crate::Result<T>,
98) -> crate::Result<T> {
99    match catch_unwind(AssertUnwindSafe(body)) {
100        Ok(result) => result,
101        Err(payload) => Err(Error::Panic {
102            action,
103            message: extract_panic_msg(&payload).to_string(),
104        }),
105    }
106}
107
108fn extract_panic_msg(payload: &Box<dyn std::any::Any + Send>) -> &str {
109    if let Some(s) = payload.downcast_ref::<&'static str>() {
110        s
111    } else if let Some(s) = payload.downcast_ref::<String>() {
112        s.as_str()
113    } else {
114        "<non-string panic payload>"
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    struct Dummy;
123
124    #[test]
125    fn clean_audio_block_returns_true() {
126        let ran = std::cell::Cell::new(false);
127        let result = run_audio_block::<Dummy>("test", || ran.set(true));
128        assert!(result);
129        assert!(ran.get());
130    }
131
132    #[test]
133    fn panicking_audio_block_returns_false() {
134        let result = run_audio_block::<Dummy>("test", || panic!("boom"));
135        assert!(!result);
136    }
137
138    #[test]
139    fn callable_panic_becomes_error() {
140        let result: crate::Result<()> = run_callable::<Dummy, ()>("save_state", || panic!("oops"));
141        match result {
142            Err(Error::Panic { action, .. }) => assert_eq!(action, "save_state"),
143            _ => panic!("expected Error::Panic"),
144        }
145    }
146
147    #[test]
148    fn callable_error_propagates() {
149        let result: crate::Result<()> =
150            run_callable::<Dummy, ()>("save_state", || Err(Error::Other("plain error".into())));
151        assert!(matches!(result, Err(Error::Other(_))));
152    }
153}