7
\$\begingroup\$

This is a follow-up to: An Arena/Bump Allocator in C.

LOC: 234. (Not including the sample tests, still quite small and manageable.)

The allocator uses C99, but sample tests use C11's alignof and alignas.

What has changed:

  • The internal memory pool no longer needs to be allocated on the heap by the allocator. It can be one of the following:

    1. A statically-allocated pool specified by the client.
    2. A dynamically-allocated pool specified by the client.
    3. An automatically-allocated pool specified by the client.

    A null pointer is also entertained, in which case a memory pool of size DEFAULT_BUF_CAP KiB is allocated dynamically by the allocator. arena_new() now also takes an initial size.

  • Pointers returned by the allocator are now stable. The allocator starts with a single memory pool, and adds more when needed. I have opted for an array of pointers instead of a linked list for this.

  • The allocator now returns aligned memory. The alignment is passed as an argument to the allocation functions.

  • Documentation and missing overflow checks have been added.

  • If compiled with -DDEBUG, the padding for alignment (if any) and the subsequent bytes after a user block are set to 0xA5. (The value was taken from Wikipedia, which stated that FreeBSD's PHK malloc uses it for debugging purposes.)

API Additions:

  • arena_realloc() - Extends the last allocation made in place. Can be used to shrink, grow, or delete the last allocation.

  • arena_reset() - Simply sets the count member of all the memory pools to 0.

  • arena_resize() - Adds a new memory pool to the exisiting arena passed in as an argument.

  • arena_allocarray() - Equivalent to the call:

    arena_alloc(arena, alignment, nmemb * size);
    

    But unlike arena_alloc(), fails safely where the multiplication overflows.

Code:

arena.h:

#ifndef ARENA_H
#define ARENA_H 1

/* *INDENT-OFF* */
#if defined(__GNUC__) || defined(__clang__) || defined(__INTEL_LLVM_COMPILER)
    #define ATTRIB_CONST            __attribute__((const))
    #define ATTRIB_MALLOC           __attribute__((malloc))
    #define ATTRIB_NONNULL          __attribute__((nonnull))
    #define ATTRIB_NONNULLEX(...)   __attribute__((nonnull(__VA_ARGS__)))
    #define ATTRIB_INLINE           __attribute__((always_inline))
#else
    #define ATTRIB_CONST            /**/
    #define ATTRIB_MALLOC           /**/
    #define ATTRIB_NONNULL          /**/
    #define ATTRIB_NONNULLEX(...)   /**/
    #define ATTRIB_INLINE           /**/
#endif
/* *INDENT-ON* */

#define DEFAULT_BUF_CAP     256 * (size_t)1024

#include <stddef.h>

/* Bump allocator arena. */
typedef struct arena Arena;

/* Returns a new arena with the specified `capacity`.
 * If `capacity` is 0, a default size of `DEFAULT_BUF_CAP` is used.
 * 
 * On allocation failure, or if `buf` is a non-null pointer and `capacity` is 0,
 * returns `nullptr`. 
 *
 * Passing a `buf` that is smaller than the specified `capacity` would invoke
 * undefined behavior. */
Arena *arena_new(void *buf, size_t capacity);

/* Destroys `arena`, freeing all the memory associated with it.
 *
 * Any pointer allocated by this arena is invalidated after this call. */
void arena_destroy(Arena *arena) ATTRIB_NONNULL;

/* Resets `arena`, invalidating all existing allocations.
 *
 * Whilst existing pointers allocated by this arena are valid after this call 
 * as far as the language is concerned, they should be considered invalid as 
 * using them * would invoke Undefined Behavior. */
void arena_reset(Arena *arena) ATTRIB_NONNULL;

/* Allocates a pointer from `arena`.
 *
 * The allocated pointer is at least aligned to `alignment`.
 *
 * `alignment` must be a power of 2.
 * 
 * `size` must be a multiple of `alignment`.
 *
 * If a request can not be entertained, i.e. would overflow, or `arena` is full,
 * the function returns `nullptr`. The function also returns a `nullptr` if the 
 * requested `size` is 0 or if `alignment` is not a power of 2, or if `size` is
 * not a multiple of `alignment`.
 *
 * Any allocations made prior to this call are not freed on failure, and remain 
 * valid until the arena is either reset or destroyed.
 */
void *arena_alloc(Arena *arena, size_t alignment, size_t size)
    ATTRIB_MALLOC ATTRIB_NONNULL;

/* Adds a new memory pool to the existing arena `arena`.
 * If `capacity` is 0, a default size of `DEFAULT_BUF_CAP` is used.
 *
 * On allocation failure, or if `buf` is a non-null pointer and `capacity` is 0,
 * returns `nullptr`. 
 *
 * Any allocations made prior to this call are not freed on failure, and remain 
 * valid until the arena is either reset or destroyed.

 * Passing a `buf` that is smaller than the specified `capacity`, or passing an 
 * `arena` that was not returned by `arena_new()` would invoke Undefined Behavior. */
Arena *arena_resize(Arena *restrict arena, void *restrict buf, size_t capacity) 
    ATTRIB_NONNULLEX(1);

/* Allocates a pointer from `arena` large enough for an array of `nmemb` 
 * elements, each of which is `size` bytes.  It is equivalent to the call:
 *
 *              arena_alloc(arena, alignment, nmemb * size);
 * 
 * However, unlike that `arena_alloc()` call, `arena_allocarray()` fails safely 
 * in the case where the multiplication would overflow. If such an overflow 
 * occurs, `arena_allocarray()` returns `nullptr`. 
 *
 * Has the same requirements for `size` and `alignment` as `arena_alloc()`, and 
 * returns `nullptr` for all the cases `arena_alloc()` does.
 *
 * Any allocations made prior to this call are not freed on failure, and remain 
 * valid until the arena is either reset or destroyed.
 */
void *arena_allocarray(Arena *arena, 
                       size_t alignment, 
                       size_t nmemb,
                       size_t size) ATTRIB_MALLOC ATTRIB_NONNULL;

/* Extends the last allocation in place.
 *
 * If `size` is 0, the last allocation is deleted. Else if it is less than the
 * last allocated size, it is shrinked to `size`. Else, it is expanded to `size`
 * bytes.
 *
 * Returns `false` if the request can not be entertained, i.e out of memory.
 * Else it returns `true`.
 */
bool arena_realloc(Arena *arena, size_t size) ATTRIB_NONNULL;

#endif                          /* ARENA_H */

arena.c:

#include "arena.h"

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>

/* *INDENT-OFF* */
/* In C2X/C23 or later, nullptr is a keyword. */
/* Patch up C18 (__STDC_VERSION__ == 201710L) and earlier versions.  */
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ <= 201710L
    #define nullptr ((void *)0)
#endif

#define INITIAL_MPOOL_COUNT  2

#ifdef DEBUG
    #include <string.h>
    #define D(x) x
#else
    #define D(x) (void) 0
#endif
/* *INDENT-ON* */

typedef struct pool {
    size_t count;
    size_t capacity;
    bool is_heap_alloc;
    uint8_t *buf;
} M_Pool;

typedef struct arena {
    size_t count;
    size_t capacity;
    size_t current;
    size_t last_alloc_size;
    M_Pool *pools[];
} Arena;

ATTRIB_INLINE ATTRIB_CONST static inline bool is_power_of_two(uintptr_t x)
{
    return (x & (x - 1)) == 0;
}

ATTRIB_INLINE ATTRIB_CONST static inline bool is_multiple_of(size_t a, size_t b)
{
    return a / b * b == a;
}

static M_Pool *pool_new(void *buf, size_t capacity)
{
    if (capacity == 0) {
        if (buf != nullptr) {
            return nullptr;
        }
        capacity = DEFAULT_BUF_CAP;
    }

    M_Pool *const pool = calloc(1, sizeof *pool);

/* *INDENT-OFF* */
    if (pool != nullptr) {
        *pool = (M_Pool) {
            .capacity = capacity,
            .is_heap_alloc = buf == nullptr,
            .buf = buf ? buf : calloc(1, capacity),
        };
    }
/* *INDENT-ON* */

    return pool;
}

Arena *arena_new(void *buf, size_t capacity)
{
    Arena *const arena = calloc(1,
        sizeof *arena + (INITIAL_MPOOL_COUNT * sizeof arena->pools[0]));

    if (arena == nullptr) {
        return nullptr;
    }

    arena->capacity = INITIAL_MPOOL_COUNT;
    arena->count = 1;
    arena->current = 1;
    arena->pools[0] = pool_new(buf, capacity);

    if (arena->pools[0] == nullptr) {
        free(arena);
        return nullptr;
    }

    return arena;
}

void *arena_alloc(Arena *arena, size_t alignment, size_t size)
{
    if (size == 0 
        || (alignment != 1 && !is_power_of_two(alignment))
        || !is_multiple_of(size, alignment)) {
        return nullptr;
    }

    M_Pool *curr_pool = arena->pools[arena->current - 1];
    uint8_t *const p = curr_pool->buf + curr_pool->count;
    const uintptr_t original = ((uintptr_t) p);

    if (original > UINTPTR_MAX - alignment) {
        return nullptr;
    }

    const uintptr_t remain = original & (alignment - 1);
    const uintptr_t aligned =
        remain != 0 ? original + (alignment - remain) : original;
    const size_t offset = aligned - original;

    if (size > SIZE_MAX - offset) {
        return nullptr;
    }

    size += offset;

    if (size > curr_pool->capacity - curr_pool->count) {
        return nullptr;
    }

    /* Set the optional padding for alignment immediately before a user block, 
     * and the bytes immediately following such a block to non-zero. 
     * The intent is to trigger OBOB failures to inappropiate app use of 
     * strlen()/strnlen(), which keep forging ahead till encountering ascii NUL. */
    D(
        /* 0xA5 is used in FreeBSD's PHK malloc for debugging purposes. */
        if (remain) {
        memset(p + (alignment - remain), 0xA5, alignment - remain);}
    );

    curr_pool->count += size;
    D(memset(curr_pool->buf + curr_pool->count, 0xA5,
            curr_pool->capacity - curr_pool->count));

    arena->last_alloc_size = size;

    /* Equal to "aligned", but preserves provenance. */
    return p + offset;
}

void *arena_allocarray(Arena *arena, 
                       size_t alignment, 
                       size_t nmemb,
                       size_t size)
{
    if (nmemb != 0 && size > SIZE_MAX / nmemb) {
        return nullptr;
    }

    return arena_alloc(arena, alignment, nmemb * size);
}

bool arena_realloc(Arena *arena, size_t size)
{
    if (size == arena->last_alloc_size) {
        return true;
    }

    M_Pool *const curr_pool = arena->pools[arena->current - 1];

    if (size == 0) {
        /* Delete allocation. */
        curr_pool->count -= arena->last_alloc_size;
        arena->last_alloc_size = size;
        return true;
    }

    if (size < arena->last_alloc_size) {
        /* Shrink allocation. */
        curr_pool->count -= arena->last_alloc_size - size;
        arena->last_alloc_size = size;
        return true;
    }

    if (size > (curr_pool->capacity - curr_pool->count)) {
        return false;
    }

    /* Expand allocation. */
    curr_pool->count += size - arena->last_alloc_size;
    arena->last_alloc_size = size;
    return true;
}

Arena *arena_resize(Arena *restrict arena, void *restrict buf, size_t capacity)
{
    if (arena->count >= arena->capacity) {
        arena->capacity *= 2;
        Arena *tmp = realloc(arena,
            sizeof *tmp + (arena->capacity * sizeof arena->pools[0]));

        if (tmp == nullptr) {
            return nullptr;
        }

        arena = tmp;
    }

    M_Pool *const new_pool = pool_new(buf, capacity);

    if (new_pool == nullptr) {
        return nullptr;
    }

    arena->pools[arena->count++] = new_pool;
    ++arena->current;
    return arena;
}

void arena_destroy(Arena *arena)
{
    for (size_t i = 0; i < arena->count; ++i) {
        if (arena->pools[i]->is_heap_alloc) {
            free(arena->pools[i]->buf);
        }
        free(arena->pools[i]);
    }

    free(arena);
}

void arena_reset(Arena *arena)
{
    for (size_t i = 0; i < arena->count; ++i) {
        arena->pools[i]->count = 0;
    }
    arena->current = 1;
}

#ifdef TEST_MAIN

#include <assert.h>
#include <stdalign.h>
#include <stdio.h>

static void test_arena_allocarray(void)
{
    Arena *const arena = arena_new(nullptr, 100);

    assert(arena && "error: arena_new(): failed to allocate memory.\n");

    const int *const nums = arena_allocarray(arena, alignof (int), 10, sizeof *nums);

    assert(nums && "error: arena_allocarray(): failed to allocate memory.\n");
    arena_destroy(arena);
}

static void test_arena_realloc(void)
{
    Arena *const arena = arena_new(nullptr, 100);

    assert(arena && "error: arena_new(): failed to allocate memory.\n");

    assert(arena_alloc(arena, 1, 10));
    assert(arena->pools[0]->count == 10 && arena->last_alloc_size == 10);

    /* Test expansion. */
    assert(arena_realloc(arena, 20));
    assert(arena->pools[0]->count == 20 && arena->last_alloc_size == 20);

    /* Test shrinking. */
    assert(arena_realloc(arena, 15));
    assert(arena->pools[0]->count == 15 && arena->last_alloc_size == 15);

    /* Test deletion. */
    assert(arena_realloc(arena, 0));
    assert(arena->pools[0]->count == 0 && arena->last_alloc_size == 0);

    arena_destroy(arena);
}

ATTRIB_INLINE ATTRIB_CONST static inline bool is_aligned(const void *ptr,
                                                         size_t      byte_count)
{
    return (uintptr_t) ptr % (byte_count) == 0;
}

static void test_debug_magic_bytes(void)
{
    Arena *const arena = arena_new(nullptr, 100);

    assert(arena && "error: arena_new(): failed to allocate memory.\n");
    assert(arena_alloc(arena, 1, 95));
    uint8_t *const curr_pool = arena->pools[0]->buf;

    assert(curr_pool[96] == 0xA5 && curr_pool[97] == 0xA5
        && curr_pool[98] == 0xA5 && curr_pool[99] == 0xA5);
    arena_destroy(arena);
}

static void test_alignment(void)
{
    Arena *const arena = arena_new(nullptr, 1000);

    assert(arena && "error: arena_new(): failed to allocate memory.\n");

    const int *const a = arena_alloc(arena, alignof (int), 5 * sizeof *a);
    const double *const b = arena_alloc(arena, alignof (double), 2 * sizeof *b);
    const char *const c = arena_alloc(arena, 1, 10);
    const short *const d = arena_alloc(arena, alignof (short), 5 * sizeof *d);

    assert(a && is_aligned(a, alignof (int)));
    assert(b && is_aligned(b, alignof (double)));
    assert(c && is_aligned(c, 1));
    assert(d && is_aligned(d, alignof (short)));
    arena_destroy(arena);
}

static void test_growth(void)
{
    Arena *arena = arena_new(nullptr, 1000);

    assert(arena && "error: arena_new(): failed to allocate memory.\n");
    const char *c = arena_alloc(arena, 1, 10000);

    assert(!c);

    arena = arena_resize(arena, nullptr, 10000);
    assert(arena->current == 2 && arena->count == 2);
    assert(arena && "error: arena_new(): failed to allocate memory.\n");
    c = arena_alloc(arena, 1, 10000);
    assert(c);
    arena_reset(arena);
    assert(arena->current == 1 && arena->count == 2);
    arena_destroy(arena);
}

static void test_failure(void)
{
    assert(arena_new(stderr, 0) == nullptr);

    Arena *const arena = arena_new(nullptr, 100);

    assert(arena && "error: arena_new(): failed to allocate memory.\n");
    assert(arena_alloc(arena, 1, 112) == nullptr);
    arena_reset(arena);
    assert(arena_alloc(arena, 16, 80));
    arena_destroy(arena);
}

static void test_allocation(Arena * arena)
{
    assert(arena && "error: arena_new(): failed to allocate memory.\n");

    char *const c = arena_alloc(arena, 1, 5);
    int *const i = arena_alloc(arena, alignof (int), sizeof *i);
    double *const d = arena_alloc(arena, alignof (double), sizeof *d);

    assert(c && i && d && "error: arena_alloc(): failed to allocate memory.\n");

    *c = 'A';
    *i = 1;
    *d = 20103.212;

    printf("&c (char *): %p, c: %c\n"
        "&i (int *): %p, i: %d\n"
        "&d (double *): %p, d: %lf\n",
        (void *) c, *c, (void *) i, *i, (void *) d, *d);
    arena_destroy(arena);
}

static void test_client_static_arena(void)
{
    static uint8_t alignas (max_align_t) static_pool[BUFSIZ];
    Arena *const static_arena = arena_new(static_pool, sizeof static_pool);

    puts("---- Using a statically-allocated arena ----");
    test_allocation(static_arena);
}

static void test_client_automatic_arena(void)
{
    uint8_t alignas (max_align_t) thread_local_pool[BUFSIZ];
    Arena *const thread_local_arena =
        arena_new(thread_local_pool, sizeof thread_local_pool);

    puts("---- Using an automatically-allocated arena ----");
    test_allocation(thread_local_arena);
}

static void test_client_dynamic_arena(void)
{
    uint8_t *const client_heap_pool = malloc(100 * (size_t) 1024);

    assert(client_heap_pool && "error: failed to allocate client_heap_pool.\n");

    Arena *const client_heap_arena =
        arena_new(client_heap_pool, 100 * (size_t) 1024);

    puts("---- Using a dynamically-allocated arena ----");
    test_allocation(client_heap_arena);
    free(client_heap_pool);
}

static void test_lib_dynamic_arena(void)
{
    Arena *const lib_arena = arena_new(nullptr, 100);

    puts("---- Using the library's internal arena ----");
    test_allocation(lib_arena);
}

int main(void)
{
    test_lib_dynamic_arena();
    test_client_dynamic_arena();
    test_client_automatic_arena();
    test_client_static_arena();
    test_arena_allocarray();
    test_arena_realloc();
    test_failure();
    test_alignment();
    test_growth();
    D(test_debug_magic_bytes());
    return EXIT_SUCCESS;
}

#undef ATTRIB_CONST
#undef ATTRIB_MALLOC
#undef ATTRIB_NONNULL
#undef ATTRIB_NONNULLEX
#undef ATTRIB_INLINE
#undef nullptr
#undef DEFAULT_BUF_CAP
#undef INITIAL_MPOOL_COUNT
#undef DEBUG
#undef D

#endif                          /* TEST_MAIN */

Makefile:

CC := gcc-13

CFLAGS += -std=c2x
CFLAGS += -fPIC
CFLAGS += -Wall
CFLAGS += -Wextra
CFLAGS += -Werror
CFLAGS += -Wwrite-strings
CFLAGS += -Wno-unused-variable
CFLAGS += -Wno-parentheses
CFLAGS += -Wpedantic
CFLAGS += -Warray-bounds
CFLAGS += -Wno-unused-function
CFLAGS += -Wstrict-prototypes
CFLAGS += -Wconversion
CFLAGS += -Wdeprecated

CFLAGS += -DTEST_MAIN

TARGET := arena

release: CFLAGS += -O2 -s
release: $(TARGET)

# To run under valgrind, remove the sanitizer.
debug: CFLAGS += -DDEBUG -g3 -ggdb -fanalyzer -fsanitize=address,leak,undefined
debug: $(TARGET)

clean: 
    $(RM) $(TARGET)

.PHONY: release debug clean
.DELETE_ON_ERROR:

Or clone this repository: An Arena Allocator in C.

Review Request:

Have I introduced any new bugs (memory leaks, undefined/implementation-defined) behavior in the code?

Is the documentation lacking at any place?

Are my overflow checks correct?

Naming.

Anything, everything.

Valgrind's output:

==9152== Memcheck, a memory error detector
==9152== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==9152== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==9152== Command: ./arena
==9152== 
---- Using the library's internal arena ----
&c (char *): 0x4a9b110, c: A
&i (int *): 0x4a9b118, i: 1
&d (double *): 0x4a9b120, d: 20103.212000
---- Using a dynamically-allocated arena ----
&c (char *): 0x4a9c200, c: A
&i (int *): 0x4a9c208, i: 1
&d (double *): 0x4a9c210, d: 20103.212000
---- Using an automatically-allocated arena ----
&c (char *): 0x1ffeffdde0, c: A
&i (int *): 0x1ffeffdde8, i: 1
&d (double *): 0x1ffeffddf0, d: 20103.212000
---- Using a statically-allocated arena ----
&c (char *): 0x10c020, c: A
&i (int *): 0x10c028, i: 1
&d (double *): 0x10c030, d: 20103.212000
==9152== 
==9152== HEAP SUMMARY:
==9152==     in use at exit: 0 bytes in 0 blocks
==9152==   total heap usage: 29 allocs, 29 frees, 119,696 bytes allocated
==9152== 
==9152== All heap blocks were freed -- no leaks are possible
==9152== 
==9152== For lists of detected and suppressed errors, rerun with: -s
==9152== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
\$\endgroup\$
10
  • \$\begingroup\$ Have you run valgrind on this? \$\endgroup\$
    – Reinderien
    Commented Apr 7 at 12:29
  • 2
    \$\begingroup\$ @Reinderien Of course, both debug and release builds. "All heap blocks were freed -- no leaks are possible." "ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)". \$\endgroup\$
    – Harith
    Commented Apr 7 at 13:06
  • 1
    \$\begingroup\$ Well done. If only all of the C developers I've worked with were as diligent. \$\endgroup\$
    – Reinderien
    Commented Apr 7 at 13:08
  • 1
    \$\begingroup\$ @Reinderien They have been quite useful to me at times (as long as the programs are not too large) for memory leaks. For instance, see Toby's answer here: codereview.stackexchange.com/a/289238/265278 on my question where he detected a memory leak with the sanitizer that I did not. The leak was only present when the program was sent 2 consecutive newlines, and did not show up under Valgrind normally. But these flags do increase the compilation times a lot. \$\endgroup\$
    – Harith
    Commented Apr 7 at 13:28
  • 1
    \$\begingroup\$ @camp0 As stated in the second comment, valgrind reports nothing awry, but I have included its output here. You're right that it would benefit from a good set of unit tests. \$\endgroup\$
    – Harith
    Commented Apr 10 at 12:51

2 Answers 2

5
\$\begingroup\$

Passing arena to arena_new

Instead of allocated arena on the heap during arena_new, consider passing the arena to the function.

bool arena_new(Arena* arena, void* buf, size_t size) {
    ...
}

This leaves the construction of the Arena upto the caller and they can decide where they want to allocate it. On the stack, heap, thread local/static variable.


is_multiple_of

is_multiple_of can be rewritten as a % b == 0. Compilers will generate the same code as your implementation, but it is a little easier to grok. Compiler explorer link: https://godbolt.org/z/Eef8vq4c3

Also, you have undefined behavior in the function if a user passes in 0 as the alignment. You need to add a check for it and fail early.


Allocating sizes that are not multiples of alignment

The is_multiple_of function essentially limits the types of allocations that a user can make to even sizes (if desired alignment is more than 1). There may be valid use cases for allocating memory that is not a multiple of the alignment; a user may want to allocate an object aligned to the cache boundary (which is 64 or 128 bytes on modern systems).


Consider having helper function/macros

Most users won't care about alignment. Similar to malloc, they just want to specify a size and get a pointer back. Consider having a helper function or macro that would allow them to do so. If you implement it as a macro, users can specify the type of the object they want to allocate.

#define ARENA_ALLOC(arena, ty) arena_alloc(arena, alignof(ty), sizeof(ty))
#define ARENA_ALLOC_ARRAY(arena, ty, sz) arena_allocarray(arena, alignof(ty), sz, sizeof(ty))

int main() {
   Arena* arena = arena_new(/* stuff */);
   int* ptr = ARENA_ALLOC(arena, int);
   double* arr = ARENA_ALLOC_ARRAY(arena, double, 5);
}

Other comments

  • The alignment != 1 check in arena_alloc is superfluous.

  • I find the count field naming a bit weird. I would expect count to represent an actual count (perhaps, the number of allocations within the pool), but it's really the offset within the pool from where the next allocation should happen.

  • ((void*)0) could and probably should be replaced with NULL.

\$\endgroup\$
3
  • \$\begingroup\$ "The alignment != 1 check in arena_alloc is superfluous." ==> Is a single comparison not better than a multiplication, division, and a comparison? \$\endgroup\$
    – Harith
    Commented Apr 9 at 22:30
  • \$\begingroup\$ "This leaves the construction of the Arena upto the caller and they can decide where they want to allocate it." ==> This requires including the definition of the structure in the header, and then it would no longer be opaque, and the client would be free to look into the internals of the structure. How do you find that? \$\endgroup\$
    – Harith
    Commented Apr 9 at 22:31
  • \$\begingroup\$ "Also, you have undefined behavior in the function if a user passes in 0 as the alignment." ==> That's a good catch. \$\endgroup\$
    – Harith
    Commented Apr 9 at 22:34
3
\$\begingroup\$

Useful additions:

Rust's bumpalo crate provides:

  • pub fn chunk_capacity(&self) -> usize - Gets the remaining capacity in the current chunk (in bytes).
  • pub fn allocated_bytes(&self) -> usize - Calculates the number of bytes currently allocated across all chunks in this bump arena.
  • pub fn allocated_bytes_including_metadata(&self) -> usize - Calculates the number of bytes requested from the Rust allocator for this Bump.

These would be useful, and trivial, to add to this allocator as well.

Use the counted_by attribute for the the flexible array member:

typedef struct arena {
    size_t count;
    size_t capacity;
    size_t current;
    size_t last_alloc_size;
    // M_Pool *pools[];
    M_Pool *pools[];        __attribute(__counted_by__((capacity))
} Arena;

One crucial requirement is that the counter must be initialized before the first reference to the flexible-array member. Another requirement is that the array must always contain at least as many elements as indicated by the counter. See the full requirements here. See also: How to use the new counted_by attribute in C (and Linux).

It was introduced in Clang-18 and will be soon available in GCC 15. Code can define it conditionally as other attributes have been defined.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.