{"action":"created","issue":{"url":"https://api.github.com/repos/rust-lang/rfcs/issues/916","labels_url":"https://api.github.com/repos/rust-lang/rfcs/issues/916/labels{/name}","comments_url":"https://api.github.com/repos/rust-lang/rfcs/issues/916/comments","events_url":"https://api.github.com/repos/rust-lang/rfcs/issues/916/events","html_url":"https://github.com/rust-lang/rfcs/issues/916","id":59184937,"number":916,"title":"Add support for the wrapping of heap allocation and deallocation functions","user":{"login":"nnethercote","id":1940286,"avatar_url":"https://avatars.githubusercontent.com/u/1940286?v=3","gravatar_id":"","url":"https://api.github.com/users/nnethercote","html_url":"https://github.com/nnethercote","followers_url":"https://api.github.com/users/nnethercote/followers","following_url":"https://api.github.com/users/nnethercote/following{/other_user}","gists_url":"https://api.github.com/users/nnethercote/gists{/gist_id}","starred_url":"https://api.github.com/users/nnethercote/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nnethercote/subscriptions","organizations_url":"https://api.github.com/users/nnethercote/orgs","repos_url":"https://api.github.com/users/nnethercote/repos","events_url":"https://api.github.com/users/nnethercote/events{/privacy}","received_events_url":"https://api.github.com/users/nnethercote/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"milestone":null,"comments":2,"created_at":"2015-02-27T03:34:45Z","updated_at":"2015-02-27T15:38:47Z","closed_at":null,"body":"(An earlier version of this proposal was posted at http://internals.rust-lang.org/t/add-support-for-the-wrapping-of-heap-allocation-and-deallocation-functions/1618/6, where it didn't get much attention.)\r\n\r\nIt can be very useful for user programs to be able to take arbitrary actions when when heap allocation and deallocation occurs.\r\n\r\nMy motivation is that I want to build a heap profiler for Servo, one that's similar to Firefox's DMD. This profiler would record a stack trace on each allocation, and use that to emit data about which parts of the code are performing many allocations (contributing to heap churn) and which ones are responsible for allocating live blocks (contributing to peak memory usage). This may sound like a generic tool that could be built into Rust itself, but it's likely to end up with Servo-specific parts, so flexible building blocks would be better than a canned solution.\r\n\r\nThere are lots of other potential uses for this, and this kind of facility is common in other systems. E.g. glibc provides one for [malloc/realloc/free](http://www.gnu.org/software/libc/manual/html_node/Hooks-for-Malloc.html), and there's also the more general `LD_PRELOAD` (on Linux) and `DYLD_INSERT_LIBRARIES` (on Mac) which allow you to hook any library functions.\r\n\r\nWhat this needs.\r\n\r\n- A program needs to be able to specify that it wants to opt in to this feature, and a way to specify the wrapping functions (one each for std::rt::heap::allocate, reallocate, reallocate_inplace, deallocate, and possibly usable_size and stats_print). The opting-in could be at compile-time or runtime; the latter is probably preferable because it's more flexible, but this is not a strong preference.\r\n\r\n- The allocation/deallocation functions need to call any provided wrappers.\r\n\r\n- A way for wrappers to temporarily disable wrapping while they are running, so that we don't get infinite recursion if the wrapper itself triggers a call to the function that it wraps.\r\n\r\nI have a basic, ugly, proof-of-concept implementation. It adds the following code to `src/liballoc/heap.rs`, which defines a struct for holding the wrapper function and a function for setting the wrapper. (I've only shown the code for `allocate`; the other functions are handled similarly.)\r\n```\r\npub type AllocateFn = unsafe fn(usize, usize) -> *mut u8;\r\n        \r\npub type AllocateFnWrapper = fn(AllocateFn, usize, usize) -> *mut u8;\r\n\r\nstruct AllocatorWrappers {\r\n    allocate: AllocateFnWrapper,\r\n}\r\n\r\nstatic mut wrappers: Option<AllocatorWrappers> = Option::None;\r\n\r\npub fn set_allocator_wrappers(allocate: AllocateFnWrapper) {\r\n    let new_wrappers = AllocatorWrappers {\r\n        allocate: allocate,\r\n    };\r\n    unsafe { wrappers = Option::Some(new_wrappers) };\r\n}\r\n```\r\nIt also modifies allocate like so:\r\n```\r\n #[inline]\r\n pub unsafe fn allocate(size: uint, align: uint) -> *mut u8 {\r\n-    imp::allocate(size, align)\r\n+    match wrappers {\r\n+        Option::None        => imp::allocate(size, align),\r\n+        Option::Some(ref h) => (h.allocate)(imp::allocate, size, align),\r\n+    }\r\n }\r\n```\r\nIn the normal case this adds a single, perfectly-predictable branch to the alloc/dealloc path, which is hopefully small in relation to the cost of an alloc/dealloc.\r\n\r\nAnd here is a sample program that uses it.\r\n```\r\n#![feature(alloc)]\r\n#![feature(core)]\r\n#![feature(std_misc)]\r\n#![feature(unmarked_api)]\r\n#![feature(unsafe_destructor)]\r\n\r\n// This demonstrates how an allocation wrapping could be made to work in Rust\r\n// using std::heap::set_allocator_wrappers().\r\n\r\nuse std::rt::heap::*;\r\nuse std::cell::Cell;\r\nuse std::sync::{MutexGuard,StaticMutex,MUTEX_INIT};\r\n\r\n//---------------------------------------------------------------------------\r\n\r\n// A global table counting how many times each operation is called. This is\r\n// representative of more complex global state likely to be used by any kind of\r\n// heap profiler. It requires explicit locking via a `StaticMutex`, which is\r\n// best done by using `AutoDisableWrappersAndLockCounts`.\r\n\r\nstruct Counts {\r\n    allocate: u32,\r\n    reallocate: u32,\r\n    reallocate_inplace: u32,\r\n    deallocate: u32,\r\n}\r\n\r\nstatic mut COUNTS: Counts = Counts {\r\n    allocate: 0,\r\n    reallocate: 0,\r\n    reallocate_inplace: 0,\r\n    deallocate: 0,\r\n};\r\n\r\nstatic COUNTS_LOCK: StaticMutex = MUTEX_INIT;\r\n\r\n//---------------------------------------------------------------------------\r\n\r\n// If the `allocate` wrapper itself called `allocate`, we'd get infinite\r\n// recursion, and likewise for the other wrappers, and the other functions\r\n// dealing with the analysis state. Therefore, within each wrapper, we need a\r\n// way to to temporarily disable wrapping, which we do on a per-thread basis.\r\n//\r\n// One complication with this is that it can interact badly with other TLS.\r\n// For example, if we call println! within `deallocate_wrapper`, we get a\r\n// \"cannot access TLS after it's destroyed\" panic. This is because the module\r\n// implementing println! (and related printing functions) has its own TLS,\r\n// called `LOCAL_STDOUT`, in stdio.rs. If `LOCAL_STDOUT` is destroyed before\r\n// `WRAPPERS_ENABLED`, during its destruction it deallocates a\r\n// LineBufferedOutput struct allocated by stdout(), which triggers a call to\r\n// `deallocate_wrapper`, and the println! triggers the panic because\r\n// `LOCAL_STDOUT` is in the middle of being destroyed.\r\n//\r\n// Therefore, calling any code that uses a droppable TLS value is potentially a\r\n// problem in `deallocate_wrapper`. Hmm.\r\n\r\nthread_local!(static WRAPPERS_ENABLED: Cell<bool> = Cell::new(true));\r\n\r\n// `AutoDisableWrappersAndLockCounts` is an RAII struct for (a) the scoped disabling of\r\n// wrapping, and (b) the scoped locking of `Counts`. It is not just convenient,\r\n// but crucial; for (a) if we do `set(false)` and then `set(true)` manually\r\n// then any destructors that run will happen after the latter, which will lead\r\n// to infinite recursion.\r\n\r\nstruct AutoDisableWrappersAndLockCounts<'a> {\r\n    wrappers_enabled: &'a Cell<bool>,\r\n    _lock: MutexGuard<'a, ()>,\r\n}\r\n\r\nimpl<'a> AutoDisableWrappersAndLockCounts<'a> {\r\n    fn new(w: &Cell<bool>) -> AutoDisableWrappersAndLockCounts {\r\n        w.set(false);\r\n        AutoDisableWrappersAndLockCounts {\r\n            wrappers_enabled: w,\r\n            _lock: COUNTS_LOCK.lock().unwrap(),\r\n        }\r\n    }\r\n}\r\n\r\n#[unsafe_destructor]\r\nimpl<'a> Drop for AutoDisableWrappersAndLockCounts<'a> {\r\n    fn drop(&mut self) {\r\n        self.wrappers_enabled.set(true);\r\n    }\r\n}\r\n\r\n//---------------------------------------------------------------------------\r\n\r\n// The wrapper functions themselves. If wrapping is disabled when they are\r\n// called, they just do a vanilla alloc/realloc/dealloc. Otherwise, they update\r\n// COUNTS appropriately as well.\r\n\r\nfn allocate_wrapper(real_allocate: AllocateFn, size: usize, align: usize) -> *mut u8 {\r\n    WRAPPERS_ENABLED.with(|wrappers_enabled| {\r\n        if wrappers_enabled.get() {\r\n            let _a = AutoDisableWrappersAndLockCounts::new(wrappers_enabled);\r\n            unsafe { COUNTS.allocate += 1; }\r\n        }\r\n        unsafe { real_allocate(size, align) }\r\n    })\r\n}\r\n\r\nfn reallocate_wrapper(real_reallocate: ReallocateFn, ptr: *mut u8, old_size: usize, size: usize,\r\n                      align: usize) -> *mut u8 {\r\n    WRAPPERS_ENABLED.with(|wrappers_enabled| {\r\n        if wrappers_enabled.get() {\r\n            let _a = AutoDisableWrappersAndLockCounts::new(wrappers_enabled);\r\n            unsafe { COUNTS.reallocate += 1; }\r\n        }\r\n        unsafe { real_reallocate(ptr, old_size, size, align) }\r\n    })\r\n}\r\n\r\nfn reallocate_inplace_wrapper(real_reallocate_inplace: ReallocateInplaceFn, ptr: *mut u8,\r\n                              old_size: usize, size: usize, align: usize) -> usize {\r\n    WRAPPERS_ENABLED.with(|wrappers_enabled| {\r\n        if wrappers_enabled.get() {\r\n            let _a = AutoDisableWrappersAndLockCounts::new(wrappers_enabled);\r\n            unsafe { COUNTS.reallocate_inplace += 1; }\r\n        }\r\n        unsafe { real_reallocate_inplace(ptr, old_size, size, align) }\r\n    })\r\n}\r\n\r\nfn deallocate_wrapper(real_deallocate: DeallocateFn, ptr: *mut u8, old_size: usize, align: usize) {\r\n    WRAPPERS_ENABLED.with(|wrappers_enabled| {\r\n        if wrappers_enabled.get() {\r\n            let _a = AutoDisableWrappersAndLockCounts::new(wrappers_enabled);\r\n            unsafe { COUNTS.deallocate += 1; }\r\n        }\r\n        unsafe { real_deallocate(ptr, old_size, align) }\r\n    })\r\n}\r\n\r\n//---------------------------------------------------------------------------\r\n\r\n// Prints the current measurements in COUNTS.\r\nfn snapshot() {\r\n    WRAPPERS_ENABLED.with(|wrappers_enabled| {\r\n        let _a = AutoDisableWrappersAndLockCounts::new(wrappers_enabled);\r\n        unsafe {\r\n            println!(\"--------\");\r\n            println!(\"counts:\");\r\n            println!(\"- allocate:           {}\", COUNTS.allocate);\r\n            println!(\"- reallocate:         {}\", COUNTS.reallocate);\r\n            println!(\"- reallocate_inplace: {}\", COUNTS.reallocate_inplace);\r\n            println!(\"- deallocate:         {}\", COUNTS.deallocate);\r\n            println!(\"--------\");\r\n        }\r\n    });\r\n}\r\n\r\n//---------------------------------------------------------------------------\r\n\r\nfn main() {\r\n    // Want to do this as early as possible. (Really, it would be better to\r\n    // have a way to specify these wrapper functions that is visible and takes\r\n    // effect before we even reach main().)\r\n    set_allocator_wrappers(allocate_wrapper, reallocate_wrapper, reallocate_inplace_wrapper,\r\n                           deallocate_wrapper);\r\n\r\n    // Just some sample code showing a variety of allocations on two threads.\r\n\r\n    snapshot();\r\n\r\n    for _ in range(0, 5) {\r\n        let _x = Box::new(0u64);\r\n    }\r\n\r\n    snapshot();\r\n\r\n    let guard = std::thread::Thread::scoped(|| {\r\n        println!(\"start new thread!\");\r\n        for _ in range(0, 5) {\r\n            let _x = Box::new(0u32);\r\n        }\r\n        println!(\"finish new thread!\");\r\n    });\r\n    let _result = guard.join();\r\n\r\n    snapshot();\r\n}\r\n\r\n```\r\nLike I said, it's ugly.\r\n- `set_allocation_hooks` should be called as early as possible, so that it doesn't miss any allocations. It's also entirely non-thread-safe, which is probably ok if you do call it right at the start of `main`, but is still nasty. Putting the wrappers table inside an `RwLock` or something might be possible but it would be a shame, performance-wise, to do that for a data structure that's written once and then read zillions of times. I figure there must be a more Rust-y way of doing this. Could a `#[feature(\"...\")]` annotation work? We really want these wrappers to be enabled the language runtime at start-up, rather than the program itself having to do it.\r\n\r\n- The thread-local storage to avoid infinite recursion isn't so bad, though it would be nice if this could be somehow handled within the Rust implementation somehow so that each individual program doesn't have to reuse it. The issues with wrappers calling code that uses TLS are also annoying.\r\n\r\nIt was really just a prototype to see if I could get something working. And it does.\r\n\r\nSo... I'm wondering if (a) a feature like this seems like something that might be accepted into Rust, and (b) if there are better ways of implementing it. I hope so, because my ideas for measuring Servo's memory usage are dead in the water without this.\r\n\r\nThis idea has a small amount of overlap with [this old RFC](https://github.com/rust-lang/rfcs/blob/d3c6068e823f495ee241caa05d4782b16e5ef5d8/active/0000-allocator.md) -- I want a custom allocator for the entire program, basically -- but is much smaller.\r\n\r\nThank you for reading.\r\n"},"comment":{"url":"https://api.github.com/repos/rust-lang/rfcs/issues/comments/76413778","html_url":"https://github.com/rust-lang/rfcs/issues/916#issuecomment-76413778","issue_url":"https://api.github.com/repos/rust-lang/rfcs/issues/916","id":76413778,"user":{"login":"pnkfelix","id":173127,"avatar_url":"https://avatars.githubusercontent.com/u/173127?v=3","gravatar_id":"","url":"https://api.github.com/users/pnkfelix","html_url":"https://github.com/pnkfelix","followers_url":"https://api.github.com/users/pnkfelix/followers","following_url":"https://api.github.com/users/pnkfelix/following{/other_user}","gists_url":"https://api.github.com/users/pnkfelix/gists{/gist_id}","starred_url":"https://api.github.com/users/pnkfelix/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pnkfelix/subscriptions","organizations_url":"https://api.github.com/users/pnkfelix/orgs","repos_url":"https://api.github.com/users/pnkfelix/repos","events_url":"https://api.github.com/users/pnkfelix/events{/privacy}","received_events_url":"https://api.github.com/users/pnkfelix/received_events","type":"User","site_admin":false},"created_at":"2015-02-27T15:38:47Z","updated_at":"2015-02-27T15:38:47Z","body":"@nnethercote @nikomatsakis is there any reason we would need to address this before 1.0?  AFAICT we can add support for this backwards compatibly.\r\n\r\n(I agree that something like this is desirable.  I just want to know if it something that we need to rush to work on, or if we can wait until the 1.0-related distractions are behind us...)"}}