MEM_guardedalloc: Refactor to add more type-safety.

The main goal of these changes are to improve static (i.e. build-time)
checks on whether a given data can be allocated and freed with `malloc`
and `free` (C-style), or requires proper C++-style construction and
destruction (`new` and `delete`).

* Add new `MEM_malloc_arrayN_aligned` API.
* Make `MEM_freeN` a template function in C++, which does static assert on
  type triviality.
* Add `MEM_SAFE_DELETE`, similar to `MEM_SAFE_FREE` but calling
  `MEM_delete`.

The changes to `MEM_freeN` was painful and useful, as it allowed to fix a bunch
of invalid calls in existing codebase already.

It also highlighted a fair amount of places where it is called to free incomplete
type pointers, which is likely a sign of badly designed code (there should
rather be an API to destroy and free these data then, if the data type is not fully
publicly exposed). For now, these are 'worked around' by explicitly casting the
freed pointers to `void *` in these cases - which also makes them easy to search for.
Some of these will be addressed separately (see blender/blender!134765).

Finally, MSVC seems to consider structs defining new/delete operators (e.g. by
using the `MEM_CXX_CLASS_ALLOC_FUNCS` macro) as non-trivial. This does not
seem to follow the definition of type triviality, so for now static type checking in
`MEM_freeN` has been disabled for Windows. We'll likely have to do the same
with type-safe `MEM_[cm]allocN` API being worked on in blender/blender!134771

Based on ideas from Brecht in blender/blender!134452

Pull Request: https://projects.blender.org/blender/blender/pulls/134463
This commit is contained in:
Bastien Montagne
2025-02-20 10:37:10 +01:00
committed by Bastien Montagne
parent 6be8dd16e7
commit 48e26c3afe
32 changed files with 203 additions and 62 deletions

View File

@@ -136,6 +136,16 @@ void *MEM_mallocN_aligned(size_t len,
const char *str) /* ATTR_MALLOC */ ATTR_WARN_UNUSED_RESULT
ATTR_ALLOC_SIZE(1) ATTR_NONNULL(3);
/**
* Allocate an aligned block of memory that remains uninitialized.
*/
extern void *(*MEM_malloc_arrayN_aligned)(
size_t len,
size_t size,
size_t alignment,
const char *str) /* ATTR_MALLOC */ ATTR_WARN_UNUSED_RESULT ATTR_ALLOC_SIZE(1, 2)
ATTR_NONNULL(4);
/**
* Allocate an aligned block of memory that is initialized with zeros.
*/
@@ -190,11 +200,9 @@ extern size_t (*MEM_get_peak_memory)(void) ATTR_WARN_UNUSED_RESULT;
#ifdef __cplusplus
# define MEM_SAFE_FREE(v) \
do { \
static_assert(std::is_pointer_v<std::decay_t<decltype(v)>>); \
void **_v = (void **)&(v); \
if (*_v) { \
MEM_freeN(*_v); \
*_v = NULL; \
if (v) { \
MEM_freeN<std::remove_pointer_t<std::decay_t<decltype(v)>>>(v); \
(v) = nullptr; \
} \
} while (0)
#else
@@ -334,6 +342,18 @@ template<typename T> inline void MEM_delete(const T *ptr)
mem_guarded::internal::AllocationType::NEW_DELETE);
}
/**
* Helper shortcut to #MEM_delete, that also ensures that the target pointer is set to nullptr
* after deleting it.
*/
# define MEM_SAFE_DELETE(v) \
do { \
if (v) { \
MEM_delete(v); \
(v) = nullptr; \
} \
} while (0)
/**
* Allocate zero-initialized memory for an object of type #T. The constructor of #T is not called,
* therefore this should only be used with trivial types (like all C types).
@@ -377,6 +397,25 @@ template<typename T> inline T *MEM_cnew(const char *allocation_name, const T &ot
return new_object;
}
template<typename T> inline void MEM_freeN(T *ptr)
{
if constexpr (std::is_void_v<T>) {
mem_guarded::internal::mem_freeN_ex(const_cast<void *>(ptr),
mem_guarded::internal::AllocationType::ALLOC_FREE);
}
else {
# ifndef _WIN32
/* MSVC seems to consider C-style types using the MEM_CXX_CLASS_ALLOC_FUNCS as non-trivial. GCC
* and clang (both on linux and OSX) do not.
*
* So for now, disable the triviality check on Windows. */
static_assert(std::is_trivial_v<T>, "For non-trivial types, MEM_delete must be used.");
# endif
mem_guarded::internal::mem_freeN_ex(const_cast<void *>(static_cast<const void *>(ptr)),
mem_guarded::internal::AllocationType::ALLOC_FREE);
}
}
/** Allocation functions (for C++ only). */
# define MEM_CXX_CLASS_ALLOC_FUNCS(_id) \
public: \

View File

@@ -48,6 +48,10 @@ void *(*mem_guarded::internal::mem_mallocN_aligned_ex)(size_t len,
const char *str,
AllocationType allocation_type) =
MEM_lockfree_mallocN_aligned;
void *(*MEM_malloc_arrayN_aligned)(size_t len,
size_t size,
size_t alignment,
const char *str) = MEM_lockfree_malloc_arrayN_aligned;
void *(*MEM_calloc_arrayN_aligned)(size_t len,
size_t size,
size_t alignment,
@@ -146,6 +150,7 @@ void MEM_use_lockfree_allocator()
MEM_mallocN = MEM_lockfree_mallocN;
MEM_malloc_arrayN = MEM_lockfree_malloc_arrayN;
mem_mallocN_aligned_ex = MEM_lockfree_mallocN_aligned;
MEM_malloc_arrayN_aligned = MEM_lockfree_malloc_arrayN_aligned;
MEM_calloc_arrayN_aligned = MEM_lockfree_calloc_arrayN_aligned;
MEM_printmemlist_pydict = MEM_lockfree_printmemlist_pydict;
MEM_printmemlist = MEM_lockfree_printmemlist;
@@ -181,6 +186,7 @@ void MEM_use_guarded_allocator()
MEM_mallocN = MEM_guarded_mallocN;
MEM_malloc_arrayN = MEM_guarded_malloc_arrayN;
mem_mallocN_aligned_ex = MEM_guarded_mallocN_aligned;
MEM_malloc_arrayN_aligned = MEM_guarded_malloc_arrayN_aligned;
MEM_calloc_arrayN_aligned = MEM_guarded_calloc_arrayN_aligned;
MEM_printmemlist_pydict = MEM_guarded_printmemlist_pydict;
MEM_printmemlist = MEM_guarded_printmemlist;

View File

@@ -702,13 +702,13 @@ void *MEM_guarded_calloc_arrayN(size_t len, size_t size, const char *str)
return MEM_guarded_callocN(total_size, str);
}
void *MEM_guarded_calloc_arrayN_aligned(const size_t len,
const size_t size,
const size_t alignment,
const char *str)
static void *mem_guarded_malloc_arrayN_aligned(const size_t len,
const size_t size,
const size_t alignment,
const char *str,
size_t &r_bytes_num)
{
size_t bytes_num;
if (UNLIKELY(!MEM_size_safe_multiply(len, size, &bytes_num))) {
if (UNLIKELY(!MEM_size_safe_multiply(len, size, &r_bytes_num))) {
print_error(
"Calloc array aborted due to integer overflow: "
"len=" SIZET_FORMAT "x" SIZET_FORMAT " in %s, total " SIZET_FORMAT "\n",
@@ -720,11 +720,29 @@ void *MEM_guarded_calloc_arrayN_aligned(const size_t len,
return nullptr;
}
if (alignment <= MEM_MIN_CPP_ALIGNMENT) {
return MEM_callocN(bytes_num, str);
return MEM_callocN(r_bytes_num, str);
}
return MEM_mallocN_aligned(r_bytes_num, alignment, str);
}
void *MEM_guarded_malloc_arrayN_aligned(const size_t len,
const size_t size,
const size_t alignment,
const char *str)
{
size_t bytes_num;
return mem_guarded_malloc_arrayN_aligned(len, size, alignment, str, bytes_num);
}
void *MEM_guarded_calloc_arrayN_aligned(const size_t len,
const size_t size,
const size_t alignment,
const char *str)
{
size_t bytes_num;
/* There is no lower level #calloc with an alignment parameter, so we have to fallback to using
* #memset unfortunately. */
void *ptr = MEM_mallocN_aligned(bytes_num, alignment, str);
void *ptr = mem_guarded_malloc_arrayN_aligned(len, size, alignment, str, bytes_num);
if (!ptr) {
return nullptr;
}

View File

@@ -135,6 +135,11 @@ void *MEM_lockfree_mallocN_aligned(size_t len,
const char *str,
mem_guarded::internal::AllocationType allocation_type)
ATTR_MALLOC ATTR_WARN_UNUSED_RESULT ATTR_ALLOC_SIZE(1) ATTR_NONNULL(3);
void *MEM_lockfree_malloc_arrayN_aligned(size_t len,
size_t size,
size_t alignment,
const char *str) ATTR_MALLOC ATTR_WARN_UNUSED_RESULT
ATTR_ALLOC_SIZE(1, 2) ATTR_NONNULL(4);
void *MEM_lockfree_calloc_arrayN_aligned(size_t len,
size_t size,
size_t alignment,
@@ -188,6 +193,8 @@ void *MEM_guarded_mallocN_aligned(size_t len,
const char *str,
mem_guarded::internal::AllocationType allocation_type)
ATTR_MALLOC ATTR_WARN_UNUSED_RESULT ATTR_ALLOC_SIZE(1) ATTR_NONNULL(3);
void *MEM_guarded_malloc_arrayN_aligned(size_t len, size_t size, size_t alignment, const char *str)
ATTR_MALLOC ATTR_WARN_UNUSED_RESULT ATTR_ALLOC_SIZE(1, 2) ATTR_NONNULL(4);
void *MEM_guarded_calloc_arrayN_aligned(size_t len, size_t size, size_t alignment, const char *str)
ATTR_MALLOC ATTR_WARN_UNUSED_RESULT ATTR_ALLOC_SIZE(1, 2) ATTR_NONNULL(4);
void MEM_guarded_printmemlist_pydict(void);

View File

@@ -456,13 +456,13 @@ void *MEM_lockfree_mallocN_aligned(size_t len,
return nullptr;
}
void *MEM_lockfree_calloc_arrayN_aligned(const size_t len,
const size_t size,
const size_t alignment,
const char *str)
static void *mem_lockfree_malloc_arrayN_aligned(const size_t len,
const size_t size,
const size_t alignment,
const char *str,
size_t &r_bytes_num)
{
size_t bytes_num;
if (UNLIKELY(!MEM_size_safe_multiply(len, size, &bytes_num))) {
if (UNLIKELY(!MEM_size_safe_multiply(len, size, &r_bytes_num))) {
print_error(
"Calloc array aborted due to integer overflow: "
"len=" SIZET_FORMAT "x" SIZET_FORMAT " in %s, total " SIZET_FORMAT "\n",
@@ -474,11 +474,30 @@ void *MEM_lockfree_calloc_arrayN_aligned(const size_t len,
return nullptr;
}
if (alignment <= MEM_MIN_CPP_ALIGNMENT) {
return MEM_callocN(bytes_num, str);
return MEM_mallocN(r_bytes_num, str);
}
void *ptr = MEM_mallocN_aligned(r_bytes_num, alignment, str);
return ptr;
}
void *MEM_lockfree_malloc_arrayN_aligned(const size_t len,
const size_t size,
const size_t alignment,
const char *str)
{
size_t bytes_num;
return mem_lockfree_malloc_arrayN_aligned(len, size, alignment, str, bytes_num);
}
void *MEM_lockfree_calloc_arrayN_aligned(const size_t len,
const size_t size,
const size_t alignment,
const char *str)
{
size_t bytes_num;
/* There is no lower level #calloc with an alignment parameter, so we have to fallback to using
* #memset unfortunately. */
void *ptr = MEM_mallocN_aligned(bytes_num, alignment, str);
void *ptr = mem_lockfree_malloc_arrayN_aligned(len, size, alignment, str, bytes_num);
if (!ptr) {
return nullptr;
}