Files
wmaker/wutil-rs/src/memory.rs
T
trurl 8c68f95291 Update calls to malloc/free, etc., to use wmalloc/wfree/...
This fixes a lot of memory bugs which arose as a result of doing something
different from the system malloc/free when allocators were rewritten in Rust.

These changes originate from a different approach to writing the allocator in
Rust:
https://git.sdf.org/vitrine/wmaker/pulls/1/files#diff-04a0fd2319b9969373b75377716e45c836d22869

There are other function calls (to XFree) that need to be fixed, but that can be
done in another commit. This one is already getting large.
2025-10-24 15:35:07 -04:00

284 lines
8.8 KiB
Rust

//! Custom implementations of malloc/free/realloc.
//!
//! These are intended for use by C functions that need to allocate. Window
//! Maker originally provided [`wmalloc`], [`wfree`], and [`wrealloc`] for
//! customizable handling of memory exhaustion (to save workspace state before
//! aborting) and to allow optional use of the Boehm GC library. It also tracked
//! reference counts, via [`wretain`] and [`wrelease`].
//!
//! If everything gets rewritten in Rust, we won't need this module anymore. For
//! now, it helps to move our allocations into Rust so that it is more
//! straightforward to store Rust objects in heap memory that was allocated from
//! C. (Rust may have stricter requirements for heap-allocated segments than are
//! provided by arbitrary C allocators).
//!
//! TODO: We may want to restore handling of OOM errors. This would require
//! installing a customized Rust allocator, which isn't something you can do yet
//! in stable Rust. And, unless our rewrite ends up taking up obscenely more
//! memory than the baseline Window Maker code, it isn't really necessary in
//! this day and age.
use std::{alloc, ffi::{c_char, CStr}, mem, ptr::{self, NonNull}};
/// Tracks the layout and reference count of an allocated chunk of memory.
#[derive(Clone, Copy)]
struct Header {
ptr: NonNull<u8>,
payload_size: usize,
layout: alloc::Layout,
refcount: u16,
}
impl Header {
/// Recovers the `Header` for the allocated memory chunk `b`.
///
/// ## Safety
///
/// Callers must ensure that `b` is a live allocation from [`wmalloc`] or [`wrealloc`].
unsafe fn for_alloc_bytes(b: *mut u8) -> *mut Header {
unsafe {
b.sub(mem::size_of::<Header>())
.cast::<Header>()
}
}
}
/// Allocates at least `size` bytes and returns a pointer to them.
///
/// Returns null if `size` is 0.
pub fn alloc_bytes(size: usize) -> *mut u8 {
if size == 0 {
return ptr::null_mut();
}
let Ok(header_layout) = alloc::Layout::from_size_align(mem::size_of::<Header>(), 8) else {
return ptr::null_mut();
};
let Ok(layout) = alloc::Layout::from_size_align(size, 8) else {
return ptr::null_mut();
};
let Ok((full_layout, result_offset)) = header_layout.extend(layout) else {
return ptr::null_mut();
};
let full_segment = unsafe { alloc::alloc_zeroed(full_layout) };
if full_segment.is_null() {
return ptr::null_mut();
}
let result = unsafe { full_segment.add(result_offset) };
if result.is_null() {
return ptr::null_mut();
}
unsafe {
let header = result.sub(mem::size_of::<Header>()).cast::<Header>();
header.write_unaligned(Header {
ptr: NonNull::new_unchecked(full_segment),
payload_size: size,
layout: full_layout,
refcount: 0,
});
}
result
}
/// Allocates a segment with [`alloc_bytes`] and fills it with the contents of
/// `s`. The resulting string should be free'd by passing it to [`free_bytes`].
pub fn alloc_string(s: &CStr) -> *mut c_char {
let len = s.count_bytes() + 1;
let result = alloc_bytes(len).cast::<c_char>();
unsafe { ptr::copy_nonoverlapping(s.as_ptr().cast::<c_char>(), result, len); }
result
}
/// Frees the bytes pointed to by `b`.
///
/// ## Safety
///
/// Callers must ensure that `b` is a live allocation from [`wmalloc`] or [`wrealloc`].
pub unsafe fn free_bytes(b: *mut u8) {
if b.is_null() {
return;
}
unsafe {
let header = &*Header::for_alloc_bytes(b);
alloc::dealloc(header.ptr.as_ptr(), header.layout);
}
}
/// Functions to be called from C.
pub mod ffi {
use super::{alloc_bytes, free_bytes, Header};
use std::{ffi::c_void, ptr};
/// Allocates `size` bytes. Returns null if `size` is 0. Data will be
/// initialized to 0.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wmalloc(size: usize) -> *mut c_void {
alloc_bytes(size).cast::<c_void>()
}
/// Frees `ptr`, which must have come from [`wmalloc`] or [`wrealloc`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wfree(ptr: *mut c_void) {
unsafe { free_bytes(ptr.cast::<u8>()); }
}
/// Resizes `ptr` to be at least `newsize` bytes in size, returning the
/// start of the new segment. If `newsize` is larger than `ptr`'s segment,
/// data in the new space will be initialized but have an arbitrary value.
///
/// ## Safety
///
/// If `ptr` is non-null, callers must ensure that it came from from
/// [`wmalloc`] or [`wrealloc`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wrealloc(ptr: *mut c_void, newsize: usize) -> *mut c_void {
if ptr.is_null() {
unsafe {
return wmalloc(newsize);
}
}
unsafe {
let result = wmalloc(newsize);
let result_header = ptr::read_unaligned(Header::for_alloc_bytes(result.cast()));
let ptr_header = ptr::read_unaligned(Header::for_alloc_bytes(ptr.cast()));
let copy_size = usize::min(
ptr_header.payload_size,
result_header.payload_size,
);
ptr::copy_nonoverlapping(ptr, result, copy_size);
wfree(ptr);
result
}
}
/// Bumps the refcount for `ptr`.
///
/// ## Safety
///
/// Callers must ensure that `b` is a live allocation from [`wmalloc`] or [`wrealloc`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wretain(ptr: *mut c_void) -> *mut c_void {
if ptr.is_null() {
return ptr::null_mut();
}
unsafe {
let header = Header::for_alloc_bytes(ptr.cast::<u8>());
(*header).refcount += 1;
}
ptr
}
/// Decrements the refcount for `ptr`. If this brings the refcount to 0,
/// frees `ptr`.
///
/// ## Safety
///
/// Callers must ensure that `ptr` is a live allocation from [`wmalloc`] or [`wrealloc`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn wrelease(ptr: *mut c_void) {
let ptr = ptr.cast::<u8>();
if ptr.is_null() {
return;
}
let header = unsafe { &mut *Header::for_alloc_bytes(ptr) };
match header.refcount {
0 | 1 => unsafe { free_bytes(ptr) },
_ => header.refcount -= 1,
}
}
}
#[cfg(test)]
mod test {
use super::{alloc_bytes, alloc_string, ffi::{wfree, wmalloc, wrealloc}, free_bytes, Header};
use std::{ffi::CStr, mem, os::raw::c_void, ptr, slice};
#[test]
fn recover_header() {
unsafe {
let x = alloc_bytes(mem::size_of::<i64>());
let header = Header::for_alloc_bytes(x);
assert_eq!(header.cast::<u8>().add(mem::size_of::<Header>()), x);
// This may be allocator-dependent, but it's a reasonable sanity check for now.
assert!((*header).ptr.as_ptr() <= header.cast::<u8>());
}
}
#[test]
fn alloc_zero_returns_null() {
assert!(alloc_bytes(0).is_null());
}
#[test]
fn free_null() {
unsafe { free_bytes(ptr::null_mut()); }
}
#[test]
fn realloc_null() {
unsafe { assert!(wrealloc(ptr::null_mut(), 0).is_null()); }
}
#[test]
fn alloc_free_nonzero() {
let x = alloc_bytes(mem::size_of::<i64>()).cast::<i64>();
assert!(!x.is_null());
unsafe { *x = 42; }
assert_eq!(unsafe { *x }, 42);
unsafe { free_bytes(x.cast::<u8>()); }
}
#[test]
fn multiple_allocs() {
unsafe {
let x = wmalloc(mem::size_of::<i64>());
*x.cast::<i64>() = 30;
let y = wmalloc(mem::size_of::<i32>());
*y.cast::<i32>() = 5;
let z = wmalloc(48);
*z.cast::<f32>() = 1.0;
wfree(x);
wfree(y);
wfree(z);
}
}
#[test]
fn realloc_nonzero() {
let x = alloc_bytes(mem::size_of::<i64>()).cast::<c_void>();
assert!(!x.is_null());
let y = unsafe { wrealloc(x, mem::size_of::<i32>()).cast::<i32>() };
assert!(!y.is_null());
unsafe { *y = 17; }
assert_eq!(unsafe { *y }, 17);
unsafe { free_bytes(y.cast::<u8>()); }
}
#[test]
fn realloc_retains_data() {
let x: *mut u8 = unsafe { wmalloc(10).cast() };
unsafe {
let xs = slice::from_raw_parts_mut(&mut *x, 10);
// We know that xs should be zeroed.
assert_eq!(xs, &[0u8; 10]);
for i in 0u8..10 {
xs[i as usize] = i;
}
assert_eq!(xs, (0..10).collect::<Vec::<u8>>());
}
}
#[test]
fn alloc_free_string() {
let s = alloc_string(c"hello");
assert!(!s.is_null());
assert_eq!(unsafe { CStr::from_ptr(s) }, c"hello");
unsafe { free_bytes(s.cast::<u8>()); }
}
}