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