Renaming and a new macro

pull/1/head
Bradlee Speice 2018-12-06 23:02:44 -05:00
parent a1ee8934b4
commit 65673e1af2
8 changed files with 164 additions and 63 deletions

View File

@ -8,15 +8,53 @@
--- ---
# The Quick And Dirty Allocation Profiling Tool # QADAPT - `debug_assert!` for your memory
This allocator is a helper for writing high-performance code that is allocation/drop free; This allocator is a helper for writing high-performance code that is memory-sensitive;
for functions annotated with `#[allocate_panic]`, QADAPT will detect when allocations/drops a thread panic will be triggered if a function annotated with `#[no_alloc]`,
happen during their execution (and execution of any functions they call) and throw a or code inside an `assert_no_alloc!` macro interacts with the allocator in any way.
thread panic if this occurs. QADAPT-related code is *stripped out during release builds*, Wanton allocations and unforeseen drops no more - this library lets you focus on
so no worries about random allocations crashing in production. writing code without worrying if Rust properly managed to inline the variable into the stack.
Currently this crate is Nightly-only, but will work once `const fn` is in Stable. Now, an allocator blowing up in production is a scary thought; that's why QADAPT
is designed to strip its own code out whenever you're running with a release build.
Just like the [`debug_assert!` macro](https://doc.rust-lang.org/std/macro.debug_assert.html)
in Rust's standard library, it's safe to use without worrying about a unforeseen
circumstance causing your application to crash.
Please also take a look at [qadapt-macro](https://github.com/bspeice/qadapt/tree/master/qadapt-macro) # Usage
for some helper macros to make working with QADAPT a bit easier.
Actually making use of QADAPT is straight-forward. To set up the allocator,
place the following snippet in either your program binaries (main.rs) or tests:
```rust,ignore
use qadapt::QADAPT;
#[global_allocator]
static Q: QADAPT = QADAPT;
```
After that, there are two ways of telling QADAPT that it should trigger a panic:
1. Annotate functions with the `#[no_alloc]` proc macro:
```rust,no_run
use qadapt::no_alloc;
#[no_alloc]
fn do_math() -> u8 {
2 + 2
}
```
2. Evaluate expressions with the `assert_no_alloc!` macro
```rust,no_run
use qadapt::assert_no_alloc;
fn do_work() {
// This code is allowed to trigger an allocation
let b = Box::new(8);
// This code would panic if an allocation occurred inside it
let x = assert_no_alloc!(*b + 2);
assert_eq!(x, 10);
}

View File

@ -1,10 +1,10 @@
use qadapt::allocate_panic; use qadapt::no_alloc;
use qadapt::QADAPT; use qadapt::QADAPT;
#[global_allocator] #[global_allocator]
static Q: QADAPT = QADAPT; static Q: QADAPT = QADAPT;
#[allocate_panic] #[no_alloc]
fn does_allocate() -> Box<u8> { fn does_allocate() -> Box<u8> {
Box::new(0) Box::new(0)
} }

View File

@ -1,20 +0,0 @@
use env_logger;
use qadapt::allocate_panic;
// Note that we're missing the `#[global_allocator]` attribute
#[allocate_panic]
fn does_allocate() -> Box<u8> {
Box::new(0)
}
fn main() {
// This code will warn that QADAPT isn't being used, but won't trigger a panic.
// Run with `RUST_LOG=warn cargo run --example setup_warning`
env_logger::init();
does_allocate();
// The warning will only trigger once though
does_allocate();
}

View File

@ -155,7 +155,7 @@ fn escape_return(ts: TokenStream) -> TokenStream {
/// separate thread, or defers allocations via closure/Future, those results /// separate thread, or defers allocations via closure/Future, those results
/// will not trigger an error. /// will not trigger an error.
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn allocate_panic(_attr: TokenStream, item: TokenStream) -> TokenStream { pub fn no_alloc(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut protected_fn: Vec<TokenTree> = Vec::new(); let mut protected_fn: Vec<TokenTree> = Vec::new();
let mut item_iter = item.into_iter(); let mut item_iter = item.into_iter();

View File

@ -1,15 +1,53 @@
//! # The Quick And Dirty Allocation Profiling Tool //! # QADAPT - `debug_assert!` for your memory
//! //!
//! This allocator is a helper for writing high-performance code that is allocation/drop free; //! This allocator is a helper for writing high-performance code that is memory-sensitive;
//! for functions annotated with `#[allocate_panic]`, QADAPT will detect when allocations/drops //! a thread panic will be triggered if a function annotated with `#[no_alloc]`,
//! happen during their execution (and execution of any functions they call) and throw a //! or code inside an `assert_no_alloc!` macro interacts with the allocator in any way.
//! thread panic if this occurs. QADAPT-related code is *stripped out during release builds*, //! Wanton allocations and unforeseen drops no more - this library lets you focus on
//! so no worries about random allocations crashing in production. //! writing code without worrying if Rust properly managed to inline the variable into the stack.
//! //!
//! Currently this crate is Nightly-only, but will work once `const fn` is in Stable. //! Now, an allocator blowing up in production is a scary thought; that's why QADAPT
//! //! is designed to strip its own code out whenever you're running with a release build.
//! Please also take a look at [qadapt-macro](https://github.com/bspeice/qadapt/tree/master/qadapt-macro) //! Just like the [`debug_assert!` macro](https://doc.rust-lang.org/std/macro.debug_assert.html)
//! for some helper macros to make working with QADAPT a bit easier. //! in Rust's standard library, it's safe to use without worrying about a unforeseen
//! circumstance causing your application to crash.
//!
//! # Usage
//!
//! Actually making use of QADAPT is straight-forward. To set up the allocator,
//! place the following snippet in either your program binaries (main.rs) or tests:
//!
//! ```rust,ignore
//! use qadapt::QADAPT;
//!
//! #[global_allocator]
//! static Q: QADAPT = QADAPT;
//! ```
//!
//! After that, there are two ways of telling QADAPT that it should trigger a panic:
//!
//! 1. Annotate functions with the `#[no_alloc]` proc macro:
//! ```rust,no_run
//! use qadapt::no_alloc;
//!
//! #[no_alloc]
//! fn do_math() -> u8 {
//! 2 + 2
//! }
//! ```
//!
//! 2. Evaluate expressions with the `assert_no_alloc!` macro
//! ```rust,no_run
//! use qadapt::assert_no_alloc;
//!
//! fn do_work() {
//! // This code is allowed to trigger an allocation
//! let b = Box::new(8);
//!
//! // This code would panic if an allocation occurred inside it
//! let x = assert_no_alloc!(*b + 2);
//! assert_eq!(x, 10);
//! }
#![deny(missing_docs)] #![deny(missing_docs)]
use log::warn; use log::warn;
@ -45,8 +83,7 @@ pub fn enter_protected() {
} }
if !*IS_ACTIVE.read() { if !*IS_ACTIVE.read() {
*IS_ACTIVE.write() = true; panic!("QADAPT not initialized when using allocation guards; please verify `#[global_allocator]` is set!");
warn!("QADAPT not initialized when using allocation guards; please verify `#[global_allocator]` is set!");
} }
PROTECTION_LEVEL PROTECTION_LEVEL
@ -80,17 +117,30 @@ pub fn exit_protected() {
} }
} }
/// Get the result of an expression, guaranteeing that no allocations occur
/// during its evaluation.
///
/// **Warning**: Unexpected behavior may occur when using the `return` keyword.
/// Because the macro cleanup logic will not be run, QADAPT may trigger a panic
/// in code that was not specifically intended to be allocation-free.
#[macro_export]
macro_rules! assert_no_alloc {
($e:expr) => {{
::qadapt::enter_protected();
let e = { $e };
::qadapt::exit_protected();
e
}};
}
static IS_ACTIVE: RwLock<bool> = RwLock::new(false); static IS_ACTIVE: RwLock<bool> = RwLock::new(false);
static INTERNAL_ALLOCATION: RwLock<usize> = RwLock::new(usize::max_value()); static INTERNAL_ALLOCATION: RwLock<usize> = RwLock::new(usize::max_value());
/// Get the current "protection level" in QADAPT: calls to enter_protected() - exit_protected() /// Get the current "protection level" in QADAPT: calls to enter_protected() - exit_protected()
pub fn protection_level() -> usize { pub fn protection_level() -> usize {
#[cfg(debug_assertions)] if cfg!(debug_assertions) {
{
PROTECTION_LEVEL.try_with(|v| *v.read()).unwrap_or(0) PROTECTION_LEVEL.try_with(|v| *v.read()).unwrap_or(0)
} } else {
#[cfg(not(debug_assertions))]
{
0 0
} }
} }
@ -131,7 +181,7 @@ unsafe impl GlobalAlloc for QADAPT {
} }
// Because accessing PROTECTION_LEVEL has the potential to trigger an allocation, // Because accessing PROTECTION_LEVEL has the potential to trigger an allocation,
// we need to spin until we can claim the INTERNAL_ALLOCATION lock for our thread. // we need to acquire the INTERNAL_ALLOCATION lock for our thread.
claim_internal_alloc(); claim_internal_alloc();
let protection_level: Result<usize, ()> = let protection_level: Result<usize, ()> =
PROTECTION_LEVEL.try_with(|v| *v.read()).or(Ok(0)); PROTECTION_LEVEL.try_with(|v| *v.read()).or(Ok(0));
@ -167,7 +217,7 @@ unsafe impl GlobalAlloc for QADAPT {
free(ptr as *mut c_void); free(ptr as *mut c_void);
match protection_level { match protection_level {
Ok(v) if v > 0 => { Ok(v) if v > 0 => {
// Tripped a bad dealloc, but make sure further memory access during unwind // Tripped a bad drop, but make sure further memory access during unwind
// doesn't have issues // doesn't have issues
PROTECTION_LEVEL.with(|v| *v.write() = 0); PROTECTION_LEVEL.with(|v| *v.write() = 0);
panic!( panic!(

26
tests/assert_macro.rs Normal file
View File

@ -0,0 +1,26 @@
use qadapt::assert_no_alloc;
use qadapt::QADAPT;
#[global_allocator]
static Q: QADAPT = QADAPT;
#[test]
fn math() {
let x = assert_no_alloc!(2 + 2);
assert_eq!(x, 4);
}
fn early_return() -> usize {
assert_no_alloc!(return 8)
}
fn into_box() -> Box<usize> {
Box::new(early_return())
}
#[test]
#[should_panic]
fn early_return_boxing() {
into_box();
}

View File

@ -1,12 +1,12 @@
use std::io; use std::io;
use qadapt::allocate_panic; use qadapt::no_alloc;
use qadapt::QADAPT; use qadapt::QADAPT;
#[global_allocator] #[global_allocator]
static Q: QADAPT = QADAPT; static Q: QADAPT = QADAPT;
#[allocate_panic] #[no_alloc]
fn no_allocate() { fn no_allocate() {
let _v: Vec<()> = Vec::with_capacity(0); let _v: Vec<()> = Vec::with_capacity(0);
} }
@ -16,7 +16,7 @@ fn macro_no_allocate() {
no_allocate(); no_allocate();
} }
#[allocate_panic] #[no_alloc]
fn allocates() { fn allocates() {
assert_eq!(::qadapt::protection_level(), 1); assert_eq!(::qadapt::protection_level(), 1);
// Without boxing, release profile can actually optimize out the allocation // Without boxing, release profile can actually optimize out the allocation
@ -30,7 +30,7 @@ fn macro_allocates() {
allocates(); allocates();
} }
#[allocate_panic] #[no_alloc]
fn no_allocate_ret() -> bool { fn no_allocate_ret() -> bool {
return true; return true;
} }
@ -40,7 +40,7 @@ fn macro_return() {
assert!(no_allocate_ret()); assert!(no_allocate_ret());
} }
#[allocate_panic] #[no_alloc]
fn no_allocate_implicit_ret() -> bool { fn no_allocate_implicit_ret() -> bool {
true true
} }
@ -50,7 +50,7 @@ fn macro_implicit_return() {
assert!(no_allocate_implicit_ret()); assert!(no_allocate_implicit_ret());
} }
#[allocate_panic] #[no_alloc]
fn no_allocate_arg(b: bool) -> bool { fn no_allocate_arg(b: bool) -> bool {
b b
} }
@ -61,7 +61,7 @@ fn macro_allocate_arg() {
no_allocate_arg(false); no_allocate_arg(false);
} }
#[allocate_panic] #[no_alloc]
fn no_allocate_args(_b: bool, _u: usize, i: i64) -> i64 { fn no_allocate_args(_b: bool, _u: usize, i: i64) -> i64 {
i i
} }
@ -72,7 +72,7 @@ fn macro_allocate_args() {
no_allocate_args(false, 4, -90); no_allocate_args(false, 4, -90);
} }
#[allocate_panic] #[no_alloc]
fn return_result(r: Result<usize, io::Error>) -> Result<Result<usize, io::Error>, ()> { fn return_result(r: Result<usize, io::Error>) -> Result<Result<usize, io::Error>, ()> {
Ok(r) Ok(r)
} }
@ -82,7 +82,7 @@ fn macro_return_result() {
return_result(Ok(16)).unwrap().unwrap(); return_result(Ok(16)).unwrap().unwrap();
} }
#[allocate_panic] #[no_alloc]
fn branching_return(a: bool, b: bool, c: bool) -> u8 { fn branching_return(a: bool, b: bool, c: bool) -> u8 {
if a { if a {
if b { if b {
@ -131,7 +131,7 @@ fn run_closure(x: impl Fn(bool, bool) -> bool) -> bool {
x(true, false) x(true, false)
} }
#[allocate_panic] #[no_alloc]
fn example_closure() { fn example_closure() {
let c = run_closure(|a: bool, b| return a && b); let c = run_closure(|a: bool, b| return a && b);
assert!(!c); assert!(!c);
@ -145,7 +145,7 @@ fn macro_closure() {
} }
#[test] #[test]
#[allocate_panic] #[no_alloc]
fn macro_release_safe() { fn macro_release_safe() {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {

7
tests/unused_panic.rs Normal file
View File

@ -0,0 +1,7 @@
use qadapt::enter_protected;
#[test]
#[should_panic]
fn guard_without_initialization() {
enter_protected();
}