Skip to main content

nice_plug/wrapper/
util.rs

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