use {
core::ffi::c_void,
pyo3::ffi as pyffi,
python_packaging::interpreter::MemoryAllocatorBackend,
std::{
alloc,
collections::HashMap,
ops::{Deref, DerefMut},
sync::Mutex,
},
};
const MIN_ALIGN: usize = 16;
struct AllocationTracker {
allocations: Mutex<HashMap<*mut c_void, alloc::Layout>>,
}
impl AllocationTracker {
fn new() -> Box<Self> {
Box::new(Self {
allocations: Mutex::new(HashMap::with_capacity(128)),
})
}
fn from_owned_ptr(ptr: *mut c_void) -> BorrowedAllocationTracker {
if ptr.is_null() {
panic!("must not pass NULL pointer");
}
BorrowedAllocationTracker {
inner: Some(unsafe { Box::from_raw(ptr as *mut AllocationTracker) }),
}
}
#[inline]
fn get_allocation(&self, ptr: *mut c_void) -> Option<alloc::Layout> {
self.allocations.lock().unwrap().get(&ptr).cloned()
}
#[inline]
fn insert_allocation(&mut self, ptr: *mut c_void, layout: alloc::Layout) {
self.allocations.lock().unwrap().insert(ptr, layout);
}
#[inline]
fn remove_allocation(&mut self, ptr: *mut c_void) -> alloc::Layout {
self.allocations
.lock()
.unwrap()
.remove(&ptr)
.expect("memory address not tracked")
}
}
struct BorrowedAllocationTracker {
inner: Option<Box<AllocationTracker>>,
}
impl Deref for BorrowedAllocationTracker {
type Target = AllocationTracker;
fn deref(&self) -> &Self::Target {
self.inner.as_ref().unwrap()
}
}
impl DerefMut for BorrowedAllocationTracker {
fn deref_mut(&mut self) -> &mut Self::Target {
self.inner.as_mut().unwrap()
}
}
impl Drop for BorrowedAllocationTracker {
fn drop(&mut self) {
Box::into_raw(self.inner.take().unwrap());
}
}
pub(crate) struct TrackingAllocator {
pub allocator: pyffi::PyMemAllocatorEx,
pub arena: pyffi::PyObjectArenaAllocator,
_state: Box<AllocationTracker>,
}
extern "C" fn rust_malloc(ctx: *mut c_void, size: usize) -> *mut c_void {
let size = match size {
0 => 1,
val => val,
};
let mut tracker = AllocationTracker::from_owned_ptr(ctx);
let layout = unsafe { alloc::Layout::from_size_align_unchecked(size, MIN_ALIGN) };
let res = unsafe { alloc::alloc(layout) } as *mut _;
tracker.insert_allocation(res, layout);
res
}
#[cfg(feature = "jemalloc-sys")]
extern "C" fn jemalloc_malloc(_ctx: *mut c_void, size: usize) -> *mut c_void {
let size = match size {
0 => 1,
val => val,
};
unsafe { jemalloc_sys::mallocx(size, 0) }
}
#[cfg(feature = "libmimalloc-sys")]
extern "C" fn mimalloc_malloc(_ctx: *mut c_void, size: usize) -> *mut c_void {
let size = match size {
0 => 1,
val => val,
};
unsafe { libmimalloc_sys::mi_malloc(size) as *mut _ }
}
#[cfg(feature = "snmalloc-sys")]
extern "C" fn snmalloc_malloc(_ctx: *mut c_void, size: usize) -> *mut c_void {
let size = match size {
0 => 1,
val => val,
};
unsafe { snmalloc_sys::sn_malloc(size) as *mut _ }
}
extern "C" fn rust_calloc(ctx: *mut c_void, nelem: usize, elsize: usize) -> *mut c_void {
let size = match nelem * elsize {
0 => 1,
val => val,
};
let mut tracker = AllocationTracker::from_owned_ptr(ctx);
let layout = unsafe { alloc::Layout::from_size_align_unchecked(size, MIN_ALIGN) };
let res = unsafe { alloc::alloc_zeroed(layout) } as *mut _;
tracker.insert_allocation(res, layout);
res
}
#[cfg(feature = "jemalloc-sys")]
extern "C" fn jemalloc_calloc(_ctx: *mut c_void, nelem: usize, elsize: usize) -> *mut c_void {
let size = match nelem * elsize {
0 => 1,
val => val,
};
unsafe { jemalloc_sys::mallocx(size, jemalloc_sys::MALLOCX_ZERO) }
}
#[cfg(feature = "libmimalloc-sys")]
extern "C" fn mimalloc_calloc(_ctx: *mut c_void, nelem: usize, elsize: usize) -> *mut c_void {
let size = match nelem * elsize {
0 => 1,
val => val,
};
unsafe { libmimalloc_sys::mi_calloc(nelem, size) as *mut _ }
}
#[cfg(feature = "snmalloc-sys")]
extern "C" fn snmalloc_calloc(_ctx: *mut c_void, nelem: usize, elsize: usize) -> *mut c_void {
let size = match nelem * elsize {
0 => 1,
val => val,
};
unsafe { snmalloc_sys::sn_calloc(nelem, size) as *mut _ }
}
extern "C" fn rust_realloc(ctx: *mut c_void, ptr: *mut c_void, new_size: usize) -> *mut c_void {
if ptr.is_null() {
return rust_malloc(ctx, new_size);
}
let new_size = match new_size {
0 => 1,
val => val,
};
let mut tracker = AllocationTracker::from_owned_ptr(ctx);
let layout = unsafe { alloc::Layout::from_size_align_unchecked(new_size, MIN_ALIGN) };
let old_layout = tracker.remove_allocation(ptr);
let res = unsafe { alloc::realloc(ptr as *mut _, old_layout, new_size) } as *mut _;
tracker.insert_allocation(res, layout);
res
}
#[cfg(feature = "jemalloc-sys")]
extern "C" fn jemalloc_realloc(ctx: *mut c_void, ptr: *mut c_void, new_size: usize) -> *mut c_void {
if ptr.is_null() {
return jemalloc_malloc(ctx, new_size);
}
let new_size = match new_size {
0 => 1,
val => val,
};
unsafe { jemalloc_sys::rallocx(ptr, new_size, 0) }
}
#[cfg(feature = "libmimalloc-sys")]
extern "C" fn mimalloc_realloc(ctx: *mut c_void, ptr: *mut c_void, new_size: usize) -> *mut c_void {
if ptr.is_null() {
return mimalloc_malloc(ctx, new_size);
}
let new_size = match new_size {
0 => 1,
val => val,
};
unsafe { libmimalloc_sys::mi_realloc(ptr as *mut _, new_size) as *mut _ }
}
#[cfg(feature = "snmalloc-sys")]
extern "C" fn snmalloc_realloc(ctx: *mut c_void, ptr: *mut c_void, new_size: usize) -> *mut c_void {
if ptr.is_null() {
return snmalloc_malloc(ctx, new_size);
}
let new_size = match new_size {
0 => 1,
val => val,
};
unsafe { snmalloc_sys::sn_realloc(ptr as *mut _, new_size) as *mut _ }
}
extern "C" fn rust_free(ctx: *mut c_void, ptr: *mut c_void) {
if ptr.is_null() {
return;
}
let mut tracker = AllocationTracker::from_owned_ptr(ctx);
let layout = tracker
.get_allocation(ptr)
.unwrap_or_else(|| panic!("could not find allocated memory record: {:?}", ptr));
unsafe {
alloc::dealloc(ptr as *mut _, layout);
}
tracker.remove_allocation(ptr);
}
#[cfg(feature = "jemalloc-sys")]
extern "C" fn jemalloc_free(_ctx: *mut c_void, ptr: *mut c_void) {
if ptr.is_null() {
return;
}
unsafe { jemalloc_sys::dallocx(ptr, 0) }
}
#[cfg(feature = "libmimalloc-sys")]
extern "C" fn mimalloc_free(_ctx: *mut c_void, ptr: *mut c_void) {
if ptr.is_null() {
return;
}
unsafe { libmimalloc_sys::mi_free(ptr as *mut _) }
}
#[cfg(feature = "snmalloc-sys")]
extern "C" fn snmalloc_free(_ctx: *mut c_void, ptr: *mut c_void) {
if ptr.is_null() {
return;
}
unsafe { snmalloc_sys::sn_free(ptr as *mut _) }
}
extern "C" fn rust_arena_free(ctx: *mut c_void, ptr: *mut c_void, _size: usize) {
if ptr.is_null() {
return;
}
let mut tracker = AllocationTracker::from_owned_ptr(ctx);
let layout = tracker
.get_allocation(ptr)
.unwrap_or_else(|| panic!("could not find allocated memory record: {:?}", ptr));
unsafe {
alloc::dealloc(ptr as *mut _, layout);
}
tracker.remove_allocation(ptr);
}
#[cfg(feature = "jemalloc-sys")]
extern "C" fn jemalloc_arena_free(_ctx: *mut c_void, ptr: *mut c_void, _size: usize) {
if ptr.is_null() {
return;
}
unsafe { jemalloc_sys::dallocx(ptr, 0) }
}
#[cfg(feature = "libmimalloc-sys")]
extern "C" fn mimalloc_arena_free(_ctx: *mut c_void, ptr: *mut c_void, _size: usize) {
if ptr.is_null() {
return;
}
unsafe { libmimalloc_sys::mi_free(ptr as *mut _) }
}
#[cfg(feature = "snmalloc-sys")]
extern "C" fn snmalloc_arena_free(_ctx: *mut c_void, ptr: *mut c_void, _size: usize) {
if ptr.is_null() {
return;
}
unsafe { snmalloc_sys::sn_free(ptr as *mut _) }
}
enum AllocatorInstance {
#[allow(dead_code)]
Simple(pyffi::PyMemAllocatorEx, pyffi::PyObjectArenaAllocator),
Tracking(TrackingAllocator),
}
pub struct PythonMemoryAllocator {
backend: MemoryAllocatorBackend,
instance: AllocatorInstance,
}
impl PythonMemoryAllocator {
pub fn from_backend(backend: MemoryAllocatorBackend) -> Option<Self> {
match backend {
MemoryAllocatorBackend::Default => None,
MemoryAllocatorBackend::Jemalloc => Some(Self::jemalloc()),
MemoryAllocatorBackend::Mimalloc => Some(Self::mimalloc()),
MemoryAllocatorBackend::Snmalloc => Some(Self::snmalloc()),
MemoryAllocatorBackend::Rust => Some(Self::rust()),
}
}
#[cfg(feature = "jemalloc-sys")]
pub fn jemalloc() -> Self {
Self {
backend: MemoryAllocatorBackend::Jemalloc,
instance: AllocatorInstance::Simple(
pyffi::PyMemAllocatorEx {
ctx: std::ptr::null_mut(),
malloc: Some(jemalloc_malloc),
calloc: Some(jemalloc_calloc),
realloc: Some(jemalloc_realloc),
free: Some(jemalloc_free),
},
pyffi::PyObjectArenaAllocator {
ctx: std::ptr::null_mut(),
alloc: Some(jemalloc_malloc),
free: Some(jemalloc_arena_free),
},
),
}
}
#[cfg(not(feature = "jemalloc-sys"))]
pub fn jemalloc() -> Self {
panic!("jemalloc allocator requested but it isn't compiled into this build configuration; try `cargo build --features allocator-jemalloc`");
}
#[cfg(feature = "libmimalloc-sys")]
pub fn mimalloc() -> Self {
Self {
backend: MemoryAllocatorBackend::Mimalloc,
instance: AllocatorInstance::Simple(
pyffi::PyMemAllocatorEx {
ctx: std::ptr::null_mut(),
malloc: Some(mimalloc_malloc),
calloc: Some(mimalloc_calloc),
realloc: Some(mimalloc_realloc),
free: Some(mimalloc_free),
},
pyffi::PyObjectArenaAllocator {
ctx: std::ptr::null_mut(),
alloc: Some(mimalloc_malloc),
free: Some(mimalloc_arena_free),
},
),
}
}
#[cfg(not(feature = "libmimalloc-sys"))]
pub fn mimalloc() -> Self {
panic!("mimalloc allocator requested but it isn't compiled into this build configuration; try `cargo build --features allocator-mimalloc`");
}
pub fn rust() -> Self {
let state = Box::into_raw(AllocationTracker::new());
let allocator = pyffi::PyMemAllocatorEx {
ctx: state as *mut c_void,
malloc: Some(rust_malloc),
calloc: Some(rust_calloc),
realloc: Some(rust_realloc),
free: Some(rust_free),
};
Self {
backend: MemoryAllocatorBackend::Rust,
instance: AllocatorInstance::Tracking(TrackingAllocator {
allocator,
arena: pyffi::PyObjectArenaAllocator {
ctx: state as *mut c_void,
alloc: Some(rust_malloc),
free: Some(rust_arena_free),
},
_state: unsafe { Box::from_raw(state) },
}),
}
}
#[cfg(feature = "snmalloc-sys")]
pub fn snmalloc() -> Self {
Self {
backend: MemoryAllocatorBackend::Snmalloc,
instance: AllocatorInstance::Simple(
pyffi::PyMemAllocatorEx {
ctx: std::ptr::null_mut(),
malloc: Some(snmalloc_malloc),
calloc: Some(snmalloc_calloc),
realloc: Some(snmalloc_realloc),
free: Some(snmalloc_free),
},
pyffi::PyObjectArenaAllocator {
ctx: std::ptr::null_mut(),
alloc: Some(snmalloc_malloc),
free: Some(snmalloc_arena_free),
},
),
}
}
#[cfg(not(feature = "snmalloc-sys"))]
pub fn snmalloc() -> Self {
panic!("snmalloc allocator requested but it isn't compiled into this build configuration; try `cargo build --features allocator-snmalloc`");
}
#[allow(unused)]
pub fn backend(&self) -> MemoryAllocatorBackend {
self.backend
}
pub fn set_allocator(&self, domain: pyffi::PyMemAllocatorDomain) {
unsafe {
pyffi::PyMem_SetAllocator(domain, self.as_memory_allocator() as *mut _);
}
}
#[allow(dead_code)]
pub fn set_arena_allocator(&self) {
unsafe { pyffi::PyObject_SetArenaAllocator(self.as_arena_allocator()) }
}
fn as_memory_allocator(&self) -> *const pyffi::PyMemAllocatorEx {
match &self.instance {
AllocatorInstance::Simple(alloc, _) => alloc as *const _,
AllocatorInstance::Tracking(alloc) => &alloc.allocator as *const _,
}
}
#[allow(dead_code)]
fn as_arena_allocator(&self) -> *mut pyffi::PyObjectArenaAllocator {
match &self.instance {
AllocatorInstance::Simple(_, arena) => arena as *const _ as *mut _,
AllocatorInstance::Tracking(alloc) => &alloc.arena as *const _ as *mut _,
}
}
}