Skip to main content

nice_plug/wrapper/
util.rs

1use backtrace::Backtrace;
2use nice_plug_core::plugin::Plugin;
3use std::cmp;
4use std::marker::PhantomData;
5use std::os::raw::c_char;
6use std::sync::atomic::AtomicBool;
7
8use crate::util::permit_alloc;
9
10pub(crate) mod buffer_management;
11#[cfg(debug_assertions)]
12pub(crate) mod context_checks;
13
14/// The bit that controls flush-to-zero behavior for denormals in 32 and 64-bit floating point
15/// numbers on x86 family architectures. Rust 1.75 deprecated the built in functions for controlling
16/// these registers. As listed in section 10.2.3.3 (Flush-To-Zero), bit 15 of the MXCSR register
17/// controls the FTZ behavior.
18///
19/// <https://cdrdv2-public.intel.com/843823/252046-sdm-change-document-1.pdf>
20#[cfg(all(not(miri), target_feature = "sse", feature = "unsafe_flush_denormals"))]
21const SSE_FTZ_BIT: u32 = 1 << 15;
22
23/// The bit that controls flush-to-zero behavior for denormals in 32 and 64-bit floating point
24/// numbers on AArch64.
25///
26/// <https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/FPCR--Floating-point-Control-Register>
27#[cfg(all(not(miri), target_arch = "aarch64", feature = "unsafe_flush_denormals"))]
28const AARCH64_FTZ_BIT: u64 = 1 << 24;
29
30#[cfg(all(
31    debug_assertions,
32    feature = "assert_process_allocs",
33    all(windows, target_env = "gnu")
34))]
35compile_error!(
36    "The 'assert_process_allocs' feature does not work correctly in combination with the 'x86_64-pc-windows-gnu' target, see https://github.com/Windfisch/rust-assert-no-alloc/issues/7"
37);
38
39#[cfg(all(debug_assertions, feature = "assert_process_allocs"))]
40#[global_allocator]
41static A: nice_assert_no_alloc::AllocDisabler = nice_assert_no_alloc::AllocDisabler;
42
43/// A Rabin fingerprint based string hash for parameter ID strings.
44pub fn hash_param_id(id: &str) -> u32 {
45    let mut hash: u32 = 0;
46    for char in id.bytes() {
47        hash = hash.wrapping_mul(31).wrapping_add(char as u32);
48    }
49
50    // In VST3 the last bit is reserved for parameters provided by the host
51    // https://developer.steinberg.help/display/VST/Parameters+and+Automation
52    hash &= !(1 << 31);
53
54    hash
55}
56
57/// The equivalent of the `strlcpy()` C function. Copy `src` to `dest` as a null-terminated
58/// C-string. If `dest` does not have enough capacity, add a null terminator at the end to prevent
59/// buffer overflows.
60pub fn strlcpy(dest: &mut [c_char], src: &str) {
61    if dest.is_empty() {
62        return;
63    }
64
65    let src_bytes: &[u8] = src.as_bytes();
66    // NOTE: `c_char` is i8 on x86 based archs, and u8 on AArch64. There this line won't do
67    //       anything.
68    let src_bytes_signed: &[c_char] = unsafe { &*(src_bytes as *const [u8] as *const [c_char]) };
69
70    // Make sure there's always room for a null terminator
71    let copy_len = cmp::min(dest.len() - 1, src.len());
72    dest[..copy_len].copy_from_slice(&src_bytes_signed[..copy_len]);
73    dest[copy_len] = 0;
74}
75
76/// Clamp an input event's timing to the buffer length. Emits a debug assertion failure if it was
77/// out of bounds.
78#[inline]
79pub fn clamp_input_event_timing(timing: u32, total_buffer_len: u32) -> u32 {
80    // If `total_buffer_len == 0`, then 0 is a valid timing
81    let last_valid_index = total_buffer_len.saturating_sub(1);
82
83    crate::nice_debug_assert!(
84        timing <= last_valid_index,
85        "Input event is out of bounds, will be clamped to the buffer's size"
86    );
87
88    timing.min(last_valid_index)
89}
90
91/// Clamp an output event's timing to the buffer length. Emits a debug assertion failure if it was
92/// out of bounds.
93#[inline]
94pub fn clamp_output_event_timing(timing: u32, total_buffer_len: u32) -> u32 {
95    let last_valid_index = total_buffer_len.saturating_sub(1);
96
97    crate::nice_debug_assert!(
98        timing <= last_valid_index,
99        "Output event is out of bounds, will be clamped to the buffer's size"
100    );
101
102    timing.min(last_valid_index)
103}
104
105/// Set up the logger so that the `nice_*!()` logging and assertion macros log output to a
106/// centralized location and panics also get written there. By default this logs to STDERR. If a
107/// Windows debugger is attached, then messages will be sent there instead. This uses
108/// [nice-log](https://github.com/BillyDM/nice-log). See the readme there for more information.
109///
110/// In short, nice-log's behavior can be controlled by setting the `NICE_LOG` environment variable to:
111///
112/// - `stderr`, in which case the log output always gets written to STDERR.
113/// - `windbg` (only on Windows), in which case the output always gets logged using
114///   `OutputDebugString()`.
115/// - A file path, in which case the output gets appended to the end of that file which will be
116///   created if necessary.
117pub fn setup_logger<P: Plugin>() {
118    static DID_SETUP: AtomicBool = AtomicBool::new(false);
119
120    let did_setup = DID_SETUP.swap(true, std::sync::atomic::Ordering::SeqCst);
121    if !did_setup {
122        if let Some(is_ok) = P::setup_logger() {
123            if is_ok {
124                log_panics();
125            }
126
127            return;
128        }
129
130        #[cfg(feature = "tracing-subscriber")]
131        {
132            #[cfg(debug_assertions)]
133            let sub = tracing_subscriber::FmtSubscriber::builder()
134                .with_max_level(tracing::level_filters::LevelFilter::DEBUG)
135                .with_target(true)
136                .with_ansi(false)
137                .with_writer(nice_log::writer_from_env())
138                .finish();
139
140            #[cfg(not(debug_assertions))]
141            let sub = tracing_subscriber::FmtSubscriber::builder()
142                .with_max_level(tracing::level_filters::LevelFilter::INFO)
143                .with_target(false)
144                .without_time()
145                .with_ansi(false)
146                .with_writer(nice_log::writer_from_env())
147                .finish();
148
149            if tracing::subscriber::set_global_default(sub).is_ok() {
150                log_panics();
151            }
152        }
153    }
154}
155
156/// This is copied from same as the `log_panics` crate, but it's wrapped in `permit_alloc()`.
157/// Otherwise logging panics will trigger `assert_no_alloc` as this also allocates.
158fn log_panics() {
159    std::panic::set_hook(Box::new(|info| {
160        permit_alloc(|| {
161            // All of this is directly copied from `permit_no_alloc`, except that `error!()` became
162            // `nice_error!()` and `Shim` has been inlined
163            let backtrace = Backtrace::new();
164
165            let thread = std::thread::current();
166            let thread = thread.name().unwrap_or("unnamed");
167
168            let msg = match info.payload().downcast_ref::<&'static str>() {
169                Some(s) => *s,
170                None => match info.payload().downcast_ref::<String>() {
171                    Some(s) => &**s,
172                    None => "Box<Any>",
173                },
174            };
175
176            match info.location() {
177                Some(location) => {
178                    crate::nice_error!(
179                        target: "panic", "thread '{}' panicked at '{}': {}:{}\n{:?}",
180                        thread,
181                        msg,
182                        location.file(),
183                        location.line(),
184                        backtrace
185                    );
186                }
187                None => {
188                    crate::nice_error!(
189                        target: "panic",
190                        "thread '{}' panicked at '{}'\n{:?}",
191                        thread,
192                        msg,
193                        backtrace
194                    )
195                }
196            }
197        })
198    }));
199}
200
201/// A wrapper around the entire process function, including the plugin wrapper parts. This sets up
202/// `assert_no_alloc` if needed, while also making sure that things like FTZ are set up correctly if
203/// the host has not already done so.
204pub fn process_wrapper<T, F: FnOnce() -> T>(f: F) -> T {
205    // Make sure FTZ is always enabled, even if the host doesn't do it for us
206    let _ftz_guard = ScopedFtz::enable();
207
208    #[cfg(all(debug_assertions, feature = "assert_process_allocs"))]
209    return nice_assert_no_alloc::assert_no_alloc(f);
210
211    #[cfg(not(all(debug_assertions, feature = "assert_process_allocs")))]
212    return f();
213}
214
215/// Enable the CPU's Flush To Zero flag while this object is in scope. If the flag was not already
216/// set, it will be restored to its old value when this gets dropped.
217struct ScopedFtz {
218    /// Whether FTZ should be disabled again, i.e. if FTZ was not enabled before.
219    /// Only unused if not on SSE or aarch64 with the "unsafe_flush_denormals"
220    /// feature enabled.
221    #[allow(unused)]
222    should_disable_again: bool,
223    /// We can't directly implement !Send and !Sync, but this will do the same thing. This object
224    /// affects the current thread's floating point registers, so it may only be dropped on the
225    /// current thread.
226    _send_sync_marker: PhantomData<*const ()>,
227}
228
229impl ScopedFtz {
230    fn enable() -> Self {
231        #[cfg(all(not(miri), feature = "unsafe_flush_denormals"))]
232        {
233            #[cfg(target_feature = "sse")]
234            {
235                // Rust 1.75 deprecated `_mm_setcsr()` and `_MM_SET_FLUSH_ZERO_MODE()`, so this now
236                // requires inline assembly. See sections 10.2.3 (MXCSR Control and Status Register)
237                // and 10.2.3.3 (Flush-To-Zero) from this document for more details:
238                //
239                // <https://cdrdv2-public.intel.com/843823/252046-sdm-change-document-1.pdf>
240                let mut mxcsr: u32 = 0;
241                unsafe { std::arch::asm!("stmxcsr [{}]", in(reg) &mut mxcsr) };
242                let should_disable_again = mxcsr & SSE_FTZ_BIT == 0;
243                if should_disable_again {
244                    unsafe { std::arch::asm!("ldmxcsr [{}]", in(reg) &(mxcsr | SSE_FTZ_BIT)) };
245                }
246
247                return Self {
248                    should_disable_again,
249                    _send_sync_marker: PhantomData,
250                };
251            }
252
253            #[cfg(target_arch = "aarch64")]
254            {
255                // There are no convient intrinsics to change the FTZ settings on AArch64, so this
256                // requires inline assembly:
257                // https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/FPCR--Floating-point-Control-Register
258                let mut fpcr: u64;
259                unsafe { std::arch::asm!("mrs {}, fpcr", out(reg) fpcr) };
260
261                let should_disable_again = fpcr & AARCH64_FTZ_BIT == 0;
262                if should_disable_again {
263                    unsafe { std::arch::asm!("msr fpcr, {}", in(reg) fpcr | AARCH64_FTZ_BIT) };
264                }
265
266                return Self {
267                    should_disable_again,
268                    _send_sync_marker: PhantomData,
269                };
270            }
271        }
272
273        // This is only unreachable if on SSE or aarch64 with the "unsafe_flush_denormals"
274        // feature enabled.
275        #[allow(unreachable_code)]
276        Self {
277            should_disable_again: false,
278            _send_sync_marker: PhantomData,
279        }
280    }
281}
282
283impl Drop for ScopedFtz {
284    fn drop(&mut self) {
285        #[cfg(all(not(miri), feature = "unsafe_flush_denormals"))]
286        if self.should_disable_again {
287            #[cfg(target_feature = "sse")]
288            {
289                let mut mxcsr: u32 = 0;
290                unsafe { std::arch::asm!("stmxcsr [{}]", in(reg) &mut mxcsr) };
291                unsafe { std::arch::asm!("ldmxcsr [{}]", in(reg) &(mxcsr & !SSE_FTZ_BIT)) };
292            }
293
294            #[cfg(target_arch = "aarch64")]
295            {
296                let mut fpcr: u64;
297                unsafe { std::arch::asm!("mrs {}, fpcr", out(reg) fpcr) };
298                unsafe { std::arch::asm!("msr fpcr, {}", in(reg) fpcr & !AARCH64_FTZ_BIT) };
299            }
300        }
301    }
302}
303
304#[cfg(test)]
305mod miri {
306    use std::ffi::CStr;
307
308    use super::*;
309
310    #[test]
311    fn strlcpy_normal() {
312        let mut dest = [0; 256];
313        strlcpy(&mut dest, "Hello, world!");
314
315        assert_eq!(
316            unsafe { CStr::from_ptr(dest.as_ptr()) }.to_str(),
317            Ok("Hello, world!")
318        );
319    }
320
321    #[test]
322    fn strlcpy_overflow() {
323        let mut dest = [0; 6];
324        strlcpy(&mut dest, "Hello, world!");
325
326        assert_eq!(
327            unsafe { CStr::from_ptr(dest.as_ptr()) }.to_str(),
328            Ok("Hello")
329        );
330    }
331}