diff --git a/rust/rust-assert-no-alloc b/rust/rust-assert-no-alloc deleted file mode 160000 index 11f0f41..0000000 --- a/rust/rust-assert-no-alloc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 11f0f41123f7fcaf0ad1c23a6a17d97f4650e824 diff --git a/rust/rust-assert-no-alloc/.gitignore b/rust/rust-assert-no-alloc/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/rust/rust-assert-no-alloc/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/rust/rust-assert-no-alloc/Cargo.toml b/rust/rust-assert-no-alloc/Cargo.toml new file mode 100644 index 0000000..bbe18fb --- /dev/null +++ b/rust/rust-assert-no-alloc/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "assert_no_alloc" +version = "1.1.2" +authors = ["Florian Jung "] +edition = "2018" +license = "BSD-1-Clause" +description = "Custom Rust allocator allowing to temporarily disable memory (de)allocations for a thread. Aborts or prints a warning if allocating although forbidden." +homepage = "https://github.com/Windfisch/rust-assert-no-alloc" +repository = "https://github.com/Windfisch/rust-assert-no-alloc" +readme = "README.md" +keywords = ["allocator", "real-time", "debug", "audio"] +categories = ["development-tools::debugging"] + +[features] +default = ["disable_release"] +warn_debug = [] +warn_release = [] +disable_release = [] + +# Print a backtrace before aborting the program when an allocation failure happens +backtrace = ["dep:backtrace"] +# Use the `log` crate instead of printing to STDERR +# WARNING: If the allocation failure happens during a logger call, then +# depending on the logger's implementation this may block indefinitely +log = ["dep:log"] + +[dependencies] +backtrace = { version = "0.3", optional = true } +log = { version = "0.4", optional = true } + +[package.metadata.docs.rs] +features = ["warn_debug"] diff --git a/rust/rust-assert-no-alloc/LICENSE b/rust/rust-assert-no-alloc/LICENSE new file mode 100644 index 0000000..c108e4a --- /dev/null +++ b/rust/rust-assert-no-alloc/LICENSE @@ -0,0 +1,21 @@ +assert_no_alloc -- A custom Rust allocator allowing to temporarily disable +memory (de)allocations for a thread. + +Copyright (c) 2020 Florian Jung + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/rust/rust-assert-no-alloc/README.md b/rust/rust-assert-no-alloc/README.md new file mode 100644 index 0000000..8411b1c --- /dev/null +++ b/rust/rust-assert-no-alloc/README.md @@ -0,0 +1,143 @@ +assert_no_alloc +=============== + +This crate provides a custom allocator that allows to temporarily disable +memory (de)allocations for a thread. If a (de)allocation is attempted +anyway, the program will abort or print a warning. + +It uses thread local storage for the "disabled-flag/counter", and thus +should be thread safe, if the underlying allocator (currently hard-coded +to `std::alloc::System`) is. + +[documentation @ docs.rs](https://docs.rs/assert_no_alloc/1.1.0/assert_no_alloc/), +[crates.io](https://crates.io/crates/assert_no_alloc) + +Rationale +--------- + +No-allocation-zones are relevant e.g. in real-time scenarios like audio +callbacks. Allocation and deallocation can take unpredictable amounts of +time, and thus can *sometimes* lead to audible glitches because the audio +data is not served in time. + +Debugging such problems can be hard, because it is difficult to reproduce +such problems consistently. Avoiding such problems is also hard, since +allocation/deallocation is a common thing to do and most libraries are not +explicit whether certain functions can allocate or not. Also, this might +even depend on the run-time situation (e.g. a `Vec::push` might allocate, +but it is guaranteed to not allocate *if* enough space has been `reserve()`d +before). + +To aid the developer in tackling these problems, this crate offers an easy +way of detecting all forbidden allocations. + +How to use +---------- + +First, configure the features: `warn_debug` and `warn_release` change the +behaviour from aborting your program into just printing an error message +on `stderr`. Aborting is useful for debugging purposes, as it allows you +to retrieve a stacktrace, while warning is less intrusive. + +Note that you need to disable the (default-enabled) `disable_release` feature +by specify `default-features = false` if you want to use `warn_release`. If +`disable_release` is set (which is the default), then this crate will do +nothing if built in `--release` mode. + +Second, use the allocator provided by this crate. Add this to `main.rs`: + +```rust +use assert_no_alloc::*; + +#[cfg(debug_assertions)] // required when disable_release is set (default) +#[global_allocator] +static A: AllocDisabler = AllocDisabler; +``` + +Third, wrap code sections that may not allocate like this: + +```rust +assert_no_alloc(|| { + println!("This code can not allocate."); +}); +``` + +Advanced use +------------ + +Values can be returned using: + +```rust +let answer = assert_no_alloc(|| { 42 }); +``` + +The effect of `assert_no_alloc` can be overridden using `permit_alloc`: + +```rust +assert_no_alloc(|| { + permit_alloc(|| { + // Allocate some memory here. This will work. + }); +}); +``` + +This is useful for test stubs whose code is executed in an `assert_no_alloc` +context. + +Objects that deallocate upon `Drop` can be wrapped in `PermitDrop`: + +```rust +let foo = PermitDrop::new( + permit_alloc(|| + Box::new(...) + ) +); +``` + +Dropping `foo` will not trigger an assertion (but dropping a `Box` would). + +`assert_no_alloc()` calls can be nested, with proper panic unwinding handling. + +Note that to fully bypass this crate, e.g. when in release mode, you need to +*both* have the `disable_release` feature flag enabled (which it is by default) +and to not register `AllocDisabler` as `global_allocator`. + +Optional features +----------------- + +These compile time features are not enabled by default: + +- `backtrace` causes a backtrace to be printed before the allocation failure. + This backtrace is gathered at runtime, and its accuracy depends on the + platform and the compilation options used. +- `log` uses the `log` crate to write the allocation failure message to the + configured logger. If the `backtrace` feature is also enabled, then the + backtrace will also be written to the logger This can be useful when using a + logger that writes directly to a file or any other place that isn't STDERR. + + The main caveat here is that if the allocation was caused by the logger and if + the logger wraps its entire log function in a regular non-entrant mutex, then + this may result in a deadlock. Make sure your logger doesn't do this before + enabling this feature. + +Examples +-------- + +See [examples/main.rs](https://github.com/Windfisch/rust-assert-no-alloc/blob/master/examples/main.rs) for an example. + +You can try out the different feature flags: + +- `cargo run --example main` -> memory allocation of 4 bytes failed. Aborted (core dumped) +- `cargo run --example main --release --no-default-features` -> same as above. +- `cargo run --example main --features=warn_debug` -> Tried to (de)allocate memory in a thread that forbids allocator calls! This will not be executed if the above allocation has aborted. +- `cargo run --example main --features=warn_release --release --no-default-features` -> same as above. +- `cargo run --example main --release` will not even check for forbidden allocations + +Test suite +---------- + +The tests will fail to compile with the default features. Run them using: + +``` +cargo test --features=warn_debug --tests +``` diff --git a/rust/rust-assert-no-alloc/examples/main.rs b/rust/rust-assert-no-alloc/examples/main.rs new file mode 100644 index 0000000..7f43d57 --- /dev/null +++ b/rust/rust-assert-no-alloc/examples/main.rs @@ -0,0 +1,34 @@ +use assert_no_alloc::*; + +#[cfg(debug_assertions)] +#[global_allocator] +static A: AllocDisabler = AllocDisabler; + +fn main() { + println!("Alloc is allowed. Let's allocate some memory..."); + let mut vec_can_push = Vec::new(); + vec_can_push.push(42); + + println!(); + + let fib5 = assert_no_alloc(|| { + println!("Alloc is forbidden. Let's calculate something without memory allocations..."); + + fn fib(n: u32) -> u32 { + if n<=1 { 1 } + else { fib(n-1) + fib(n-2) } + } + + fib(5) + }); + println!("\tSuccess, the 5th fibonacci number is {}", fib5); + println!(); + + assert_no_alloc(|| { + println!("Alloc is forbidden. Let's allocate some memory..."); + let mut vec_cannot_push = Vec::new(); + vec_cannot_push.push(42); // panics + }); + + println!("This will not be executed if the above allocation has aborted."); +} diff --git a/rust/rust-assert-no-alloc/src/lib.rs b/rust/rust-assert-no-alloc/src/lib.rs new file mode 100644 index 0000000..aa42238 --- /dev/null +++ b/rust/rust-assert-no-alloc/src/lib.rs @@ -0,0 +1,259 @@ +/* assert_no_alloc -- A custom Rust allocator allowing to temporarily disable + * memory (de)allocations for a thread. + * + * Copyright (c) 2020 Florian Jung + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#![doc = include_str!("../README.md")] + +use std::alloc::{System,GlobalAlloc,Layout}; +use std::cell::Cell; + +// check for mutually exclusive features. +#[cfg(all(feature = "disable_release", feature = "warn_release"))] +compile_error!("disable_release cannot be active at the same time with warn_release"); + + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +thread_local! { + static ALLOC_FORBID_COUNT: Cell = Cell::new(0); + static ALLOC_PERMIT_COUNT: Cell = Cell::new(0); + + #[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] + static ALLOC_VIOLATION_COUNT: Cell = Cell::new(0); +} + +#[cfg(all(feature = "disable_release", not(debug_assertions)))] // if disabled +pub fn assert_no_alloc T> (func: F) -> T { // no-op + func() +} + +#[cfg(all(feature = "disable_release", not(debug_assertions)))] // if disabled +pub fn permit_alloc T> (func: F) -> T { // no-op + func() +} + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +/// Calls the `func` closure, but forbids any (de)allocations. +/// +/// If a call to the allocator is made, the program will abort with an error, +/// print a warning (depending on the `warn_debug` feature flag. Or ignore +/// the situation, when compiled in `--release` mode with the `disable_release` +///feature flag set (which is the default)). +pub fn assert_no_alloc T> (func: F) -> T { + // RAII guard for managing the forbid counter. This is to ensure correct behaviour + // when catch_unwind is used + struct Guard; + impl Guard { + fn new() -> Guard { + ALLOC_FORBID_COUNT.with(|c| c.set(c.get()+1)); + Guard + } + } + impl Drop for Guard { + fn drop(&mut self) { + ALLOC_FORBID_COUNT.with(|c| c.set(c.get()-1)); + } + } + + + #[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] // if warn mode is selected + let old_violation_count = violation_count(); + + + let guard = Guard::new(); // increment the forbid counter + let ret = func(); + std::mem::drop(guard); // decrement the forbid counter + + + #[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] // if warn mode is selected + if violation_count() > old_violation_count { + eprintln!("Tried to (de)allocate memory in a thread that forbids allocator calls!"); + } + + return ret; +} + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +/// Calls the `func` closure, but ensures that the forbid and permit counters +/// are maintained accurately even if a longjmp originates and terminates +/// within the closure. If you longjmp over this function, we can't fix +/// anything about it. +pub fn ensure_alloc_counters T> (func: F) -> T { + let forbid_counter = ALLOC_FORBID_COUNT.with(|c| c.get()); + let permit_counter = ALLOC_PERMIT_COUNT.with(|c| c.get()); + + let ret = func(); + + ALLOC_FORBID_COUNT.with(|c| c.set(forbid_counter)); + ALLOC_PERMIT_COUNT.with(|c| c.set(permit_counter)); + + return ret; +} + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +/// Calls the `func` closure. Allocations are temporarily allowed, even if this +/// code runs inside of assert_no_alloc. +pub fn permit_alloc T> (func: F) -> T { + // RAII guard for managing the permit counter + struct Guard; + impl Guard { + fn new() -> Guard { + ALLOC_PERMIT_COUNT.with(|c| c.set(c.get()+1)); + Guard + } + } + impl Drop for Guard { + fn drop(&mut self) { + ALLOC_PERMIT_COUNT.with(|c| c.set(c.get()-1)); + } + } + + let guard = Guard::new(); // increment the forbid counter + let ret = func(); + std::mem::drop(guard); // decrement the forbid counter + + return ret; +} + +#[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] // if warn mode is selected +/// Returns the count of allocation warnings emitted so far. +/// +/// Only available when the `warn_debug` or `warn release` features are enabled. +pub fn violation_count() -> u32 { + ALLOC_VIOLATION_COUNT.with(|c| c.get()) +} + +#[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] // if warn mode is selected +/// Resets the count of allocation warnings to zero. +/// +/// Only available when the `warn_debug` or `warn release` features are enabled. +pub fn reset_violation_count() { + ALLOC_VIOLATION_COUNT.with(|c| c.set(0)); +} + + + + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +/// The custom allocator that handles the checking. +/// +/// To use this crate, you must add the following in your `main.rs`: +/// ```rust +/// use assert_no_alloc::*; +/// // ... +/// #[cfg(debug_assertions)] +/// #[global_allocator] +/// static A: AllocDisabler = AllocDisabler; +/// ``` +pub struct AllocDisabler; + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +impl AllocDisabler { + fn check(&self, layout: Layout) { + let forbid_count = ALLOC_FORBID_COUNT.with(|f| f.get()); + let permit_count = ALLOC_PERMIT_COUNT.with(|p| p.get()); + if forbid_count > 0 && permit_count == 0 { + #[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))] // if warn mode is selected + ALLOC_VIOLATION_COUNT.with(|c| c.set(c.get()+1)); + + #[cfg(any( all(not(feature="warn_debug"), debug_assertions), all(not(feature="warn_release"), not(debug_assertions)) ))] // if abort mode is selected + { + #[cfg(all(feature = "log", feature = "backtrace"))] + permit_alloc(|| log::error!("Memory allocation of {} bytes failed from:\n{:?}", layout.size(), backtrace::Backtrace::new())); + #[cfg(all(feature = "log", not(feature = "backtrace")))] + permit_alloc(|| log::error!("Memory allocation of {} bytes failed", layout.size())); + + #[cfg(all(not(feature = "log"), feature = "backtrace"))] + permit_alloc(|| eprintln!("Allocation failure from:\n{:?}", backtrace::Backtrace::new())); + + // This handler can be overridden (although as of writing, the API to do so is still + // unstable) so we must always call this even when the log feature is enabled + std::alloc::handle_alloc_error(layout); + } + } + } +} + +#[cfg(not(all(feature = "disable_release", not(debug_assertions))))] // if not disabled +unsafe impl GlobalAlloc for AllocDisabler { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + self.check(layout); + System.alloc(layout) + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + self.check(layout); + System.dealloc(ptr, layout) + } +} + +/// Wrapper for objects whose Drop implementation shall be permitted +/// to (de)allocate. +/// +/// Typical usage: +/// +/// ```rust +/// let foo = PermitDrop::new( +/// permit_alloc(|| +/// Box::new(...) +/// ) +/// ); +/// ``` +/// +/// Here, creation of the Box is guarded by the explicit `permit_alloc` call, +/// and destruction of the Box is guarded by PermitDrop. Neither creation nor +/// destruction will cause an assertion failure from within `assert_no_alloc`. +pub struct PermitDrop(Option); + +impl PermitDrop { + pub fn new(t: T) -> PermitDrop { + permit_alloc(|| { + PermitDrop(Some(t)) + }) + } +} + +impl std::ops::Deref for PermitDrop { + type Target = T; + fn deref(&self) -> &T { self.0.as_ref().unwrap() } +} + +impl std::ops::DerefMut for PermitDrop { + fn deref_mut(&mut self) -> &mut T { self.0.as_mut().unwrap() } +} + +impl Iterator for PermitDrop { + type Item = I::Item; + fn next(&mut self) -> Option { + (**self).next() + } +} + + +impl Drop for PermitDrop { + fn drop(&mut self) { + let mut tmp = None; + std::mem::swap(&mut tmp, &mut self.0); + permit_alloc(|| { + std::mem::drop(tmp); + }); + } +} diff --git a/rust/rust-assert-no-alloc/tests/test.rs b/rust/rust-assert-no-alloc/tests/test.rs new file mode 100644 index 0000000..d5f6c8d --- /dev/null +++ b/rust/rust-assert-no-alloc/tests/test.rs @@ -0,0 +1,148 @@ +use assert_no_alloc::*; +use std::panic::catch_unwind; + +#[global_allocator] +static A: AllocDisabler = AllocDisabler; + +#[cfg(not(feature = "warn_debug"))] +compile_error!("The test suite requires the warn_debug feature to be enabled. Use `cargo test --features warn_debug`"); + +// This is only a kludge; what we actually want to check is "will do_alloc() be optimized out?", e.g. due to +// compiler optimizations turned on in --release mode. We can't do that, the closest we can get is to check +// whether debug_assertions are disabled, which coincidentially also happens in release mode. +#[cfg(not(debug_assertions))] +compile_error!("The test suite only works in debug mode. Use `cargo test --features warn_debug`"); + +#[cfg(feature = "warn_debug")] +fn check_and_reset() -> bool { + let result = violation_count() > 0; + reset_violation_count(); + result +} + +// Provide a stub check_and_reset() function if warn_debug is disabled. This will never be compiled due to the +// compile_error!() above, but this stub ensures that the output will not be cluttered with spurious error +// messages. +#[cfg(not(feature = "warn_debug"))] +fn check_and_reset() -> bool { unreachable!() } + +fn do_alloc() { + let _tmp: Box = Box::new(42); +} + +#[test] +fn ok_noop() { + assert_eq!(check_and_reset(), false); + do_alloc(); + assert_eq!(check_and_reset(), false); +} + +#[test] +fn ok_simple() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + }); + + do_alloc(); + assert_eq!(check_and_reset(), false); +} + +#[test] +fn ok_nested() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + assert_no_alloc(|| { + }); + }); + + do_alloc(); + assert_eq!(check_and_reset(), false); +} + +#[test] +fn forbidden_simple() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + do_alloc(); + }); + assert_eq!(check_and_reset(), true); +} + +#[test] +fn forbidden_in_nested() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + assert_no_alloc(|| { + do_alloc(); + }); + }); + assert_eq!(check_and_reset(), true); +} + +#[test] +fn forbidden_after_nested() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + assert_no_alloc(|| { + }); + do_alloc(); + }); + assert_eq!(check_and_reset(), true); +} + +#[test] +fn unwind_ok() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + let r = catch_unwind(|| { + assert_no_alloc(|| { + panic!(); + }); + }); + assert!(r.is_err()); + }); + check_and_reset(); // unwinding might have allocated memory; we don't care about that. + do_alloc(); + assert_eq!(check_and_reset(), false); +} + +#[test] +fn unwind_nested() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + let r = catch_unwind(|| { + assert_no_alloc(|| { + panic!(); + }); + }); + assert!(r.is_err()); + + check_and_reset(); // unwinding might have allocated memory; we don't care about that. + do_alloc(); + assert_eq!(check_and_reset(), true); + }); +} + +#[test] +fn unwind_nested2() { + assert_eq!(check_and_reset(), false); + assert_no_alloc(|| { + assert_no_alloc(|| { + let r = catch_unwind(|| { + assert_no_alloc(|| { + assert_no_alloc(|| { + panic!(); + }); + }); + }); + assert!(r.is_err()); + + check_and_reset(); // unwinding might have allocated memory; we don't care about that. + do_alloc(); + assert_eq!(check_and_reset(), true); + }); + }); + check_and_reset(); // unwinding might have allocated memory; we don't care about that. + do_alloc(); + assert_eq!(check_and_reset(), false); +}