From 56ff75848a951a5293a331afa7bf1af220b85d30 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 19:08:36 +0300 Subject: [PATCH 01/44] add `PyMemRawAllocator` --- Cargo.toml | 2 + src/lib.rs | 3 ++ src/pymem_alloc.rs | 126 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 src/pymem_alloc.rs diff --git a/Cargo.toml b/Cargo.toml index ae72c983804..c52a4004eb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -157,6 +157,8 @@ chrono-local = ["chrono/clock", "dep:iana-time-zone"] # Optimizes PyObject to Vec conversion and so on. nightly = [] +pymem-raw-alloc = [] + # Activates all additional features # This is mostly intended for testing purposes - activating *all* of these isn't particularly useful. full = [ diff --git a/src/lib.rs b/src/lib.rs index b9fce6cf463..8d1ebb2bf48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -480,6 +480,9 @@ pub mod inspect; // other paths to the same items. (e.g. `pyo3::types::PyAnyMethods` instead of `pyo3::prelude::PyAnyMethods`). pub mod prelude; +#[cfg(feature = "pymem-raw-alloc")] +pub mod pymem_alloc; + /// Test readme and user guide #[cfg(doctest)] pub mod doc_test { diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs new file mode 100644 index 00000000000..3c546c44958 --- /dev/null +++ b/src/pymem_alloc.rs @@ -0,0 +1,126 @@ +// TODO https://github.com/PyO3/pyo3/issues/5487 +#![allow(clippy::undocumented_unsafe_blocks)] + +//! GlobalAlloc backed by CPython's `PyMem_Raw*` (`PYMEM_DOMAIN_RAW`). +//! +//! ``` +//! use pyo3::pymem_alloc::PyMemRawAllocator; +//! +//! #[global_allocator] +//! static GLOBAL_ALLOCATOR: PyMemRawAllocator = PyMemRawAllocator; +//! ``` + +use core::{ + alloc::{GlobalAlloc, Layout}, + mem::size_of, + ptr, +}; + +/// `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` functions +/// (`PYMEM_DOMAIN_RAW`). Safe to use from any thread, attached or not, +/// since the raw domain doesn't require an attached thread state. +pub struct PyMemRawAllocator; + +// CPython documents this alignment as `ALIGNOF_MAX_ALIGN_T` +// (8 on Windows; autoconf-derived on Unix, not guaranteed >8 on every target/libc). +const MAX_ALIGN: usize = 8; + +/// Bytes reserved before an over-aligned block to stash the original +/// pointer returned by `PyMem_RawMalloc`, so it can be recovered for +/// `PyMem_RawFree` / `PyMem_RawRealloc`. +const HEADER: usize = size_of::<*mut u8>(); + +#[cold] +unsafe fn raw_alloc_aligned_with_header(layout: Layout) -> *mut u8 { + let Some(total) = layout + .size() + .checked_add(layout.align()) + .and_then(|total| total.checked_add(HEADER)) + else { + return ptr::null_mut(); + }; + + let raw = unsafe { pyo3_ffi::PyMem_RawMalloc(total) } as *mut u8; + + if raw.is_null() { + return ptr::null_mut(); + } + + unsafe { finish_aligned(raw, layout) } +} + +#[cold] +unsafe fn raw_calloc_aligned_with_header(layout: Layout) -> *mut u8 { + let Some(total) = layout + .size() + .checked_add(layout.align()) + .and_then(|total| total.checked_add(HEADER)) + else { + return ptr::null_mut(); + }; + let raw = unsafe { pyo3_ffi::PyMem_RawCalloc(1, total) } as *mut u8; + + if raw.is_null() { + return ptr::null_mut(); + } + unsafe { finish_aligned(raw, layout) } +} + +#[inline] +unsafe fn finish_aligned(raw: *mut u8, layout: Layout) -> *mut u8 { + let addr = raw as usize + HEADER; + let aligned_addr = (addr + layout.align() - 1) & !(layout.align() - 1); + let block = unsafe { raw.add(aligned_addr - raw as usize) }; + unsafe { (block.sub(HEADER) as *mut *mut u8).write_unaligned(raw) }; + + block +} + +#[inline] +unsafe fn recover_raw(ptr: *mut u8) -> *mut u8 { + unsafe { (ptr.sub(HEADER) as *mut *mut u8).read_unaligned() } +} + +unsafe impl GlobalAlloc for PyMemRawAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + if layout.align() <= MAX_ALIGN { + unsafe { pyo3_ffi::PyMem_RawMalloc(layout.size()) as *mut u8 } + } else { + unsafe { raw_alloc_aligned_with_header(layout) } + } + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + if layout.align() <= MAX_ALIGN { + unsafe { pyo3_ffi::PyMem_RawFree(ptr as *mut _) } + } else { + let raw = unsafe { recover_raw(ptr) }; + unsafe { pyo3_ffi::PyMem_RawFree(raw as *mut _) } + } + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + if layout.align() <= MAX_ALIGN { + unsafe { pyo3_ffi::PyMem_RawCalloc(1, layout.size()) as *mut u8 } + } else { + unsafe { raw_calloc_aligned_with_header(layout) } + } + } + + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) }; + + if layout.align() <= MAX_ALIGN && new_layout.align() <= MAX_ALIGN { + return unsafe { pyo3_ffi::PyMem_RawRealloc(ptr as *mut _, new_size) as *mut u8 }; + } + + let new_ptr = unsafe { self.alloc(new_layout) }; + if !new_ptr.is_null() { + unsafe { + ptr::copy_nonoverlapping(ptr, new_ptr, layout.size().min(new_size)); + self.dealloc(ptr, layout); + } + } + new_ptr + } +} From 27eab032a6795d0e8c4e81caef6e5128acd0cd58 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 19:25:03 +0300 Subject: [PATCH 02/44] docs --- guide/src/features.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/guide/src/features.md b/guide/src/features.md index c3a3c107094..4ac8fc79ff7 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -147,6 +147,32 @@ See [the `#[pyclass]` implementation details](class.md#implementation-details) f The `nightly` feature needs the nightly Rust compiler. This allows PyO3 to use the `auto_traits` and `negative_impls` features to fix the `Python::detach` function. +### `pymem-raw-alloc` + +This feature adds the `pyo3::pymem_alloc` module, providing `PyMemRawAllocator`, +a `GlobalAlloc` implementation backed by CPython's `PyMem_RawMalloc` / +`PyMem_RawCalloc` / `PyMem_RawRealloc` / `PyMem_RawFree` (the `PYMEM_DOMAIN_RAW` +allocator domain). + +Unlike `PyMem_Malloc` (`PYMEM_DOMAIN_MEM`), the raw domain does not require an +attached thread state, so it is safe to use as a process-wide `#[global_allocator]`, +including on threads that are never attached to the Python interpreter. + +Routing all Rust allocations through `PyMem_Raw*` gives `tracemalloc` visibility +into Rust-side memory usage for free, and can reduce allocator contention on the +free-threaded build compared to the system allocator, without pulling in a large +allocator such as `mimalloc`. + +This type is *not* registered as `#[global_allocator]` automatically; enabling +the feature only makes the type available, opt in explicitly: + +```rust,ignore +use pyo3::pymem_alloc::PyMemRawAllocator; + +#[global_allocator] +static ALLOCATOR: PyMemRawAllocator = PyMemRawAllocator; +``` + ## Optional Dependencies These features enable conversions between Python types and types from other Rust crates, enabling easy access to the rest of the Rust ecosystem. From 6c554a07743c2e66b1d575a7c1a6687372a06844 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 19:33:15 +0300 Subject: [PATCH 03/44] docs (x2) --- src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 8d1ebb2bf48..a02dc6d5856 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,8 +102,12 @@ //! - `multiple-pymethods`: Enables the use of multiple [`#[pymethods]`](macro@crate::pymethods) //! blocks per [`#[pyclass]`](macro@crate::pyclass). This adds a dependency on the [inventory] //! crate, which is not supported on all platforms. +//! - `pymem-raw-alloc`: Adds the [`pymem_alloc`] module, providing [`PyMemRawAllocator`], a +//! `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` allocator functions +//! (`PYMEM_DOMAIN_RAW`). Not registered as the global allocator automatically. //! //! The following features enable interactions with other crates in the Rust ecosystem: +//! //! - [`anyhow`]: Enables a conversion from [anyhow]’s [`Error`][anyhow_error] type to [`PyErr`]. //! - [`chrono`]: Enables a conversion from [chrono]'s structures to the equivalent Python ones. //! - [`chrono-tz`]: Enables a conversion from [chrono-tz]'s `Tz` enum. Requires Python 3.9+. From 781b2c3807b12b88734167993bd464884c04c578 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 19:34:48 +0300 Subject: [PATCH 04/44] add newsfragments --- newsfragments/6280.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/6280.added.md diff --git a/newsfragments/6280.added.md b/newsfragments/6280.added.md new file mode 100644 index 00000000000..30404ce4c54 --- /dev/null +++ b/newsfragments/6280.added.md @@ -0,0 +1 @@ +TODO \ No newline at end of file From eadec337c65646e49d9df634b66449e4c452de71 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 19:57:17 +0300 Subject: [PATCH 05/44] update MAX_ALIGN --- src/pymem_alloc.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 3c546c44958..e7db26fd644 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -21,9 +21,17 @@ use core::{ /// since the raw domain doesn't require an attached thread state. pub struct PyMemRawAllocator; -// CPython documents this alignment as `ALIGNOF_MAX_ALIGN_T` -// (8 on Windows; autoconf-derived on Unix, not guaranteed >8 on every target/libc). -const MAX_ALIGN: usize = 8; +// CPython documents this alignment as `ALIGNOF_MAX_ALIGN_T`/ +const MAX_ALIGN: usize = cfg_select! { + // Windows: 8 for both `MS_WIN32` / `MS_WIN64`. + target_os = "windows" => 8, + // macOS: 16 on Intel (`i386` / `x86_64`), + all(target_vendor = "apple", any(target_arch = "x86", target_arch = "x86_64")) => 16, + // macOS: 8 on other archs (e.g. `arm64`). + all(target_vendor = "apple", not(any(target_arch = "x86", target_arch = "x86_64"))) => 8, + // Other Unix: autoconf-derived at build time, not checked in, not guaranteed > 8. + _ => 8, +}; /// Bytes reserved before an over-aligned block to stash the original /// pointer returned by `PyMem_RawMalloc`, so it can be recovered for From 707c87350eb4a0c6e6a29c5d214308ff1dd56b68 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 20:00:54 +0300 Subject: [PATCH 06/44] update newsfragments --- newsfragments/6280.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/6280.added.md b/newsfragments/6280.added.md index 30404ce4c54..45cedbb941e 100644 --- a/newsfragments/6280.added.md +++ b/newsfragments/6280.added.md @@ -1 +1 @@ -TODO \ No newline at end of file +Added the `pymem-raw-alloc` feature, providing `pyo3::pymem_alloc::PyMemRawAllocator`, a `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` (`PYMEM_DOMAIN_RAW`) allocator functions. \ No newline at end of file From 0ae37ccea929aae4bc5fb7ae2c9fab51f5a3dda3 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 20:02:29 +0300 Subject: [PATCH 07/44] fix comment --- src/pymem_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index e7db26fd644..48133330224 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -21,7 +21,7 @@ use core::{ /// since the raw domain doesn't require an attached thread state. pub struct PyMemRawAllocator; -// CPython documents this alignment as `ALIGNOF_MAX_ALIGN_T`/ +// CPython documents this alignment as `ALIGNOF_MAX_ALIGN_T` const MAX_ALIGN: usize = cfg_select! { // Windows: 8 for both `MS_WIN32` / `MS_WIN64`. target_os = "windows" => 8, From c430046a0c329ed56c97226f2f3f608f75d2e990 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 20:02:46 +0300 Subject: [PATCH 08/44] update doc --- src/pymem_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 48133330224..0a3d136163c 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -1,7 +1,7 @@ // TODO https://github.com/PyO3/pyo3/issues/5487 #![allow(clippy::undocumented_unsafe_blocks)] -//! GlobalAlloc backed by CPython's `PyMem_Raw*` (`PYMEM_DOMAIN_RAW`). +//! `GlobalAlloc` backed by CPython's `PyMem_Raw*` (`PYMEM_DOMAIN_RAW`). //! //! ``` //! use pyo3::pymem_alloc::PyMemRawAllocator; From d5dc6760aeef94fe2367351d4cfd326842554b26 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 20:10:08 +0300 Subject: [PATCH 09/44] refactor --- src/pymem_alloc.rs | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 0a3d136163c..c022eee4c90 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -12,6 +12,7 @@ use core::{ alloc::{GlobalAlloc, Layout}, + ffi::c_void, mem::size_of, ptr, }; @@ -39,7 +40,10 @@ const MAX_ALIGN: usize = cfg_select! { const HEADER: usize = size_of::<*mut u8>(); #[cold] -unsafe fn raw_alloc_aligned_with_header(layout: Layout) -> *mut u8 { +unsafe fn raw_alloc_with_header( + layout: Layout, + alloc_fn: impl FnOnce(usize) -> *mut c_void, +) -> *mut u8 { let Some(total) = layout .size() .checked_add(layout.align()) @@ -48,7 +52,7 @@ unsafe fn raw_alloc_aligned_with_header(layout: Layout) -> *mut u8 { return ptr::null_mut(); }; - let raw = unsafe { pyo3_ffi::PyMem_RawMalloc(total) } as *mut u8; + let raw = unsafe { alloc_fn(total) } as *mut u8; if raw.is_null() { return ptr::null_mut(); @@ -57,23 +61,6 @@ unsafe fn raw_alloc_aligned_with_header(layout: Layout) -> *mut u8 { unsafe { finish_aligned(raw, layout) } } -#[cold] -unsafe fn raw_calloc_aligned_with_header(layout: Layout) -> *mut u8 { - let Some(total) = layout - .size() - .checked_add(layout.align()) - .and_then(|total| total.checked_add(HEADER)) - else { - return ptr::null_mut(); - }; - let raw = unsafe { pyo3_ffi::PyMem_RawCalloc(1, total) } as *mut u8; - - if raw.is_null() { - return ptr::null_mut(); - } - unsafe { finish_aligned(raw, layout) } -} - #[inline] unsafe fn finish_aligned(raw: *mut u8, layout: Layout) -> *mut u8 { let addr = raw as usize + HEADER; @@ -94,7 +81,7 @@ unsafe impl GlobalAlloc for PyMemRawAllocator { if layout.align() <= MAX_ALIGN { unsafe { pyo3_ffi::PyMem_RawMalloc(layout.size()) as *mut u8 } } else { - unsafe { raw_alloc_aligned_with_header(layout) } + unsafe { raw_alloc_with_header(layout, |total| pyo3_ffi::PyMem_RawMalloc(total)) } } } @@ -111,7 +98,7 @@ unsafe impl GlobalAlloc for PyMemRawAllocator { if layout.align() <= MAX_ALIGN { unsafe { pyo3_ffi::PyMem_RawCalloc(1, layout.size()) as *mut u8 } } else { - unsafe { raw_calloc_aligned_with_header(layout) } + unsafe { raw_alloc_with_header(layout, |total| pyo3_ffi::PyMem_RawCalloc(1, total)) } } } From 56dce48e52a7961c6c0574d805f3756d0db61039 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 20:11:14 +0300 Subject: [PATCH 10/44] remove unnecessary `unsafe` block --- src/pymem_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index c022eee4c90..9b1576721f2 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -52,7 +52,7 @@ unsafe fn raw_alloc_with_header( return ptr::null_mut(); }; - let raw = unsafe { alloc_fn(total) } as *mut u8; + let raw = alloc_fn(total) as *mut u8; if raw.is_null() { return ptr::null_mut(); From 2f1bdba83a0e7ee7885f661b92091a98073772c4 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 20:48:58 +0300 Subject: [PATCH 11/44] refactoring --- src/pymem_alloc.rs | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 9b1576721f2..f241b8bd090 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -23,7 +23,7 @@ use core::{ pub struct PyMemRawAllocator; // CPython documents this alignment as `ALIGNOF_MAX_ALIGN_T` -const MAX_ALIGN: usize = cfg_select! { +const MIN_ALIGN: usize = cfg_select! { // Windows: 8 for both `MS_WIN32` / `MS_WIN64`. target_os = "windows" => 8, // macOS: 16 on Intel (`i386` / `x86_64`), @@ -34,10 +34,12 @@ const MAX_ALIGN: usize = cfg_select! { _ => 8, }; -/// Bytes reserved before an over-aligned block to stash the original -/// pointer returned by `PyMem_RawMalloc`, so it can be recovered for -/// `PyMem_RawFree` / `PyMem_RawRealloc`. -const HEADER: usize = size_of::<*mut u8>(); +/// Original pointer returned by the allocator, stashed directly before +/// an over-aligned block. +/// +/// Recovered by [`recover_raw`] to pass back to `PyMem_RawFree` / `PyMem_RawRealloc`. +#[repr(transparent)] +struct Header(*mut u8); #[cold] unsafe fn raw_alloc_with_header( @@ -47,7 +49,7 @@ unsafe fn raw_alloc_with_header( let Some(total) = layout .size() .checked_add(layout.align()) - .and_then(|total| total.checked_add(HEADER)) + .and_then(|total| total.checked_add(size_of::
())) else { return ptr::null_mut(); }; @@ -63,22 +65,22 @@ unsafe fn raw_alloc_with_header( #[inline] unsafe fn finish_aligned(raw: *mut u8, layout: Layout) -> *mut u8 { - let addr = raw as usize + HEADER; - let aligned_addr = (addr + layout.align() - 1) & !(layout.align() - 1); + let addr = raw as usize + size_of::
(); + let mask = layout.align() - 1; + let aligned_addr = (addr + mask) & !mask; let block = unsafe { raw.add(aligned_addr - raw as usize) }; - unsafe { (block.sub(HEADER) as *mut *mut u8).write_unaligned(raw) }; - + unsafe { ptr::write((block as *mut Header).sub(1), Header(raw)) }; block } #[inline] unsafe fn recover_raw(ptr: *mut u8) -> *mut u8 { - unsafe { (ptr.sub(HEADER) as *mut *mut u8).read_unaligned() } + unsafe { ptr::read((ptr as *mut Header).sub(1)).0 } } unsafe impl GlobalAlloc for PyMemRawAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - if layout.align() <= MAX_ALIGN { + if layout.align() <= MIN_ALIGN { unsafe { pyo3_ffi::PyMem_RawMalloc(layout.size()) as *mut u8 } } else { unsafe { raw_alloc_with_header(layout, |total| pyo3_ffi::PyMem_RawMalloc(total)) } @@ -86,7 +88,7 @@ unsafe impl GlobalAlloc for PyMemRawAllocator { } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - if layout.align() <= MAX_ALIGN { + if layout.align() <= MIN_ALIGN { unsafe { pyo3_ffi::PyMem_RawFree(ptr as *mut _) } } else { let raw = unsafe { recover_raw(ptr) }; @@ -95,7 +97,7 @@ unsafe impl GlobalAlloc for PyMemRawAllocator { } unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { - if layout.align() <= MAX_ALIGN { + if layout.align() <= MIN_ALIGN { unsafe { pyo3_ffi::PyMem_RawCalloc(1, layout.size()) as *mut u8 } } else { unsafe { raw_alloc_with_header(layout, |total| pyo3_ffi::PyMem_RawCalloc(1, total)) } @@ -105,7 +107,7 @@ unsafe impl GlobalAlloc for PyMemRawAllocator { unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) }; - if layout.align() <= MAX_ALIGN && new_layout.align() <= MAX_ALIGN { + if layout.align() <= MIN_ALIGN && new_layout.align() <= MIN_ALIGN { return unsafe { pyo3_ffi::PyMem_RawRealloc(ptr as *mut _, new_size) as *mut u8 }; } From 8a1777c6769ed604dcb44564f1514429ac12621a Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 20:49:38 +0300 Subject: [PATCH 12/44] fmt --- src/pymem_alloc.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index f241b8bd090..430fb61338b 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -112,6 +112,7 @@ unsafe impl GlobalAlloc for PyMemRawAllocator { } let new_ptr = unsafe { self.alloc(new_layout) }; + if !new_ptr.is_null() { unsafe { ptr::copy_nonoverlapping(ptr, new_ptr, layout.size().min(new_size)); From 583d780a4d74bb89f6c5c9687709d21ab0ca89f7 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 20:58:33 +0300 Subject: [PATCH 13/44] rumdl fmt guide/src/features.md --- guide/src/features.md | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/guide/src/features.md b/guide/src/features.md index 4ac8fc79ff7..1b1e71ebeac 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -149,22 +149,13 @@ This allows PyO3 to use the `auto_traits` and `negative_impls` features to fix t ### `pymem-raw-alloc` -This feature adds the `pyo3::pymem_alloc` module, providing `PyMemRawAllocator`, -a `GlobalAlloc` implementation backed by CPython's `PyMem_RawMalloc` / -`PyMem_RawCalloc` / `PyMem_RawRealloc` / `PyMem_RawFree` (the `PYMEM_DOMAIN_RAW` -allocator domain). - -Unlike `PyMem_Malloc` (`PYMEM_DOMAIN_MEM`), the raw domain does not require an -attached thread state, so it is safe to use as a process-wide `#[global_allocator]`, -including on threads that are never attached to the Python interpreter. - -Routing all Rust allocations through `PyMem_Raw*` gives `tracemalloc` visibility -into Rust-side memory usage for free, and can reduce allocator contention on the -free-threaded build compared to the system allocator, without pulling in a large -allocator such as `mimalloc`. - -This type is *not* registered as `#[global_allocator]` automatically; enabling -the feature only makes the type available, opt in explicitly: +This feature adds the `pyo3::pymem_alloc` module, providing `PyMemRawAllocator`, a `GlobalAlloc` implementation backed by CPython's `PyMem_RawMalloc` / `PyMem_RawCalloc` / `PyMem_RawRealloc` / `PyMem_RawFree` (the `PYMEM_DOMAIN_RAW` allocator domain). + +Unlike `PyMem_Malloc` (`PYMEM_DOMAIN_MEM`), the raw domain does not require an attached thread state, so it is safe to use as a process-wide `#[global_allocator]`, including on threads that are never attached to the Python interpreter. + +Routing all Rust allocations through `PyMem_Raw*` gives `tracemalloc` visibility into Rust-side memory usage for free, and can reduce allocator contention on the free-threaded build compared to the system allocator, without pulling in a large allocator such as `mimalloc`. + +This type is *not* registered as `#[global_allocator]` automatically; enabling the feature only makes the type available, opt in explicitly: ```rust,ignore use pyo3::pymem_alloc::PyMemRawAllocator; From 61f76db148c5a24d894e557bb29a6a957ec03d5c Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 21:07:32 +0300 Subject: [PATCH 14/44] fix newsfragments --- newsfragments/{6280.added.md => 6279.added.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename newsfragments/{6280.added.md => 6279.added.md} (100%) diff --git a/newsfragments/6280.added.md b/newsfragments/6279.added.md similarity index 100% rename from newsfragments/6280.added.md rename to newsfragments/6279.added.md From e3806cb780c8d83715b8caf32975bcc738c99a4c Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 21:14:58 +0300 Subject: [PATCH 15/44] fix --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index c52a4004eb1..f79a0e43be9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -185,6 +185,7 @@ full = [ "ordered-float", "parking_lot", "py-clone", + "pymem-raw-alloc", "rust_decimal", "serde", "smallvec", From 7ec0df4c903e9ef39221d4eac774a50222ca4cdf Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 21:25:30 +0300 Subject: [PATCH 16/44] try fix --- src/lib.rs | 7 ++++--- src/pymem_alloc.rs | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a02dc6d5856..0b0e33cb9f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,9 +102,10 @@ //! - `multiple-pymethods`: Enables the use of multiple [`#[pymethods]`](macro@crate::pymethods) //! blocks per [`#[pyclass]`](macro@crate::pyclass). This adds a dependency on the [inventory] //! crate, which is not supported on all platforms. -//! - `pymem-raw-alloc`: Adds the [`pymem_alloc`] module, providing [`PyMemRawAllocator`], a -//! `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` allocator functions -//! (`PYMEM_DOMAIN_RAW`). Not registered as the global allocator automatically. +//! - `pymem-raw-alloc`: Adds the [`pymem_alloc`] module, providing +//! [`PyMemRawAllocator`](pymem_alloc::PyMemRawAllocator), a `GlobalAlloc` implementation backed by +//! CPython's `PyMem_Raw*` allocator functions (`PYMEM_DOMAIN_RAW`). +//! Not registered as the global allocator automatically. //! //! The following features enable interactions with other crates in the Rust ecosystem: //! diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 430fb61338b..6ae0517f838 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -105,9 +105,11 @@ unsafe impl GlobalAlloc for PyMemRawAllocator { } unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + // `GlobalAlloc::realloc`'s contract guarantees `align` is unchanged between calls, + // so it's enough to check the (single, shared) alignment once. let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) }; - if layout.align() <= MIN_ALIGN && new_layout.align() <= MIN_ALIGN { + if layout.align() <= MIN_ALIGN { return unsafe { pyo3_ffi::PyMem_RawRealloc(ptr as *mut _, new_size) as *mut u8 }; } From bcc71cbfcd5238e63ca6d651994ae320747cc150 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 21:36:32 +0300 Subject: [PATCH 17/44] add tests --- src/pymem_alloc.rs | 156 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 6ae0517f838..4e5d8f81577 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -124,3 +124,159 @@ unsafe impl GlobalAlloc for PyMemRawAllocator { new_ptr } } + +#[cfg(test)] +mod tests { + use super::*; + + use alloc::vec::Vec; + + unsafe fn assert_zeroes(ptr: *mut u8, layout: Layout) { + unsafe { + for ofs in 0..layout.size() { + assert_eq!(0, ptr.add(ofs).read()); + } + } + } + + #[test] + fn alloc_dealloc_small_align() { + let alloc = PyMemRawAllocator; + let layout = Layout::from_size_align(64, MIN_ALIGN).unwrap(); + unsafe { + let ptr = alloc.alloc(layout); + assert!(!ptr.is_null()); + assert_eq!(ptr.addr() % layout.align(), 0); + ptr.write_bytes(0xAB, layout.size()); + alloc.dealloc(ptr, layout); + } + } + + #[test] + fn alloc_dealloc_over_aligned() { + let alloc = PyMemRawAllocator; + for align in [MIN_ALIGN * 2, 64, 256, 4096] { + let layout = Layout::from_size_align(128, align).unwrap(); + unsafe { + let ptr = alloc.alloc(layout); + assert!(!ptr.is_null()); + assert_eq!(ptr.addr() % align, 0, "align={align}"); + ptr.write_bytes(0xCD, layout.size()); + alloc.dealloc(ptr, layout); + } + } + } + + #[test] + fn alloc_zeroed_small_align() { + let alloc = PyMemRawAllocator; + let layout = Layout::from_size_align(256, MIN_ALIGN).unwrap(); + unsafe { + let ptr = alloc.alloc_zeroed(layout); + assert!(!ptr.is_null()); + assert_zeroes(ptr, layout); + alloc.dealloc(ptr, layout); + } + } + + #[test] + fn alloc_zeroed_over_aligned() { + let alloc = PyMemRawAllocator; + let layout = Layout::from_size_align(4096, 64).unwrap(); + unsafe { + let ptr = alloc.alloc_zeroed(layout); + assert!(!ptr.is_null()); + assert_eq!(ptr.addr() % layout.align(), 0); + assert_zeroes(ptr, layout); + alloc.dealloc(ptr, layout); + } + } + + #[test] + fn realloc_grow_preserves_data_small_align() { + let alloc = PyMemRawAllocator; + let old_layout = Layout::from_size_align(16, MIN_ALIGN).unwrap(); + unsafe { + let ptr = alloc.alloc(old_layout); + assert!(!ptr.is_null()); + ptr.write_bytes(0x42, old_layout.size()); + + let new_ptr = alloc.realloc(ptr, old_layout, 256); + assert!(!new_ptr.is_null()); + for i in 0..old_layout.size() { + assert_eq!(new_ptr.add(i).read(), 0x42); + } + alloc.dealloc(new_ptr, Layout::from_size_align(256, MIN_ALIGN).unwrap()); + } + } + + #[test] + fn realloc_shrink_preserves_data_small_align() { + let alloc = PyMemRawAllocator; + let old_layout = Layout::from_size_align(256, MIN_ALIGN).unwrap(); + unsafe { + let ptr = alloc.alloc(old_layout); + assert!(!ptr.is_null()); + ptr.write_bytes(0x7A, old_layout.size()); + + let new_ptr = alloc.realloc(ptr, old_layout, 16); + assert!(!new_ptr.is_null()); + for i in 0..16 { + assert_eq!(new_ptr.add(i).read(), 0x7A); + } + alloc.dealloc(new_ptr, Layout::from_size_align(16, MIN_ALIGN).unwrap()); + } + } + + #[test] + fn realloc_over_aligned_preserves_data() { + let alloc = PyMemRawAllocator; + let align = 64; + let old_layout = Layout::from_size_align(32, align).unwrap(); + unsafe { + let ptr = alloc.alloc(old_layout); + assert!(!ptr.is_null()); + ptr.write_bytes(0x55, old_layout.size()); + + let new_ptr = alloc.realloc(ptr, old_layout, 512); + assert!(!new_ptr.is_null()); + assert_eq!(new_ptr.addr() % align, 0); + for i in 0..old_layout.size() { + assert_eq!(new_ptr.add(i).read(), 0x55); + } + alloc.dealloc(new_ptr, Layout::from_size_align(512, align).unwrap()); + } + } + + #[test] + fn no_overlap_between_allocations() { + let alloc = PyMemRawAllocator; + let layouts: Vec = vec![ + Layout::from_size_align(16, MIN_ALIGN).unwrap(), + Layout::from_size_align(128, 64).unwrap(), + Layout::from_size_align(4096, 4096).unwrap(), + Layout::from_size_align(64, MIN_ALIGN).unwrap(), + ]; + unsafe { + let ptrs: Vec<*mut u8> = layouts + .iter() + .map(|&layout| { + let ptr = alloc.alloc_zeroed(layout); + assert!(!ptr.is_null()); + ptr.write_bytes(0xEE, layout.size()); + ptr + }) + .collect(); + + for (&ptr, &layout) in ptrs.iter().zip(&layouts) { + for i in 0..layout.size() { + assert_eq!(ptr.add(i).read(), 0xEE); + } + } + + for (ptr, layout) in ptrs.into_iter().zip(layouts) { + alloc.dealloc(ptr, layout); + } + } + } +} From e388cec4c10b62c71bf03d0216d41a2e4460cbe0 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 21:42:00 +0300 Subject: [PATCH 18/44] remove whitespaces --- src/pymem_alloc.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 4e5d8f81577..f1ac219283b 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -17,8 +17,8 @@ use core::{ ptr, }; -/// `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` functions -/// (`PYMEM_DOMAIN_RAW`). Safe to use from any thread, attached or not, +/// `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` functions +/// (`PYMEM_DOMAIN_RAW`). Safe to use from any thread, attached or not, /// since the raw domain doesn't require an attached thread state. pub struct PyMemRawAllocator; From d53be71954ba9c055b3ce07f580ac865a3e00167 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 21:45:38 +0300 Subject: [PATCH 19/44] more test --- Cargo.toml | 4 ++++ tests/test_pymem_alloc.rs | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 tests/test_pymem_alloc.rs diff --git a/Cargo.toml b/Cargo.toml index f79a0e43be9..97ad23b6cdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -436,3 +436,7 @@ required-features = ["macros"] [[test]] name = "test_various" required-features = ["macros"] + +[[test]] +name = "test_pymem_alloc" +required-features = ["pymem-raw-alloc"] diff --git a/tests/test_pymem_alloc.rs b/tests/test_pymem_alloc.rs new file mode 100644 index 00000000000..ca87ff15a0e --- /dev/null +++ b/tests/test_pymem_alloc.rs @@ -0,0 +1,22 @@ +#![cfg(feature = "pymem-raw-alloc")] + +use pyo3::pymem_alloc::PyMemRawAllocator; + +#[global_allocator] +static GLOBAL: PyMemRawAllocator = PyMemRawAllocator; + +#[test] +fn allocations_work_through_global_allocator() { + let mut vec: Vec = Vec::with_capacity(1024); + vec.extend_from_slice(&[1, 2, 3, 4]); + assert_eq!(vec, vec![1, 2, 3, 4]); + + let s = String::from("hello"); + assert_eq!(s.len(), 4); + + #[repr(align(64))] + struct Aligned([u8; 128]); + let boxed = Box::new(Aligned([7u8; 128])); + assert_eq!(boxed.0[0], 7); + assert_eq!((&*boxed as *const Aligned).addr() % 64, 0); +} From 24e5953257ab0534a088ccf8ad6789d4f0c18237 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 22:18:01 +0300 Subject: [PATCH 20/44] fix cfg --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 0b0e33cb9f5..3fb537dea68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -485,7 +485,7 @@ pub mod inspect; // other paths to the same items. (e.g. `pyo3::types::PyAnyMethods` instead of `pyo3::prelude::PyAnyMethods`). pub mod prelude; -#[cfg(feature = "pymem-raw-alloc")] +#[cfg(all(feature = "pymem-raw-alloc", not(Py_LIMITED_API)))] pub mod pymem_alloc; /// Test readme and user guide From ddf34d83c316db22256f62d0be90b7f84bb2c38b Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 22:27:45 +0300 Subject: [PATCH 21/44] GLOBAL -> GLOBAL_ALLOCATOR --- tests/test_pymem_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pymem_alloc.rs b/tests/test_pymem_alloc.rs index ca87ff15a0e..68877cf1558 100644 --- a/tests/test_pymem_alloc.rs +++ b/tests/test_pymem_alloc.rs @@ -3,7 +3,7 @@ use pyo3::pymem_alloc::PyMemRawAllocator; #[global_allocator] -static GLOBAL: PyMemRawAllocator = PyMemRawAllocator; +static GLOBAL_ALLOCATOR: PyMemRawAllocator = PyMemRawAllocator; #[test] fn allocations_work_through_global_allocator() { From 81ed81cd50b320e63a81fddc4323a4224c5c1927 Mon Sep 17 00:00:00 2001 From: chiri Date: Fri, 31 Jul 2026 22:40:31 +0300 Subject: [PATCH 22/44] remove feature --- Cargo.toml | 5 +---- guide/src/features.md | 17 ----------------- newsfragments/6279.added.md | 2 +- src/lib.rs | 6 +----- tests/test_pymem_alloc.rs | 2 -- 5 files changed, 3 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 97ad23b6cdd..bbb92102cba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -157,8 +157,6 @@ chrono-local = ["chrono/clock", "dep:iana-time-zone"] # Optimizes PyObject to Vec conversion and so on. nightly = [] -pymem-raw-alloc = [] - # Activates all additional features # This is mostly intended for testing purposes - activating *all* of these isn't particularly useful. full = [ @@ -185,7 +183,6 @@ full = [ "ordered-float", "parking_lot", "py-clone", - "pymem-raw-alloc", "rust_decimal", "serde", "smallvec", @@ -439,4 +436,4 @@ required-features = ["macros"] [[test]] name = "test_pymem_alloc" -required-features = ["pymem-raw-alloc"] +required-features = ["macros"] diff --git a/guide/src/features.md b/guide/src/features.md index 1b1e71ebeac..c3a3c107094 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -147,23 +147,6 @@ See [the `#[pyclass]` implementation details](class.md#implementation-details) f The `nightly` feature needs the nightly Rust compiler. This allows PyO3 to use the `auto_traits` and `negative_impls` features to fix the `Python::detach` function. -### `pymem-raw-alloc` - -This feature adds the `pyo3::pymem_alloc` module, providing `PyMemRawAllocator`, a `GlobalAlloc` implementation backed by CPython's `PyMem_RawMalloc` / `PyMem_RawCalloc` / `PyMem_RawRealloc` / `PyMem_RawFree` (the `PYMEM_DOMAIN_RAW` allocator domain). - -Unlike `PyMem_Malloc` (`PYMEM_DOMAIN_MEM`), the raw domain does not require an attached thread state, so it is safe to use as a process-wide `#[global_allocator]`, including on threads that are never attached to the Python interpreter. - -Routing all Rust allocations through `PyMem_Raw*` gives `tracemalloc` visibility into Rust-side memory usage for free, and can reduce allocator contention on the free-threaded build compared to the system allocator, without pulling in a large allocator such as `mimalloc`. - -This type is *not* registered as `#[global_allocator]` automatically; enabling the feature only makes the type available, opt in explicitly: - -```rust,ignore -use pyo3::pymem_alloc::PyMemRawAllocator; - -#[global_allocator] -static ALLOCATOR: PyMemRawAllocator = PyMemRawAllocator; -``` - ## Optional Dependencies These features enable conversions between Python types and types from other Rust crates, enabling easy access to the rest of the Rust ecosystem. diff --git a/newsfragments/6279.added.md b/newsfragments/6279.added.md index 45cedbb941e..0f6be379b56 100644 --- a/newsfragments/6279.added.md +++ b/newsfragments/6279.added.md @@ -1 +1 @@ -Added the `pymem-raw-alloc` feature, providing `pyo3::pymem_alloc::PyMemRawAllocator`, a `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` (`PYMEM_DOMAIN_RAW`) allocator functions. \ No newline at end of file +Added `pyo3::pymem_alloc::PyMemRawAllocator`, a `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` (`PYMEM_DOMAIN_RAW`) allocator functions. Not registered as the global allocator automatically. \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 3fb537dea68..e9307813f61 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,10 +102,6 @@ //! - `multiple-pymethods`: Enables the use of multiple [`#[pymethods]`](macro@crate::pymethods) //! blocks per [`#[pyclass]`](macro@crate::pyclass). This adds a dependency on the [inventory] //! crate, which is not supported on all platforms. -//! - `pymem-raw-alloc`: Adds the [`pymem_alloc`] module, providing -//! [`PyMemRawAllocator`](pymem_alloc::PyMemRawAllocator), a `GlobalAlloc` implementation backed by -//! CPython's `PyMem_Raw*` allocator functions (`PYMEM_DOMAIN_RAW`). -//! Not registered as the global allocator automatically. //! //! The following features enable interactions with other crates in the Rust ecosystem: //! @@ -485,7 +481,7 @@ pub mod inspect; // other paths to the same items. (e.g. `pyo3::types::PyAnyMethods` instead of `pyo3::prelude::PyAnyMethods`). pub mod prelude; -#[cfg(all(feature = "pymem-raw-alloc", not(Py_LIMITED_API)))] +#[cfg(not(Py_LIMITED_API))] pub mod pymem_alloc; /// Test readme and user guide diff --git a/tests/test_pymem_alloc.rs b/tests/test_pymem_alloc.rs index 68877cf1558..a38ba3377f4 100644 --- a/tests/test_pymem_alloc.rs +++ b/tests/test_pymem_alloc.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "pymem-raw-alloc")] - use pyo3::pymem_alloc::PyMemRawAllocator; #[global_allocator] From 0aa7a0785cb23c594396c21066da238edddcffec Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 00:16:14 +0300 Subject: [PATCH 23/44] fix --- Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bbb92102cba..ae72c983804 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -433,7 +433,3 @@ required-features = ["macros"] [[test]] name = "test_various" required-features = ["macros"] - -[[test]] -name = "test_pymem_alloc" -required-features = ["macros"] From 429bb8a6c1b7bc66c7222a19d102f27e6b94d05e Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 00:39:39 +0300 Subject: [PATCH 24/44] add safety comments --- src/pymem_alloc.rs | 64 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index f1ac219283b..4b0897fc8b1 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -1,6 +1,3 @@ -// TODO https://github.com/PyO3/pyo3/issues/5487 -#![allow(clippy::undocumented_unsafe_blocks)] - //! `GlobalAlloc` backed by CPython's `PyMem_Raw*` (`PYMEM_DOMAIN_RAW`). //! //! ``` @@ -17,16 +14,16 @@ use core::{ ptr, }; -/// `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` functions -/// (`PYMEM_DOMAIN_RAW`). Safe to use from any thread, attached or not, -/// since the raw domain doesn't require an attached thread state. +/// `GlobalAlloc` implementation backed by CPython's `PyMem_Raw*` functions (`PYMEM_DOMAIN_RAW`). +/// Safe to use from any thread, attached or not, since the raw domain doesn't require an attached +/// thread state. pub struct PyMemRawAllocator; // CPython documents this alignment as `ALIGNOF_MAX_ALIGN_T` const MIN_ALIGN: usize = cfg_select! { // Windows: 8 for both `MS_WIN32` / `MS_WIN64`. target_os = "windows" => 8, - // macOS: 16 on Intel (`i386` / `x86_64`), + // macOS: 16 on Intel (`i386` / `x86_64`). all(target_vendor = "apple", any(target_arch = "x86", target_arch = "x86_64")) => 16, // macOS: 8 on other archs (e.g. `arm64`). all(target_vendor = "apple", not(any(target_arch = "x86", target_arch = "x86_64"))) => 8, @@ -34,13 +31,18 @@ const MIN_ALIGN: usize = cfg_select! { _ => 8, }; -/// Original pointer returned by the allocator, stashed directly before -/// an over-aligned block. +/// Header stashing the original allocation pointer before an over-aligned block. /// /// Recovered by [`recover_raw`] to pass back to `PyMem_RawFree` / `PyMem_RawRealloc`. #[repr(transparent)] struct Header(*mut u8); +/// Allocates memory for `layout` with a [`Header`] stashed before an +/// over-aligned block. Returns null on overflow or allocation failure. +/// +/// # Safety +/// +/// `alloc_fn` must behave like `PyMem_RawMalloc`/`PyMem_RawCalloc`. #[cold] unsafe fn raw_alloc_with_header( layout: Layout, @@ -60,62 +62,89 @@ unsafe fn raw_alloc_with_header( return ptr::null_mut(); } + // SAFETY: `raw` is valid for `total` bytes, enough for `layout` plus a `Header`. unsafe { finish_aligned(raw, layout) } } +/// Writes a [`Header`] before the aligned block within a `raw_alloc_with_header` allocation. +/// +/// # Safety +/// +/// `raw` must point to an allocation of at least `layout.size() + layout.align() + +/// size_of::
()` bytes. #[inline] unsafe fn finish_aligned(raw: *mut u8, layout: Layout) -> *mut u8 { let addr = raw as usize + size_of::
(); let mask = layout.align() - 1; let aligned_addr = (addr + mask) & !mask; let block = unsafe { raw.add(aligned_addr - raw as usize) }; + // SAFETY: `block` is within the allocation and has at least `size_of::
()` + // bytes of padding before it, so writing a `Header` there is in-bounds. unsafe { ptr::write((block as *mut Header).sub(1), Header(raw)) }; block } +/// Recovers the original allocation pointer stashed by [`finish_aligned`] before `ptr`. +/// +/// # Safety +/// +/// `ptr` must have been returned by [`finish_aligned`], with its `Header` still intact. #[inline] unsafe fn recover_raw(ptr: *mut u8) -> *mut u8 { + // SAFETY: `ptr` has a `Header` written directly before it by `finish_aligned`. unsafe { ptr::read((ptr as *mut Header).sub(1)).0 } } unsafe impl GlobalAlloc for PyMemRawAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { if layout.align() <= MIN_ALIGN { + // SAFETY: `PyMem_RawMalloc` accepts any size, including 0. unsafe { pyo3_ffi::PyMem_RawMalloc(layout.size()) as *mut u8 } } else { + // SAFETY: `PyMem_RawMalloc` accepts any size, including 0. unsafe { raw_alloc_with_header(layout, |total| pyo3_ffi::PyMem_RawMalloc(total)) } } } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { if layout.align() <= MIN_ALIGN { + // SAFETY: `ptr` was allocated by `PyMem_RawMalloc`/`PyMem_RawCalloc` with this layout. unsafe { pyo3_ffi::PyMem_RawFree(ptr as *mut _) } } else { + // SAFETY: `ptr` was returned by `finish_aligned`, so it has a `Header` before it. let raw = unsafe { recover_raw(ptr) }; + // SAFETY: `raw` was allocated by `PyMem_RawMalloc`/`PyMem_RawCalloc`. unsafe { pyo3_ffi::PyMem_RawFree(raw as *mut _) } } } unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { if layout.align() <= MIN_ALIGN { + // SAFETY: `PyMem_RawCalloc` accepts any size, including 0. unsafe { pyo3_ffi::PyMem_RawCalloc(1, layout.size()) as *mut u8 } } else { + // SAFETY: `PyMem_RawCalloc` accepts any size, including 0. unsafe { raw_alloc_with_header(layout, |total| pyo3_ffi::PyMem_RawCalloc(1, total)) } } } unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { - // `GlobalAlloc::realloc`'s contract guarantees `align` is unchanged between calls, - // so it's enough to check the (single, shared) alignment once. + // SAFETY: `new_size` is non-zero per the `GlobalAlloc::realloc` contract, and + // `layout.align()` is unchanged. let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) }; if layout.align() <= MIN_ALIGN { + // SAFETY: `ptr` was allocated with `layout`, `PyMem_RawRealloc` handles the resize. return unsafe { pyo3_ffi::PyMem_RawRealloc(ptr as *mut _, new_size) as *mut u8 }; } + // SAFETY: `new_layout` has non-zero size and the caller-guaranteed alignment. let new_ptr = unsafe { self.alloc(new_layout) }; if !new_ptr.is_null() { + // SAFETY: `ptr` is valid for `layout.size()` bytes, `new_ptr` for `new_size` bytes, + // and they don't overlap since `new_ptr` is a fresh allocation. `ptr`/`layout` + // match the original allocation, as required by `dealloc`. unsafe { ptr::copy_nonoverlapping(ptr, new_ptr, layout.size().min(new_size)); self.dealloc(ptr, layout); @@ -131,6 +160,7 @@ mod tests { use alloc::vec::Vec; + // SAFETY: `ptr` must be valid for reads of `layout.size()` bytes. unsafe fn assert_zeroes(ptr: *mut u8, layout: Layout) { unsafe { for ofs in 0..layout.size() { @@ -143,6 +173,7 @@ mod tests { fn alloc_dealloc_small_align() { let alloc = PyMemRawAllocator; let layout = Layout::from_size_align(64, MIN_ALIGN).unwrap(); + // SAFETY: `layout` has non-zero size; `ptr` is deallocated with the same layout. unsafe { let ptr = alloc.alloc(layout); assert!(!ptr.is_null()); @@ -157,6 +188,7 @@ mod tests { let alloc = PyMemRawAllocator; for align in [MIN_ALIGN * 2, 64, 256, 4096] { let layout = Layout::from_size_align(128, align).unwrap(); + // SAFETY: `layout` has non-zero size; `ptr` is deallocated with the same layout. unsafe { let ptr = alloc.alloc(layout); assert!(!ptr.is_null()); @@ -171,6 +203,7 @@ mod tests { fn alloc_zeroed_small_align() { let alloc = PyMemRawAllocator; let layout = Layout::from_size_align(256, MIN_ALIGN).unwrap(); + // SAFETY: `layout` has non-zero size; `ptr` is deallocated with the same layout. unsafe { let ptr = alloc.alloc_zeroed(layout); assert!(!ptr.is_null()); @@ -183,6 +216,7 @@ mod tests { fn alloc_zeroed_over_aligned() { let alloc = PyMemRawAllocator; let layout = Layout::from_size_align(4096, 64).unwrap(); + // SAFETY: `layout` has non-zero size; `ptr` is deallocated with the same layout. unsafe { let ptr = alloc.alloc_zeroed(layout); assert!(!ptr.is_null()); @@ -196,6 +230,8 @@ mod tests { fn realloc_grow_preserves_data_small_align() { let alloc = PyMemRawAllocator; let old_layout = Layout::from_size_align(16, MIN_ALIGN).unwrap(); + // SAFETY: `old_layout` has non-zero size; `new_ptr`/`ptr` are always + // deallocated with the layout they were allocated/reallocated with. unsafe { let ptr = alloc.alloc(old_layout); assert!(!ptr.is_null()); @@ -214,6 +250,8 @@ mod tests { fn realloc_shrink_preserves_data_small_align() { let alloc = PyMemRawAllocator; let old_layout = Layout::from_size_align(256, MIN_ALIGN).unwrap(); + // SAFETY: `old_layout` has non-zero size; `new_size` is non-zero and + // `new_ptr` is deallocated with the layout it was reallocated with. unsafe { let ptr = alloc.alloc(old_layout); assert!(!ptr.is_null()); @@ -233,6 +271,8 @@ mod tests { let alloc = PyMemRawAllocator; let align = 64; let old_layout = Layout::from_size_align(32, align).unwrap(); + // SAFETY: `old_layout` has non-zero size; `new_ptr` is deallocated + // with the layout it was reallocated with. unsafe { let ptr = alloc.alloc(old_layout); assert!(!ptr.is_null()); @@ -257,6 +297,8 @@ mod tests { Layout::from_size_align(4096, 4096).unwrap(), Layout::from_size_align(64, MIN_ALIGN).unwrap(), ]; + // SAFETY: each `layout` has non-zero size; each `ptr` is deallocated + // with the same layout it was allocated with. unsafe { let ptrs: Vec<*mut u8> = layouts .iter() From 1c9b8a358afe699e9a22e3571d4e5069aa9fad6e Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 00:44:46 +0300 Subject: [PATCH 25/44] update safety comments --- src/pymem_alloc.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 4b0897fc8b1..8f69d2f2110 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -134,7 +134,10 @@ unsafe impl GlobalAlloc for PyMemRawAllocator { let new_layout = unsafe { Layout::from_size_align_unchecked(new_size, layout.align()) }; if layout.align() <= MIN_ALIGN { - // SAFETY: `ptr` was allocated with `layout`, `PyMem_RawRealloc` handles the resize. + // SAFETY: CPython's `PyMem_RawRealloc` contract mirrors `PyMem_RawFree`: + // `ptr` must come from a prior `PyMem_Raw{Malloc,Realloc,Calloc}` call. + // + // See: https://docs.python.org/3/c-api/memory.html#c.PyMem_RawRealloc return unsafe { pyo3_ffi::PyMem_RawRealloc(ptr as *mut _, new_size) as *mut u8 }; } From e416c10838e6227e4afadb003b361a818cbb5fca Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 00:49:33 +0300 Subject: [PATCH 26/44] update safety comments, add cpython docs links --- src/pymem_alloc.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 8f69d2f2110..52e234756e2 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -98,32 +98,49 @@ unsafe fn recover_raw(ptr: *mut u8) -> *mut u8 { unsafe impl GlobalAlloc for PyMemRawAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { if layout.align() <= MIN_ALIGN { - // SAFETY: `PyMem_RawMalloc` accepts any size, including 0. + // SAFETY: `PyMem_RawMalloc` accepts any size, including 0, returning a + // distinct non-NULL pointer as if `PyMem_RawMalloc(1)` were called. + // + // See: https://docs.python.org/3/c-api/memory.html#c.PyMem_RawMalloc unsafe { pyo3_ffi::PyMem_RawMalloc(layout.size()) as *mut u8 } } else { - // SAFETY: `PyMem_RawMalloc` accepts any size, including 0. + // SAFETY: same zero-size guarantee as above. + // + // See: https://docs.python.org/3/c-api/memory.html#c.PyMem_RawMalloc unsafe { raw_alloc_with_header(layout, |total| pyo3_ffi::PyMem_RawMalloc(total)) } } } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { if layout.align() <= MIN_ALIGN { - // SAFETY: `ptr` was allocated by `PyMem_RawMalloc`/`PyMem_RawCalloc` with this layout. + // SAFETY: `ptr` must have been returned by a prior `PyMem_RawMalloc`, + // `PyMem_RawRealloc`, or `PyMem_RawCalloc` call, and must not have + // been freed before. + // + // See: https://docs.python.org/3/c-api/memory.html#c.PyMem_RawFree unsafe { pyo3_ffi::PyMem_RawFree(ptr as *mut _) } } else { // SAFETY: `ptr` was returned by `finish_aligned`, so it has a `Header` before it. let raw = unsafe { recover_raw(ptr) }; - // SAFETY: `raw` was allocated by `PyMem_RawMalloc`/`PyMem_RawCalloc`. + // SAFETY: `raw` is the original pointer from `PyMem_RawMalloc`/`PyMem_RawCalloc`, + // which is what `PyMem_RawFree` requires. + // + // See: https://docs.python.org/3/c-api/memory.html#c.PyMem_RawFree unsafe { pyo3_ffi::PyMem_RawFree(raw as *mut _) } } } unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { if layout.align() <= MIN_ALIGN { - // SAFETY: `PyMem_RawCalloc` accepts any size, including 0. + // SAFETY: `PyMem_RawCalloc` accepts any size, including 0, returning a + // distinct non-NULL pointer as if `PyMem_RawCalloc(1, 1)` were called. + // + // See: https://docs.python.org/3/c-api/memory.html#c.PyMem_RawCalloc unsafe { pyo3_ffi::PyMem_RawCalloc(1, layout.size()) as *mut u8 } } else { - // SAFETY: `PyMem_RawCalloc` accepts any size, including 0. + // SAFETY: same zero-size guarantee as above. + // + // See: https://docs.python.org/3/c-api/memory.html#c.PyMem_RawCalloc unsafe { raw_alloc_with_header(layout, |total| pyo3_ffi::PyMem_RawCalloc(1, total)) } } } From 662411806a436311b9f8cad12c42fd59eae7bcad Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 01:27:31 +0300 Subject: [PATCH 27/44] sync `pymem.h` with 3.15 branch --- pyo3-ffi/src/cpython/pymem.rs | 16 --------------- pyo3-ffi/src/pymem.rs | 37 +++++++++++++++++++++++++++-------- src/lib.rs | 2 +- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/pyo3-ffi/src/cpython/pymem.rs b/pyo3-ffi/src/cpython/pymem.rs index 808e4a5b5d0..f12a6b5832a 100644 --- a/pyo3-ffi/src/cpython/pymem.rs +++ b/pyo3-ffi/src/cpython/pymem.rs @@ -1,22 +1,6 @@ use core::ffi::c_void; use libc::size_t; -extern_libpython! { - #[cfg_attr(PyPy, link_name = "PyPyMem_RawMalloc")] - pub fn PyMem_RawMalloc(size: size_t) -> *mut c_void; - #[cfg_attr(PyPy, link_name = "PyPyMem_RawCalloc")] - pub fn PyMem_RawCalloc(nelem: size_t, elsize: size_t) -> *mut c_void; - #[cfg_attr(PyPy, link_name = "PyPyMem_RawRealloc")] - pub fn PyMem_RawRealloc(ptr: *mut c_void, new_size: size_t) -> *mut c_void; - #[cfg_attr(PyPy, link_name = "PyPyMem_RawFree")] - pub fn PyMem_RawFree(ptr: *mut c_void); - - // skipped _PyMem_GetCurrentAllocatorName - // skipped _PyMem_RawStrdup - // skipped _PyMem_Strdup - // skipped _PyMem_RawWcsdup -} - #[repr(C)] #[derive(Copy, Clone)] pub enum PyMemAllocatorDomain { diff --git a/pyo3-ffi/src/pymem.rs b/pyo3-ffi/src/pymem.rs index 45e57ef1db6..98c05e78dd8 100644 --- a/pyo3-ffi/src/pymem.rs +++ b/pyo3-ffi/src/pymem.rs @@ -2,12 +2,33 @@ use core::ffi::c_void; use libc::size_t; extern_libpython! { - #[cfg_attr(PyPy, link_name = "PyPyMem_Malloc")] - pub fn PyMem_Malloc(size: size_t) -> *mut c_void; - #[cfg_attr(PyPy, link_name = "PyPyMem_Calloc")] - pub fn PyMem_Calloc(nelem: size_t, elsize: size_t) -> *mut c_void; - #[cfg_attr(PyPy, link_name = "PyPyMem_Realloc")] - pub fn PyMem_Realloc(ptr: *mut c_void, new_size: size_t) -> *mut c_void; - #[cfg_attr(PyPy, link_name = "PyPyMem_Free")] - pub fn PyMem_Free(ptr: *mut c_void); + // skipped PyMem_Malloc + // skipped PyMem_Calloc + // skipped PyMem_Realloc + // skipped PyMem_Free + // skipped PyMem_New + // skipped PyMem_Resize + // skipped PyMem_MALLOC + // skipped PyMem_NEW + // skipped PyMem_REALLOC + // skipped PyMem_RESIZE + // skipped PyMem_FREE + // skipped PyMem_Del + // skipped PyMem_DEL + + #[cfg_attr(PyPy, link_name = "PyPyMem_RawMalloc")] + #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] + pub fn PyMem_RawMalloc(size: size_t) -> *mut c_void; + + #[cfg_attr(PyPy, link_name = "PyPyMem_RawCalloc")] + #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] + pub fn PyMem_RawCalloc(nelem: size_t, elsize: size_t) -> *mut c_void; + + #[cfg_attr(PyPy, link_name = "PyPyMem_RawRealloc")] + #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] + pub fn PyMem_RawRealloc(ptr: *mut c_void, new_size: size_t) -> *mut c_void; + + #[cfg_attr(PyPy, link_name = "PyPyMem_RawFree")] + #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] + pub fn PyMem_RawFree(ptr: *mut c_void); } diff --git a/src/lib.rs b/src/lib.rs index e9307813f61..d4d53b67890 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -481,7 +481,7 @@ pub mod inspect; // other paths to the same items. (e.g. `pyo3::types::PyAnyMethods` instead of `pyo3::prelude::PyAnyMethods`). pub mod prelude; -#[cfg(not(Py_LIMITED_API))] +#[cfg(any(Py_3_13, not(Py_LIMITED_API)))] pub mod pymem_alloc; /// Test readme and user guide From 4cc1242529dea677fc79f19d9ed7d6d3d2f9b27a Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 12:30:10 +0300 Subject: [PATCH 28/44] fix clippy --- pyo3-ffi/src/lib.rs | 1 + pyo3-ffi/src/pymem.rs | 4 ---- src/pymem_alloc.rs | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 7fdf12a0094..d2b399507ef 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -479,6 +479,7 @@ pub use self::pyerrors::*; pub use self::pyframe::*; pub use self::pyhash::*; pub use self::pylifecycle::*; +#[cfg(any(Py_3_13, not(Py_LIMITED_API)))] pub use self::pymem::*; pub use self::pyport::*; pub use self::pystate::*; diff --git a/pyo3-ffi/src/pymem.rs b/pyo3-ffi/src/pymem.rs index 98c05e78dd8..feeec8bc85e 100644 --- a/pyo3-ffi/src/pymem.rs +++ b/pyo3-ffi/src/pymem.rs @@ -17,18 +17,14 @@ extern_libpython! { // skipped PyMem_DEL #[cfg_attr(PyPy, link_name = "PyPyMem_RawMalloc")] - #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] pub fn PyMem_RawMalloc(size: size_t) -> *mut c_void; #[cfg_attr(PyPy, link_name = "PyPyMem_RawCalloc")] - #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] pub fn PyMem_RawCalloc(nelem: size_t, elsize: size_t) -> *mut c_void; #[cfg_attr(PyPy, link_name = "PyPyMem_RawRealloc")] - #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] pub fn PyMem_RawRealloc(ptr: *mut c_void, new_size: size_t) -> *mut c_void; #[cfg_attr(PyPy, link_name = "PyPyMem_RawFree")] - #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] pub fn PyMem_RawFree(ptr: *mut c_void); } diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 52e234756e2..92ed53ce178 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -77,6 +77,7 @@ unsafe fn finish_aligned(raw: *mut u8, layout: Layout) -> *mut u8 { let addr = raw as usize + size_of::
(); let mask = layout.align() - 1; let aligned_addr = (addr + mask) & !mask; + // SAFETY: `aligned_addr` is within `raw`'s allocated range. let block = unsafe { raw.add(aligned_addr - raw as usize) }; // SAFETY: `block` is within the allocation and has at least `size_of::
()` // bytes of padding before it, so writing a `Header` there is in-bounds. @@ -95,6 +96,7 @@ unsafe fn recover_raw(ptr: *mut u8) -> *mut u8 { unsafe { ptr::read((ptr as *mut Header).sub(1)).0 } } +// SAFETY: forwards to `PyMem_Raw*`, satisfying the `GlobalAlloc` contract. unsafe impl GlobalAlloc for PyMemRawAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { if layout.align() <= MIN_ALIGN { @@ -182,6 +184,7 @@ mod tests { // SAFETY: `ptr` must be valid for reads of `layout.size()` bytes. unsafe fn assert_zeroes(ptr: *mut u8, layout: Layout) { + // SAFETY: caller guarantees `ptr` is valid for `layout.size()` bytes. unsafe { for ofs in 0..layout.size() { assert_eq!(0, ptr.add(ofs).read()); From 4c5106e080b3fa32d19a7b0559c8b38b3af53f8d Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 12:41:59 +0300 Subject: [PATCH 29/44] fix `cargo doc` --- pyo3-ffi/src/lib.rs | 1 - pyo3-ffi/src/pymem.rs | 44 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index d2b399507ef..7fdf12a0094 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -479,7 +479,6 @@ pub use self::pyerrors::*; pub use self::pyframe::*; pub use self::pyhash::*; pub use self::pylifecycle::*; -#[cfg(any(Py_3_13, not(Py_LIMITED_API)))] pub use self::pymem::*; pub use self::pyport::*; pub use self::pystate::*; diff --git a/pyo3-ffi/src/pymem.rs b/pyo3-ffi/src/pymem.rs index feeec8bc85e..235c5c5d735 100644 --- a/pyo3-ffi/src/pymem.rs +++ b/pyo3-ffi/src/pymem.rs @@ -2,12 +2,40 @@ use core::ffi::c_void; use libc::size_t; extern_libpython! { - // skipped PyMem_Malloc - // skipped PyMem_Calloc - // skipped PyMem_Realloc - // skipped PyMem_Free - // skipped PyMem_New - // skipped PyMem_Resize + #[cfg_attr(PyPy, link_name = "PyPyMem_Malloc")] + pub fn PyMem_Malloc(size: size_t) -> *mut c_void; + + #[cfg_attr(PyPy, link_name = "PyPyMem_Calloc")] + pub fn PyMem_Calloc(nelem: size_t, elsize: size_t) -> *mut c_void; + + #[cfg_attr(PyPy, link_name = "PyPyMem_Realloc")] + pub fn PyMem_Realloc(ptr: *mut c_void, new_size: size_t) -> *mut c_void; + + #[cfg_attr(PyPy, link_name = "PyPyMem_Free")] + pub fn PyMem_Free(ptr: *mut c_void); +} + +#[inline] +pub unsafe fn PyMem_New(n: size_t) -> *mut T { + if n > isize::MAX as size_t / size_of::() as size_t { + core::ptr::null_mut() + } else { + PyMem_Malloc(n * size_of::() as size_t).cast() + } +} + +#[inline] +pub unsafe fn PyMem_Resize(p: &mut *mut T, n: size_t) { + *p = if n > isize::MAX as size_t / size_of::() as size_t { + core::ptr::null_mut() + } else { + PyMem_Realloc(p.cast(), n * size_of::() as size_t).cast() + }; +} + +extern_libpython! { + // Deprecated aliases: + // // skipped PyMem_MALLOC // skipped PyMem_NEW // skipped PyMem_REALLOC @@ -17,14 +45,18 @@ extern_libpython! { // skipped PyMem_DEL #[cfg_attr(PyPy, link_name = "PyPyMem_RawMalloc")] + #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] pub fn PyMem_RawMalloc(size: size_t) -> *mut c_void; #[cfg_attr(PyPy, link_name = "PyPyMem_RawCalloc")] + #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] pub fn PyMem_RawCalloc(nelem: size_t, elsize: size_t) -> *mut c_void; #[cfg_attr(PyPy, link_name = "PyPyMem_RawRealloc")] + #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] pub fn PyMem_RawRealloc(ptr: *mut c_void, new_size: size_t) -> *mut c_void; #[cfg_attr(PyPy, link_name = "PyPyMem_RawFree")] + #[cfg(any(Py_3_13, not(Py_LIMITED_API)))] pub fn PyMem_RawFree(ptr: *mut c_void); } From 89b135cb137cd9fabc68bd9d34bbcddf344ed6cb Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 12:56:15 +0300 Subject: [PATCH 30/44] fix --- pyo3-ffi-check/macro/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index e6a9a85c2b9..242e739fd5d 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -455,6 +455,8 @@ const MACRO_EXCLUSIONS: &[(&str, &str)] = &[ ("Py_IsFalse", "not(Py_3_10)"), ("Py_IsTrue", "not(Py_3_10)"), ("Py_IsNone", "not(Py_3_10)"), + ("PyMem_New", ""), + ("PyMem_Resize", ""), ]; // TODO: probably need to clean these up From 16e97637134ef1b2d38cc294d95ef587d3628522 Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 12:59:03 +0300 Subject: [PATCH 31/44] fix clippy --- pyo3-ffi/src/cpython/pymem.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyo3-ffi/src/cpython/pymem.rs b/pyo3-ffi/src/cpython/pymem.rs index f12a6b5832a..16b59bf5034 100644 --- a/pyo3-ffi/src/cpython/pymem.rs +++ b/pyo3-ffi/src/cpython/pymem.rs @@ -1,4 +1,6 @@ +#[cfg(not(any(PyPy, GraalPy)))] use core::ffi::c_void; +#[cfg(not(any(PyPy, GraalPy)))] use libc::size_t; #[repr(C)] From cffcdf7689486a69c12872610d5fcff526b699c4 Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 13:05:45 +0300 Subject: [PATCH 32/44] fix test --- tests/test_pymem_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pymem_alloc.rs b/tests/test_pymem_alloc.rs index a38ba3377f4..dd9fddcc8dc 100644 --- a/tests/test_pymem_alloc.rs +++ b/tests/test_pymem_alloc.rs @@ -10,7 +10,7 @@ fn allocations_work_through_global_allocator() { assert_eq!(vec, vec![1, 2, 3, 4]); let s = String::from("hello"); - assert_eq!(s.len(), 4); + assert_eq!(s.len(), 5); #[repr(align(64))] struct Aligned([u8; 128]); From d9f21b811fc0194ad8acbc748b08a792dda8ef3c Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 13:06:08 +0300 Subject: [PATCH 33/44] fmt --- tests/test_pymem_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pymem_alloc.rs b/tests/test_pymem_alloc.rs index dd9fddcc8dc..ccd3f774c37 100644 --- a/tests/test_pymem_alloc.rs +++ b/tests/test_pymem_alloc.rs @@ -14,7 +14,7 @@ fn allocations_work_through_global_allocator() { #[repr(align(64))] struct Aligned([u8; 128]); - let boxed = Box::new(Aligned([7u8; 128])); + let boxed = Box::new(Aligned([7_u8; 128])); assert_eq!(boxed.0[0], 7); assert_eq!((&*boxed as *const Aligned).addr() % 64, 0); } From f8dd183bc65082d79e79f18fdd23d42638c32eba Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 13:09:15 +0300 Subject: [PATCH 34/44] fix clippy --- tests/test_pymem_alloc.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_pymem_alloc.rs b/tests/test_pymem_alloc.rs index ccd3f774c37..747ef5bf0a9 100644 --- a/tests/test_pymem_alloc.rs +++ b/tests/test_pymem_alloc.rs @@ -1,3 +1,5 @@ +#![cfg(any(Py_3_13, not(Py_LIMITED_API)))] + use pyo3::pymem_alloc::PyMemRawAllocator; #[global_allocator] From ea4c350618efaaf174bee0001bf070b56fa41db5 Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 14:19:25 +0300 Subject: [PATCH 35/44] fix MSRV --- src/pymem_alloc.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 92ed53ce178..af2c7a8505e 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -181,6 +181,14 @@ mod tests { use super::*; use alloc::vec::Vec; + use core::mem; + + // TODO: use `.addr()` directly on MSRV 1.84 (`ptr_addr(ptr)` -> `ptr.addr()`) + #[inline(always)] + fn ptr_addr(ptr: *const T) -> usize { + // SAFETY: Pointer-to-integer transmutes are valid. + unsafe { mem::transmute(ptr.cast::<()>()) } + } // SAFETY: `ptr` must be valid for reads of `layout.size()` bytes. unsafe fn assert_zeroes(ptr: *mut u8, layout: Layout) { @@ -200,7 +208,7 @@ mod tests { unsafe { let ptr = alloc.alloc(layout); assert!(!ptr.is_null()); - assert_eq!(ptr.addr() % layout.align(), 0); + assert_eq!(ptr_addr(ptr) % layout.align(), 0); ptr.write_bytes(0xAB, layout.size()); alloc.dealloc(ptr, layout); } @@ -215,7 +223,7 @@ mod tests { unsafe { let ptr = alloc.alloc(layout); assert!(!ptr.is_null()); - assert_eq!(ptr.addr() % align, 0, "align={align}"); + assert_eq!(ptr_addr(ptr) % align, 0, "align={align}"); ptr.write_bytes(0xCD, layout.size()); alloc.dealloc(ptr, layout); } @@ -243,7 +251,7 @@ mod tests { unsafe { let ptr = alloc.alloc_zeroed(layout); assert!(!ptr.is_null()); - assert_eq!(ptr.addr() % layout.align(), 0); + assert_eq!(ptr_addr(ptr) % layout.align(), 0); assert_zeroes(ptr, layout); alloc.dealloc(ptr, layout); } @@ -303,7 +311,7 @@ mod tests { let new_ptr = alloc.realloc(ptr, old_layout, 512); assert!(!new_ptr.is_null()); - assert_eq!(new_ptr.addr() % align, 0); + assert_eq!(ptr_addr(new_ptr) % align, 0); for i in 0..old_layout.size() { assert_eq!(new_ptr.add(i).read(), 0x55); } From e56c4a12781eafbd147514b45136ccaeebf52ca2 Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 14:28:06 +0300 Subject: [PATCH 36/44] fix MSRV (x2) --- src/pymem_alloc.rs | 3 +-- tests/test_pymem_alloc.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index af2c7a8505e..c9934d2fb4a 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -181,13 +181,12 @@ mod tests { use super::*; use alloc::vec::Vec; - use core::mem; // TODO: use `.addr()` directly on MSRV 1.84 (`ptr_addr(ptr)` -> `ptr.addr()`) #[inline(always)] fn ptr_addr(ptr: *const T) -> usize { // SAFETY: Pointer-to-integer transmutes are valid. - unsafe { mem::transmute(ptr.cast::<()>()) } + unsafe { core::mem::transmute(ptr.cast::<()>()) } } // SAFETY: `ptr` must be valid for reads of `layout.size()` bytes. diff --git a/tests/test_pymem_alloc.rs b/tests/test_pymem_alloc.rs index 747ef5bf0a9..dcff73ec632 100644 --- a/tests/test_pymem_alloc.rs +++ b/tests/test_pymem_alloc.rs @@ -18,5 +18,14 @@ fn allocations_work_through_global_allocator() { struct Aligned([u8; 128]); let boxed = Box::new(Aligned([7_u8; 128])); assert_eq!(boxed.0[0], 7); + + // TODO: use `.addr()` directly on MSRV 1.84 (`ptr_addr(ptr)` -> `ptr.addr()`) + #[inline(always)] + fn ptr_addr(ptr: *const T) -> usize { + // SAFETY: Pointer-to-integer transmutes are valid. + unsafe { core::mem::transmute(ptr.cast::<()>()) } + } + + assert_eq!(ptr_addr(&*boxed as *const Aligned) % 64, 0); assert_eq!((&*boxed as *const Aligned).addr() % 64, 0); } From db5e086bca24fa3e61bf26e5ae9339fd63d26250 Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 14:29:12 +0300 Subject: [PATCH 37/44] fix MSRV (x3) --- tests/test_pymem_alloc.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_pymem_alloc.rs b/tests/test_pymem_alloc.rs index dcff73ec632..37af117509e 100644 --- a/tests/test_pymem_alloc.rs +++ b/tests/test_pymem_alloc.rs @@ -27,5 +27,4 @@ fn allocations_work_through_global_allocator() { } assert_eq!(ptr_addr(&*boxed as *const Aligned) % 64, 0); - assert_eq!((&*boxed as *const Aligned).addr() % 64, 0); } From 5068b35a9c5757db298022c33d0e08839d699d3c Mon Sep 17 00:00:00 2001 From: chiri Date: Sat, 1 Aug 2026 14:54:04 +0300 Subject: [PATCH 38/44] fix clippy --- src/pymem_alloc.rs | 15 ++++----------- tests/test_pymem_alloc.rs | 10 +--------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index c9934d2fb4a..b2f0e444394 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -182,13 +182,6 @@ mod tests { use alloc::vec::Vec; - // TODO: use `.addr()` directly on MSRV 1.84 (`ptr_addr(ptr)` -> `ptr.addr()`) - #[inline(always)] - fn ptr_addr(ptr: *const T) -> usize { - // SAFETY: Pointer-to-integer transmutes are valid. - unsafe { core::mem::transmute(ptr.cast::<()>()) } - } - // SAFETY: `ptr` must be valid for reads of `layout.size()` bytes. unsafe fn assert_zeroes(ptr: *mut u8, layout: Layout) { // SAFETY: caller guarantees `ptr` is valid for `layout.size()` bytes. @@ -207,7 +200,7 @@ mod tests { unsafe { let ptr = alloc.alloc(layout); assert!(!ptr.is_null()); - assert_eq!(ptr_addr(ptr) % layout.align(), 0); + assert_eq!(ptr as usize % layout.align(), 0); ptr.write_bytes(0xAB, layout.size()); alloc.dealloc(ptr, layout); } @@ -222,7 +215,7 @@ mod tests { unsafe { let ptr = alloc.alloc(layout); assert!(!ptr.is_null()); - assert_eq!(ptr_addr(ptr) % align, 0, "align={align}"); + assert_eq!(ptr as usize % align, 0, "align={align}"); ptr.write_bytes(0xCD, layout.size()); alloc.dealloc(ptr, layout); } @@ -250,7 +243,7 @@ mod tests { unsafe { let ptr = alloc.alloc_zeroed(layout); assert!(!ptr.is_null()); - assert_eq!(ptr_addr(ptr) % layout.align(), 0); + assert_eq!(ptr as usize % layout.align(), 0); assert_zeroes(ptr, layout); alloc.dealloc(ptr, layout); } @@ -310,7 +303,7 @@ mod tests { let new_ptr = alloc.realloc(ptr, old_layout, 512); assert!(!new_ptr.is_null()); - assert_eq!(ptr_addr(new_ptr) % align, 0); + assert_eq!(new_ptr as usize % align, 0); for i in 0..old_layout.size() { assert_eq!(new_ptr.add(i).read(), 0x55); } diff --git a/tests/test_pymem_alloc.rs b/tests/test_pymem_alloc.rs index 37af117509e..ae25a485c42 100644 --- a/tests/test_pymem_alloc.rs +++ b/tests/test_pymem_alloc.rs @@ -18,13 +18,5 @@ fn allocations_work_through_global_allocator() { struct Aligned([u8; 128]); let boxed = Box::new(Aligned([7_u8; 128])); assert_eq!(boxed.0[0], 7); - - // TODO: use `.addr()` directly on MSRV 1.84 (`ptr_addr(ptr)` -> `ptr.addr()`) - #[inline(always)] - fn ptr_addr(ptr: *const T) -> usize { - // SAFETY: Pointer-to-integer transmutes are valid. - unsafe { core::mem::transmute(ptr.cast::<()>()) } - } - - assert_eq!(ptr_addr(&*boxed as *const Aligned) % 64, 0); + assert_eq!((&*boxed as *const Aligned) as usize % 64, 0); } From 1f8456146d3a281170403b3e375ea9f707991bd3 Mon Sep 17 00:00:00 2001 From: chiri Date: Wed, 2 Sep 2026 21:51:10 +0300 Subject: [PATCH 39/44] only 3.15+ --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 11115b203da..8de573bf58a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -481,7 +481,7 @@ pub mod inspect; // other paths to the same items. (e.g. `pyo3::types::PyAnyMethods` instead of `pyo3::prelude::PyAnyMethods`). pub mod prelude; -#[cfg(any(Py_3_13, not(Py_LIMITED_API)))] +#[cfg(all(Py_3_15, not(Py_LIMITED_API)))] pub mod pymem_alloc; /// Test readme and user guide From f68716938b0d558bcadaa203956959ba75677bfc Mon Sep 17 00:00:00 2001 From: chiri Date: Wed, 2 Sep 2026 21:52:21 +0300 Subject: [PATCH 40/44] only 3.15+ --- tests/test_pymem_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pymem_alloc.rs b/tests/test_pymem_alloc.rs index ae25a485c42..e7a4f8dc377 100644 --- a/tests/test_pymem_alloc.rs +++ b/tests/test_pymem_alloc.rs @@ -1,4 +1,4 @@ -#![cfg(any(Py_3_13, not(Py_LIMITED_API)))] +#![cfg(all(Py_3_15, not(Py_LIMITED_API)))] use pyo3::pymem_alloc::PyMemRawAllocator; From 1713286841d1b354bd113cbeb621c28f245e410d Mon Sep 17 00:00:00 2001 From: chiri Date: Wed, 2 Sep 2026 22:03:45 +0300 Subject: [PATCH 41/44] warning --- src/pymem_alloc.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index b2f0e444394..e26a040d6dd 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -1,7 +1,20 @@ //! `GlobalAlloc` backed by CPython's `PyMem_Raw*` (`PYMEM_DOMAIN_RAW`). //! +//! > [!WARNING] +//! > This is experimental and provisional. +//! > +//! > Earlier Python 3.15 release candidates had a bug where, if `tracemalloc` +//! > was enabled, its raw-domain allocator hook called `PyGILState_Ensure()` +//! > internally, which could deadlock a native thread calling `PyMem_RawMalloc`/ +//! > `PyMem_RawFree` without an attached thread state while another thread held +//! > the GIL. This was fixed upstream by storing trace data as raw strings +//! > instead of Python objects, removing the need to acquire a thread state, +//! > and the fix was backported into the Python 3.15 final release. +//! > See [python/cpython#155725](https://github.com/python/cpython/issues/155725) +//! > and [PyO3/pyo3#6268](https://github.com/PyO3/pyo3/issues/6268) for details. +//! //! ``` -//! use pyo3::pymem_alloc::PyMemRawAllocator; +//! //! use pyo3::pymem_alloc::PyMemRawAllocator; //! //! #[global_allocator] //! static GLOBAL_ALLOCATOR: PyMemRawAllocator = PyMemRawAllocator; From b140b46872cc34916877b87a30867b941d969e64 Mon Sep 17 00:00:00 2001 From: chiri Date: Sun, 27 Sep 2026 17:55:35 +0300 Subject: [PATCH 42/44] review --- src/pymem_alloc.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index e26a040d6dd..bcb1027683d 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -37,9 +37,15 @@ const MIN_ALIGN: usize = cfg_select! { // Windows: 8 for both `MS_WIN32` / `MS_WIN64`. target_os = "windows" => 8, // macOS: 16 on Intel (`i386` / `x86_64`). - all(target_vendor = "apple", any(target_arch = "x86", target_arch = "x86_64")) => 16, + all( + target_vendor = "apple", + any(target_arch = "x86", target_arch = "x86_64") + ) => 16, // macOS: 8 on other archs (e.g. `arm64`). - all(target_vendor = "apple", not(any(target_arch = "x86", target_arch = "x86_64"))) => 8, + all( + target_vendor = "apple", + not(any(target_arch = "x86", target_arch = "x86_64")) + ) => 8, // Other Unix: autoconf-derived at build time, not checked in, not guaranteed > 8. _ => 8, }; @@ -47,6 +53,7 @@ const MIN_ALIGN: usize = cfg_select! { /// Header stashing the original allocation pointer before an over-aligned block. /// /// Recovered by [`recover_raw`] to pass back to `PyMem_RawFree` / `PyMem_RawRealloc`. +#[derive(Clone, Copy)] #[repr(transparent)] struct Header(*mut u8); @@ -102,11 +109,17 @@ unsafe fn finish_aligned(raw: *mut u8, layout: Layout) -> *mut u8 { /// /// # Safety /// -/// `ptr` must have been returned by [`finish_aligned`], with its `Header` still intact. +/// `ptr` must have been returned by [`finish_aligned`] for `align`. #[inline] -unsafe fn recover_raw(ptr: *mut u8) -> *mut u8 { - // SAFETY: `ptr` has a `Header` written directly before it by `finish_aligned`. - unsafe { ptr::read((ptr as *mut Header).sub(1)).0 } +unsafe fn recover_raw(ptr: *mut u8, align: usize) -> *mut u8 { + // SAFETY: `ptr` is inside the allocation written by `finish_aligned`, which placed + // the `Header` immediately before it. + let raw = unsafe { ptr.cast::
().sub(1).read() }.0; + let offset = (ptr as usize).wrapping_sub(raw as usize); + if offset.wrapping_sub(size_of::
()) >= align { + libc::abort(); + } + raw } // SAFETY: forwards to `PyMem_Raw*`, satisfying the `GlobalAlloc` contract. @@ -135,8 +148,8 @@ unsafe impl GlobalAlloc for PyMemRawAllocator { // See: https://docs.python.org/3/c-api/memory.html#c.PyMem_RawFree unsafe { pyo3_ffi::PyMem_RawFree(ptr as *mut _) } } else { - // SAFETY: `ptr` was returned by `finish_aligned`, so it has a `Header` before it. - let raw = unsafe { recover_raw(ptr) }; + // SAFETY: `ptr` was returned by `finish_aligned` for `layout.align()`. + let raw = unsafe { recover_raw(ptr, layout.align()) }; // SAFETY: `raw` is the original pointer from `PyMem_RawMalloc`/`PyMem_RawCalloc`, // which is what `PyMem_RawFree` requires. // From 275140e5255d9ac3265fe1bf7c28ea383c632c79 Mon Sep 17 00:00:00 2001 From: chiri Date: Sun, 27 Sep 2026 18:01:40 +0300 Subject: [PATCH 43/44] add unsafe --- src/pymem_alloc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index bcb1027683d..79877178518 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -117,7 +117,7 @@ unsafe fn recover_raw(ptr: *mut u8, align: usize) -> *mut u8 { let raw = unsafe { ptr.cast::
().sub(1).read() }.0; let offset = (ptr as usize).wrapping_sub(raw as usize); if offset.wrapping_sub(size_of::
()) >= align { - libc::abort(); + unsafe { libc::abort() }; } raw } From b27b718e611f49824b53a54e97410fc304aa0101 Mon Sep 17 00:00:00 2001 From: chiri Date: Sun, 27 Sep 2026 18:35:06 +0300 Subject: [PATCH 44/44] add missing safety comment --- src/pymem_alloc.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pymem_alloc.rs b/src/pymem_alloc.rs index 79877178518..668e9984ee3 100644 --- a/src/pymem_alloc.rs +++ b/src/pymem_alloc.rs @@ -117,6 +117,7 @@ unsafe fn recover_raw(ptr: *mut u8, align: usize) -> *mut u8 { let raw = unsafe { ptr.cast::
().sub(1).read() }.0; let offset = (ptr as usize).wrapping_sub(raw as usize); if offset.wrapping_sub(size_of::
()) >= align { + // SAFETY: `abort` has no safety preconditions. unsafe { libc::abort() }; } raw