wip: add rust-assert-no-alloc

This commit is contained in:
Matthew LeVan 2024-01-30 15:29:59 -05:00
parent 680d2d254c
commit dd70aadf6e
8 changed files with 639 additions and 1 deletions

@ -1 +0,0 @@
Subproject commit 11f0f41123f7fcaf0ad1c23a6a17d97f4650e824

2
rust/rust-assert-no-alloc/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

View File

@ -0,0 +1,32 @@
[package]
name = "assert_no_alloc"
version = "1.1.2"
authors = ["Florian Jung <flo@windfis.ch>"]
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"]

View File

@ -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 <flo@windfis.ch>
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.

View File

@ -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
```

View File

@ -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.");
}

View File

@ -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 <flo@windfis.ch>
*
* 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<u32> = Cell::new(0);
static ALLOC_PERMIT_COUNT: Cell<u32> = Cell::new(0);
#[cfg(any( all(feature="warn_debug", debug_assertions), all(feature="warn_release", not(debug_assertions)) ))]
static ALLOC_VIOLATION_COUNT: Cell<u32> = Cell::new(0);
}
#[cfg(all(feature = "disable_release", not(debug_assertions)))] // if disabled
pub fn assert_no_alloc<T, F: FnOnce() -> T> (func: F) -> T { // no-op
func()
}
#[cfg(all(feature = "disable_release", not(debug_assertions)))] // if disabled
pub fn permit_alloc<T, F: FnOnce() -> 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, F: FnOnce() -> 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, F: FnOnce() -> 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, F: FnOnce() -> 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<T>(Option<T>);
impl<T> PermitDrop<T> {
pub fn new(t: T) -> PermitDrop<T> {
permit_alloc(|| {
PermitDrop(Some(t))
})
}
}
impl<T> std::ops::Deref for PermitDrop<T> {
type Target = T;
fn deref(&self) -> &T { self.0.as_ref().unwrap() }
}
impl<T> std::ops::DerefMut for PermitDrop<T> {
fn deref_mut(&mut self) -> &mut T { self.0.as_mut().unwrap() }
}
impl<I: Iterator> Iterator for PermitDrop<I> {
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
(**self).next()
}
}
impl<T> Drop for PermitDrop<T> {
fn drop(&mut self) {
let mut tmp = None;
std::mem::swap(&mut tmp, &mut self.0);
permit_alloc(|| {
std::mem::drop(tmp);
});
}
}

View File

@ -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<u32> = 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);
}