diff --git a/.gitignore b/.gitignore index 2fcd2ae..6936990 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /target **/*.rs.bk Cargo.lock -*.swp diff --git a/Cargo.toml b/Cargo.toml index b109c12..36cc0b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,6 @@ repository = "https://github.com/bspeice/qadapt.git" [dependencies] libc = "0.2" spin = "0.4" -thread-id = "3.3" \ No newline at end of file +thread-id = "3.3" + +qadapt-macro = { version = "0.3.0", path = "./qadapt-macro" } \ No newline at end of file diff --git a/qadapt-macro/.gitignore b/qadapt-macro/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/qadapt-macro/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/qadapt-macro/Cargo.toml b/qadapt-macro/Cargo.toml new file mode 100644 index 0000000..847fa28 --- /dev/null +++ b/qadapt-macro/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "qadapt-macro" +version = "0.3.0" +authors = ["Bradlee Speice "] +description = "The Quick And Dirty Allocation Profiling Tool - Support Macros" +license = "Apache-2.0" +readme = "README.md" +categories = [ + "development-tools", + "development-tools::debugging", + "development-tools::profiling", + "development-tools::testing", + "memory-management" +] +repository = "https://github.com/bspeice/qadapt.git" + +[lib] +proc-macro = true diff --git a/qadapt-macro/README.md b/qadapt-macro/README.md new file mode 100644 index 0000000..424e993 --- /dev/null +++ b/qadapt-macro/README.md @@ -0,0 +1,6 @@ +# QADAPT - Helper macros + +Helper macros to use with the QADAPT allocator system + +This crate is intended for managing the QADAPT allocator, +and is unusable on its own. \ No newline at end of file diff --git a/qadapt-macro/src/lib.rs b/qadapt-macro/src/lib.rs new file mode 100644 index 0000000..56b218a --- /dev/null +++ b/qadapt-macro/src/lib.rs @@ -0,0 +1,136 @@ +//! Helper macros to use with the QADAPT allocator system +//! +//! This crate is intended for managing the QADAPT allocator, +//! and is unusable on its own. +//! +// TODO: This causes issues, but I can't track down why +// #![deny(missing_docs)] +extern crate proc_macro; + +use proc_macro::Delimiter; +use proc_macro::Spacing; +use proc_macro::Span; +use proc_macro::TokenStream; +use proc_macro::TokenTree; +use std::iter::FromIterator; + +type TT = proc_macro::TokenTree; +type TS = proc_macro::TokenStream; +type G = proc_macro::Group; +type I = proc_macro::Ident; +type P = proc_macro::Punct; + +fn release_guard(fn_name: &str) -> TokenStream { + let rel = I::new("release", Span::call_site()); + let not_rel: Vec = vec![ + I::new("not", Span::call_site()).into(), + G::new(Delimiter::Parenthesis, TokenTree::Ident(rel).into()).into() + ]; + let cfg_not_rel: Vec = vec![ + I::new("cfg", Span::call_site()).into(), + G::new(Delimiter::Parenthesis, TS::from_iter(not_rel.into_iter())).into() + ]; + let guarded: Vec = vec![ + P::new('#', Spacing::Alone).into(), + G::new(Delimiter::Bracket, TS::from_iter(cfg_not_rel.into_iter())).into(), + P::new(':', Spacing::Joint).into(), + P::new(':', Spacing::Alone).into(), + I::new("qadapt", Span::call_site()).into(), + P::new(':', Spacing::Joint).into(), + P::new(':', Spacing::Alone).into(), + I::new(fn_name, Span::call_site()).into(), + G::new(Delimiter::Parenthesis, TokenStream::new()).into(), + ]; + + TS::from_iter(guarded.into_iter()) +} + +fn protected_body(fn_name: &str, args: G) -> TokenTree { + let mut args_filtered = Vec::new(); + let mut waiting_for_comma = false; + let mut in_type = 0; + for tt in args.stream().into_iter() { + match tt { + TokenTree::Ident(ref _i) if !waiting_for_comma && in_type == 0 => { + args_filtered.push(tt.clone()); + waiting_for_comma = true; + } + TokenTree::Punct(ref p) if p.as_char() == '<' => in_type += 1, + TokenTree::Punct(ref p) if p.as_char() == '>' => in_type -= 1, + TokenTree::Punct(ref p) if p.as_char() == ',' && in_type == 0 => { + waiting_for_comma = false; + args_filtered.push(tt.clone()) + } + _ => () + } + } + let args_group = G::new(Delimiter::Parenthesis, TS::from_iter(args_filtered)); + + let tt: Vec = vec![ + G::new(Delimiter::Brace, release_guard("enter_protected")).into(), + I::new("let", Span::call_site()).into(), + I::new("__ret__", Span::call_site()).into(), + P::new('=', Spacing::Alone).into(), + I::new(fn_name, Span::call_site()).into(), + args_group.into(), + P::new(';', Spacing::Alone).into(), + G::new(Delimiter::Brace, release_guard("exit_protected")).into(), + I::new("__ret__", Span::call_site()).into(), + ]; + + G::new(Delimiter::Brace, TS::from_iter(tt)).into() +} + +/// Set up the QADAPT allocator to trigger a panic if any allocations happen during +/// calls to this function. +/// +/// QADAPT will only track allocations in the thread that calls this function; +/// if (for example) this function receives the results of an allocation in a +/// separate thread, QADAPT will not trigger a panic. +#[proc_macro_attribute] +pub fn allocate_panic(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut original_fn: Vec = Vec::new(); + let mut protected_fn: Vec = Vec::new(); + + let mut item_iter = item.into_iter(); + + // First, get the function name we're replacing + let mut fn_name = None; + let mut fn_args = None; + while let Some(tt) = item_iter.next() { + match tt { + TokenTree::Ident(ref i) if i.to_string() == "fn" => { + original_fn.push(tt.clone()); + protected_fn.push(tt.clone()); + } + TokenTree::Ident(ref i) if fn_args.is_none() => { + let changed_name = format!("__{}__", i.to_owned()); + original_fn.push(TokenTree::Ident(I::new(&changed_name, Span::call_site()))); + protected_fn.push(tt.clone()); + fn_name = Some(changed_name); + } + TokenTree::Group(ref g) if g.delimiter() == Delimiter::Parenthesis && fn_args.is_none() => { + original_fn.push(tt.clone()); + protected_fn.push(tt.clone()); + fn_args = Some(g.clone()); + } + TokenTree::Group(ref g) if g.delimiter() == Delimiter::Brace => { + original_fn.push(tt.clone()); + protected_fn.push(protected_body( + &fn_name.take().unwrap(), + fn_args.take().unwrap(), + )); + } + tt => { + original_fn.push(tt.clone()); + protected_fn.push(tt.clone()); + } + } + } + + let mut full = Vec::new(); + full.push(TS::from_iter(original_fn)); + full.push(TS::from_iter(protected_fn)); + let ts = TS::from_iter(full.into_iter()); + ts +} diff --git a/src/lib.rs b/src/lib.rs index 8a74283..9410835 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,11 +12,15 @@ //! for some helper macros to make working with QADAPT a bit easier. #![deny(missing_docs)] extern crate libc; +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; +// Re-export the proc macros to use by other code +pub use qadapt_macro::*; + use libc::c_void; use libc::free; use libc::malloc; @@ -27,7 +31,6 @@ use std::thread; thread_local! { static PROTECTION_LEVEL: RwLock = RwLock::new(0); - static IS_STARTED: RwLock = RwLock::new(false); } /// The QADAPT allocator itself @@ -40,10 +43,6 @@ pub fn enter_protected() { return; } - IS_STARTED - .try_with(|b| assert!(*b.read(), "QADAPT not in use")) - .unwrap_or_else(|_e| ()); - PROTECTION_LEVEL .try_with(|v| { *v.write() += 1; @@ -71,17 +70,6 @@ pub fn exit_protected() { .unwrap_or_else(|_e| ()); } -/// Given a closure, run a function inside a guarded allocation block -pub fn do_protected(mut c: C) -> T -where - C: FnMut() -> T -{ - enter_protected(); - let ret = c(); - exit_protected(); - ret -} - static INTERNAL_ALLOCATION: RwLock = RwLock::new(usize::max_value()); /// Get the current "protection level" in QADAPT: calls to enter_protected() - exit_protected() @@ -120,9 +108,6 @@ unsafe impl GlobalAlloc for QADAPT { return malloc(layout.size()) as *mut u8; } - IS_STARTED.try_with(|b| *b.write() = true) - .unwrap_or_else(|_e| ()); - // 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. claim_internal_alloc(); @@ -146,7 +131,7 @@ unsafe impl GlobalAlloc for QADAPT { } } - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) { if alloc_immediate() { return free(ptr as *mut c_void); } @@ -163,27 +148,15 @@ unsafe impl GlobalAlloc for QADAPT { // Tripped a bad dealloc, but make sure further memory access during unwind // doesn't have issues PROTECTION_LEVEL.with(|v| *v.write() = 0); + /* panic!( "Unexpected deallocation for size {}, protection level: {}", layout.size(), v ) + */ } _ => (), } } } - -#[macro_export] -macro_rules! alloc_panic{ - ($fn_body:expr) => {{ - #[cfg(any(test, debug))] - { - ::qadapt::do_protected(|| $fn_body) - } - #[cfg(not(any(test, debug)))] - { - $fn_body - } - }} -} diff --git a/tests/alloc_examples.rs b/tests/allocations.rs similarity index 74% rename from tests/alloc_examples.rs rename to tests/allocations.rs index 40319da..1547832 100644 --- a/tests/alloc_examples.rs +++ b/tests/allocations.rs @@ -1,4 +1,4 @@ -#[macro_use] +#![feature(asm)] extern crate qadapt; use qadapt::enter_protected; @@ -9,14 +9,17 @@ use qadapt::QADAPT; #[global_allocator] static Q: QADAPT = QADAPT; +pub fn black_box(dummy: T) -> T { + // Taken from test lib, need to mark the arg as non-introspectable + unsafe { asm!("" : : "r"(&dummy)) } + dummy +} + #[test] fn test_copy() { - let z = alloc_panic!({ - let zero = 0; - zero - }); - - assert_eq!(z, 0); + enter_protected(); + black_box(0u8); + exit_protected(); } #[test] @@ -38,8 +41,16 @@ fn unit_result(b: bool) -> Result<(), ()> { #[test] fn test_unit_result() { enter_protected(); - unit_result(true).unwrap(); - unit_result(false).unwrap_err(); + #[allow(unused)] + { + black_box(unit_result(true)); + } + black_box(unit_result(true)).unwrap(); + #[allow(unused)] + { + black_box(unit_result(false)); + } + black_box(unit_result(false)).unwrap_err(); exit_protected(); } @@ -69,14 +80,14 @@ fn test_vec_push_capacity() { #[test] fn test_vec_with_zero() { enter_protected(); - let _v: Vec = Vec::with_capacity(0); + let _v: Vec = black_box(Vec::with_capacity(0)); exit_protected(); } #[test] fn test_vec_new() { enter_protected(); - let _v: Vec = Vec::new(); + let _v: Vec = black_box(Vec::new()); exit_protected(); } diff --git a/tests/macros.rs b/tests/macros.rs new file mode 100644 index 0000000..cd5d252 --- /dev/null +++ b/tests/macros.rs @@ -0,0 +1,89 @@ +extern crate qadapt; +use std::io; + +use qadapt::allocate_panic; +use qadapt::QADAPT; + +#[global_allocator] +static Q: QADAPT = QADAPT; + +#[allocate_panic] +fn no_allocate() { + #[cfg(not(release))] + { + let _v = 0; + } + assert_eq!(::qadapt::protection_level(), 1); + let _v: Vec<()> = Vec::with_capacity(0); +} + +#[test] +fn macro_no_allocate() { + no_allocate(); +} + +#[allocate_panic] +fn allocates() { + assert_eq!(::qadapt::protection_level(), 1); + // Without boxing, release profile can actually optimize out the allocation + let mut v = Box::new(Vec::new()); + v.push(1); +} + +#[test] +#[should_panic] +fn macro_allocates() { + allocates(); +} + +#[allocate_panic] +fn no_allocate_ret() -> bool { + return true; +} + +#[test] +fn macro_return() { + assert!(no_allocate_ret()); +} + +#[allocate_panic] +fn no_allocate_implicit_ret() -> bool { + true +} + +#[test] +fn macro_implicit_return() { + assert!(no_allocate_implicit_ret()); +} + +#[allocate_panic] +fn no_allocate_arg(b: bool) -> bool { + b +} + +#[test] +fn macro_allocate_arg() { + no_allocate_arg(true); + no_allocate_arg(false); +} + +#[allocate_panic] +fn no_allocate_args(_b: bool, _u: usize, i: i64) -> i64 { + i +} + +#[test] +fn macro_allocate_args() { + no_allocate_args(true, 0, -1); + no_allocate_args(false, 4, -90); +} + +#[allocate_panic] +fn return_result(r: Result) -> Result, ()> { + Ok(r) +} + +#[test] +fn macro_return_result() { + return_result(Ok(16)).unwrap().unwrap(); +} diff --git a/tests/usage.rs b/tests/usage.rs deleted file mode 100644 index 5f0c247..0000000 --- a/tests/usage.rs +++ /dev/null @@ -1,7 +0,0 @@ -extern crate qadapt; - -#[test] -#[should_panic] -fn panic_not_using_qadapt() { - ::qadapt::enter_protected(); -} \ No newline at end of file