diff --git a/.travis.yml b/.travis.yml index a97bb7d..6a01503 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,11 +23,16 @@ matrix: # Uniquely identifies the build that uploads to crates.io - env: CRATESIO=TRUE rust: nightly - - rust: nightly - # To build against specific Rust versions, include an item like the following: - # - rust: 1.30.0 + - rust: 1.31.0 - rust: nightly os: osx + - rust: stable + os: osx + - rust: beta + os: osx + - rust: nightly + - rust: stable + - rust: beta before_install: - set -e diff --git a/Cargo.toml b/Cargo.toml index cab42cb..143e764 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,17 +13,14 @@ categories = [ "memory-management" ] repository = "https://github.com/bspeice/qadapt.git" +edition = "2018" [badges] maintenance = { status = "actively-developed" } [dependencies] libc = "0.2" -log = "0.4" -spin = "0.4" +spin = { git = "https://github.com/bspeice/spin-rs.git" } thread-id = "3.3" qadapt-macro = { version = "0.7.1", path = "./qadapt-macro" } - -[dev-dependencies] -env_logger = "0.6" \ No newline at end of file diff --git a/Makefile b/Makefile index 16d0d47..8fc2aa7 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ readme: README.md README.md: src/lib.rs @sed -i '/---/q' README.md - @cat src/lib.rs | grep '//!' | sed 's/^\/\/\! *//g' >> README.md + @cat src/lib.rs | grep '//!' | sed -E 's/^\/\/\! ?//g' >> README.md .PHONY: doc doc: readme contributors diff --git a/README.md b/README.md index 625faf2..52790f5 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,54 @@ [![travisci](https://travis-ci.org/bspeice/qadapt.svg?branch=master)](https://travis-ci.org/bspeice/qadapt) [![appveyor](https://ci.appveyor.com/api/projects/status/km1p081tkjcptn1w/branch/master?svg=true)](https://ci.appveyor.com/project/bspeice/qadapt/branch/master) - --- -# The Quick And Dirty Allocation Profiling Tool +## `debug_assert!` for your memory usage -This allocator is a helper for writing high-performance code that is allocation/drop free; -for functions annotated with `#[allocate_panic]`, QADAPT will detect when allocations/drops -happen during their execution (and execution of any functions they call) and throw a -thread panic if this occurs. QADAPT-related code is *stripped out during release builds*, -so no worries about random allocations crashing in production. +This allocator is a helper for writing high-performance code that is memory-sensitive; +a thread panic will be triggered if a function annotated with `#[no_alloc]`, +or code inside an `assert_no_alloc!` macro interacts with the allocator in any way. +Wanton allocations and unforeseen drops no more - this library lets you focus on +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) -for some helper macros to make working with QADAPT a bit easier. +# 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); +} diff --git a/appveyor.yml b/appveyor.yml index 6400c44..22b79fe 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,6 +11,10 @@ environment: # RUST_VERSION: nightly - TARGET: x86_64-pc-windows-msvc RUST_VERSION: nightly + - TARGET: x86_64-pc-windows-msvc + RUST_VERSION: stable + - TARGET: x86_64-pc-windows-msvc + RUST_VERSION: beta install: - ps: >- diff --git a/examples/release_mode.rs b/examples/release_mode.rs index a184e07..027f6ac 100644 --- a/examples/release_mode.rs +++ b/examples/release_mode.rs @@ -1,12 +1,10 @@ -extern crate qadapt; - -use qadapt::allocate_panic; +use qadapt::no_alloc; use qadapt::QADAPT; #[global_allocator] static Q: QADAPT = QADAPT; -#[allocate_panic] +#[no_alloc] fn does_allocate() -> Box { Box::new(0) } diff --git a/examples/setup_warning.rs b/examples/setup_warning.rs deleted file mode 100644 index fdc8e61..0000000 --- a/examples/setup_warning.rs +++ /dev/null @@ -1,21 +0,0 @@ -extern crate env_logger; -extern crate qadapt; - -use qadapt::allocate_panic; - -// Note that we're missing the `#[global_allocator]` attribute - -#[allocate_panic] -fn does_allocate() -> Box { - 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(); -} diff --git a/qadapt-macro/Cargo.toml b/qadapt-macro/Cargo.toml index 94f6842..580947f 100644 --- a/qadapt-macro/Cargo.toml +++ b/qadapt-macro/Cargo.toml @@ -13,6 +13,7 @@ categories = [ "memory-management" ] repository = "https://github.com/bspeice/qadapt.git" +edition = "2018" [badges] maintenance = { status = "actively-developed" } diff --git a/qadapt-macro/src/lib.rs b/qadapt-macro/src/lib.rs index 70c78a1..4f14c75 100644 --- a/qadapt-macro/src/lib.rs +++ b/qadapt-macro/src/lib.rs @@ -111,7 +111,7 @@ fn escape_return(ts: TokenStream) -> TokenStream { let mut tt_iter = ts.into_iter(); while let Some(tt) = tt_iter.next() { - let mut tokens = match tt { + let tokens = match tt { TokenTree::Group(ref g) if g.delimiter() == Delimiter::Brace && !in_closure => { vec![group!(Delimiter::Brace, escape_return(g.stream()))] } @@ -155,7 +155,7 @@ fn escape_return(ts: TokenStream) -> TokenStream { /// separate thread, or defers allocations via closure/Future, those results /// will not trigger an error. #[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 = Vec::new(); let mut item_iter = item.into_iter(); diff --git a/src/lib.rs b/src/lib.rs index b11062e..94a6b36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,24 +1,58 @@ -//! # The Quick And Dirty Allocation Profiling Tool +//! ## `debug_assert!` for your memory usage //! -//! This allocator is a helper for writing high-performance code that is allocation/drop free; -//! for functions annotated with `#[allocate_panic]`, QADAPT will detect when allocations/drops -//! happen during their execution (and execution of any functions they call) and throw a -//! thread panic if this occurs. QADAPT-related code is *stripped out during release builds*, -//! so no worries about random allocations crashing in production. +//! This allocator is a helper for writing high-performance code that is memory-sensitive; +//! a thread panic will be triggered if a function annotated with `#[no_alloc]`, +//! or code inside an `assert_no_alloc!` macro interacts with the allocator in any way. +//! Wanton allocations and unforeseen drops no more - this library lets you focus on +//! 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) -//! for some helper macros to make working with QADAPT a bit easier. +//! # 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)] -extern crate libc; -#[macro_use] -extern crate log; -extern crate qadapt_macro; -extern crate spin; + // thread_id is necessary because `std::thread::current()` panics if we have not yet // allocated a `thread_local!{}` it depends on. -extern crate thread_id; +use thread_id; // Re-export the proc macros to use by other code pub use qadapt_macro::*; @@ -47,9 +81,8 @@ pub fn enter_protected() { return; } - if *IS_ACTIVE.read() == false { - *IS_ACTIVE.write() = true; - warn!("QADAPT not initialized when using allocation guards; please verify `#[global_allocator]` is set!"); + if !*IS_ACTIVE.read() { + panic!("QADAPT not initialized when using allocation guards; please verify `#[global_allocator]` is set!"); } PROTECTION_LEVEL @@ -83,17 +116,30 @@ pub fn exit_protected() { } } +/// Get the result of an expression, guaranteeing that no memory accesses 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 = RwLock::new(false); static INTERNAL_ALLOCATION: RwLock = RwLock::new(usize::max_value()); /// Get the current "protection level" in QADAPT: calls to enter_protected() - exit_protected() pub fn protection_level() -> usize { - #[cfg(debug_assertions)] - { + if cfg!(debug_assertions) { PROTECTION_LEVEL.try_with(|v| *v.read()).unwrap_or(0) - } - #[cfg(not(debug_assertions))] - { + } else { 0 } } @@ -123,7 +169,7 @@ fn alloc_immediate() -> bool { unsafe impl GlobalAlloc for QADAPT { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - if *IS_ACTIVE.read() == false { + if !*IS_ACTIVE.read() { *IS_ACTIVE.write() = true; } @@ -134,7 +180,7 @@ unsafe impl GlobalAlloc for QADAPT { } // 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(); let protection_level: Result = PROTECTION_LEVEL.try_with(|v| *v.read()).or(Ok(0)); @@ -170,7 +216,7 @@ unsafe impl GlobalAlloc for QADAPT { free(ptr as *mut c_void); match protection_level { 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 PROTECTION_LEVEL.with(|v| *v.write() = 0); panic!( diff --git a/tests/allocations.rs b/tests/allocations.rs index 474be54..d279c6a 100644 --- a/tests/allocations.rs +++ b/tests/allocations.rs @@ -1,5 +1,3 @@ -extern crate qadapt; - use qadapt::enter_protected; use qadapt::exit_protected; use qadapt::protection_level; diff --git a/tests/assert_macro.rs b/tests/assert_macro.rs new file mode 100644 index 0000000..03f98ec --- /dev/null +++ b/tests/assert_macro.rs @@ -0,0 +1,30 @@ +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 { + Box::new(early_return()) +} + +#[test] +#[should_panic] +fn early_return_boxing() { + if cfg!(debug_assertions) { + // The release-mode compiler is able to optimize through the Box + into_box(); + } else { + panic!("Intentional") + } +} diff --git a/tests/macros.rs b/tests/proc_macro.rs similarity index 92% rename from tests/macros.rs rename to tests/proc_macro.rs index 455a204..7e3fddd 100644 --- a/tests/macros.rs +++ b/tests/proc_macro.rs @@ -1,13 +1,12 @@ -extern crate qadapt; use std::io; -use qadapt::allocate_panic; +use qadapt::no_alloc; use qadapt::QADAPT; #[global_allocator] static Q: QADAPT = QADAPT; -#[allocate_panic] +#[no_alloc] fn no_allocate() { let _v: Vec<()> = Vec::with_capacity(0); } @@ -17,7 +16,7 @@ fn macro_no_allocate() { no_allocate(); } -#[allocate_panic] +#[no_alloc] fn allocates() { assert_eq!(::qadapt::protection_level(), 1); // Without boxing, release profile can actually optimize out the allocation @@ -31,7 +30,7 @@ fn macro_allocates() { allocates(); } -#[allocate_panic] +#[no_alloc] fn no_allocate_ret() -> bool { return true; } @@ -41,7 +40,7 @@ fn macro_return() { assert!(no_allocate_ret()); } -#[allocate_panic] +#[no_alloc] fn no_allocate_implicit_ret() -> bool { true } @@ -51,7 +50,7 @@ fn macro_implicit_return() { assert!(no_allocate_implicit_ret()); } -#[allocate_panic] +#[no_alloc] fn no_allocate_arg(b: bool) -> bool { b } @@ -62,7 +61,7 @@ fn macro_allocate_arg() { no_allocate_arg(false); } -#[allocate_panic] +#[no_alloc] fn no_allocate_args(_b: bool, _u: usize, i: i64) -> i64 { i } @@ -73,7 +72,7 @@ fn macro_allocate_args() { no_allocate_args(false, 4, -90); } -#[allocate_panic] +#[no_alloc] fn return_result(r: Result) -> Result, ()> { Ok(r) } @@ -83,7 +82,7 @@ fn macro_return_result() { return_result(Ok(16)).unwrap().unwrap(); } -#[allocate_panic] +#[no_alloc] fn branching_return(a: bool, b: bool, c: bool) -> u8 { if a { if b { @@ -132,7 +131,7 @@ fn run_closure(x: impl Fn(bool, bool) -> bool) -> bool { x(true, false) } -#[allocate_panic] +#[no_alloc] fn example_closure() { let c = run_closure(|a: bool, b| return a && b); assert!(!c); @@ -146,7 +145,7 @@ fn macro_closure() { } #[test] -#[allocate_panic] +#[no_alloc] fn macro_release_safe() { #[cfg(debug_assertions)] { diff --git a/tests/unused_panic.rs b/tests/unused_panic.rs new file mode 100644 index 0000000..e185a82 --- /dev/null +++ b/tests/unused_panic.rs @@ -0,0 +1,11 @@ +use qadapt::enter_protected; + +#[test] +#[should_panic] +fn guard_without_initialization() { + if cfg!(debug_assertions) { + enter_protected(); + } else { + panic!("Intentional") + } +}