nitro-engine/source/NEAlloc.c
Antonio Niño Díaz f57fa76576 library: Handle failures correctly
The previous code relied too much on assertions, even for things that
are just regular things that may happen while the application is running
(like running out of memory).

- Many assertions have been turned into permanent checks
- Some functions now return "success" or "failure".
- NE_Init3D() and NE_InitDual3D() (and others) have been reworked to
  handle failures gracefully.
2023-04-21 18:19:02 +01:00

647 lines
17 KiB
C

// SPDX-License-Identifier: MIT
//
// Copyright (c) 2008-2011, 2019, 2022 Antonio Niño Díaz
//
// This file is part of Nitro Engine
#include <stdlib.h>
#include "NEMain.h"
#include "NEAlloc.h"
int NE_AllocInit(NEChunk **first_chunk, void *start, void *end)
{
if (first_chunk == NULL)
{
NE_DebugPrint("Invalid arguments");
return -1;
}
if (end <= start)
{
NE_DebugPrint("Invalid range");
return -2;
}
*first_chunk = malloc(sizeof(NEChunk));
if (*first_chunk == NULL)
{
NE_DebugPrint("Not enough memory");
return -3;
}
(*first_chunk)->previous = NULL;
(*first_chunk)->state = NE_STATE_FREE;
(*first_chunk)->start = start;
(*first_chunk)->end = end;
(*first_chunk)->next = NULL;
return 0;
}
int NE_AllocEnd(NEChunk **first_chunk)
{
if (first_chunk == NULL)
{
NE_DebugPrint("Invalid arguments");
return -1;
}
NEChunk *this = *first_chunk;
while (this != NULL)
{
NEChunk *next = this->next;
free(this);
this = next;
}
*first_chunk = NULL;
return 0;
}
// The function resizes this chunk to the provided size, and creates a new chunk
// with the remaining size.
//
// +-----------------+------+
// | THIS | NEXT | Before
// +-----------------+------+
//
// +------+----------+------+
// | THIS | NEW | NEXT | After
// +------+----------+------+
//
// It returns a pointer to the new chunk.
static NEChunk *ne_split_chunk(NEChunk *this, size_t this_size)
{
NE_AssertPointer(this, "NULL pointer");
// Get next chunk and create a new one.
NEChunk *next = this->next;
NEChunk *new = malloc(sizeof(NEChunk));
if (new == NULL)
{
NE_DebugPrint("Not enough memory");
return NULL;
}
// Update pointers in the linked list
// ----------------------------------
new->previous = this;
this->next = new;
if (next == NULL)
{
new->next = NULL;
}
else
{
new->next = next;
next->previous = new;
// It shouldn't be free because deallocating a chunk should merge it
// with any free chunk next to it.
NE_Assert(next->state != NE_STATE_FREE, "Possible list corruption");
}
// Update pointers to start and end of this chunk and the new chunk
// ----------------------------------------------------------------
new->end = this->end;
this->end = (void *)((uintptr_t)this->start + this_size);
new->start = this->end;
return new;
}
// This returns a pointer to the chunk that contains the provided address.
// The start address of the chunk is considered to be part of that chunk, but
// the end address isn't considered part of that chunk.
static NEChunk *ne_search_address(NEChunk *first_chunk, void *address)
{
NE_AssertPointer(first_chunk, "NULL pointer");
NEChunk *this = first_chunk;
uintptr_t addr = (uintptr_t)address;
for ( ; this != NULL; this = this->next)
{
uintptr_t start = (uintptr_t)this->start;
uintptr_t end = (uintptr_t)this->end;
// We've gone too far. This pointer was likely before the allocated
// memory pool.
if (addr < start)
return NULL;
if (addr < end)
return this;
}
return NULL;
}
void *NE_AllocFindInRange(NEChunk *first_chunk, void *start, void *end, size_t size)
{
if ((first_chunk == NULL) || (start == NULL) || (end == NULL) || (size == 0))
{
NE_DebugPrint("Invalid arguments");
return NULL;
}
// Get the chunk that contains the first address. If the start address of
// the first chunk is after the provided start, get the first chunk.
NEChunk *this;
if (start < first_chunk->start)
this = first_chunk;
else
this = ne_search_address(first_chunk, start);
uintptr_t range_end = (uintptr_t)end;
for ( ; this != NULL; this = this->next)
{
// Is this free?
if (this->state != NE_STATE_FREE)
continue;
// "start" is inside "this", but "this->start" may be before "start". In
// that case, we need to calculate the size actually inside the range
// provided by the user.
uintptr_t real_start;
if (this->start < start)
real_start = (uintptr_t)start;
else
real_start = (uintptr_t)this->start;
uintptr_t this_end = (uintptr_t)this->end;
size_t this_size = this_end - real_start;
// Check if the requested chunk would fit here
if (this_size < size)
continue;
// Check if the expected end of the allocated chunk is within the limits
// provided by the user. If so, we've gone over the limit, there is no
// need to keep searching.
uintptr_t expected_end = real_start + size;
if (expected_end > range_end)
return NULL;
return (void *)real_start;
}
return NULL;
}
// This function searches the list and returns a chunk that contains the
// specified range of memory (address, address + size) if it is free.
static NEChunk *ne_search_free_range_chunk(NEChunk *first_chunk,
void *address, size_t size)
{
NE_AssertPointer(first_chunk, "NULL pointer");
// If that range of memory is free, it should be in one single chunk. Look
// for the chunk that contains the base address, and check if the end
// address is also part of that chunk.
NEChunk *chunk = ne_search_address(first_chunk, address);
if (chunk == NULL)
return NULL;
uintptr_t end_address = (uintptr_t)address + size;
uintptr_t chunk_end = (uintptr_t)chunk->end;
if (end_address > chunk_end)
return NULL;
if (chunk->state != NE_STATE_FREE)
return NULL;
return chunk;
}
int NE_AllocAddress(NEChunk *first_chunk, void *address, size_t size)
{
if ((first_chunk == NULL) || (address == NULL) || (size == 0))
{
NE_DebugPrint("Invalid arguments");
return -1;
}
// Force sizes multiple of NE_ALLOC_MIN_SIZE
const size_t mask = NE_ALLOC_MIN_SIZE - 1;
if ((size & mask) != 0)
size += NE_ALLOC_MIN_SIZE - (size & mask);
// Get a free chunk that contains this range of memory. This function
// returns NULL if there is no chunk that contains this range entirely.
// It also returns NULL if it isn't free.
NEChunk *this = ne_search_free_range_chunk(first_chunk, address, size);
if (this == NULL)
return -2;
uintptr_t alloc_start = (uintptr_t)address;
uintptr_t alloc_end = alloc_start + size;
uintptr_t this_start = (uintptr_t)this->start;
uintptr_t this_end = (uintptr_t)this->end;
// If this point is reached we have a free chunk such that:
//
// this_start <= alloc_start && alloc_end <= this_end
//
// - If the start address isn't the same, we need to create a new chunk.
//
// - If the end address isn't the same, we need to create another chunk.
//
// Finally, return the chunk that stays in the middle.
bool this_is_modified = false;
if (this_start < alloc_start)
{
// Split this chunk into two, ignore the first one and get the second
// one (which contains the start address)
this = ne_split_chunk(this, alloc_start - this_start);
if (this == NULL)
return -3;
this_is_modified = true;
// Only the start has changed
this_start = (uintptr_t)this->start;
NE_Assert(this_end == (uintptr_t)this->end, "Unexpected error");
}
this->state = NE_STATE_USED;
if (alloc_end < this_end)
{
// Split this chunk into two as well. The first one is the final desired
// chunk, the second one is more free space.
NEChunk *next = ne_split_chunk(this, size);
if (next == NULL)
{
if (this_is_modified)
NE_Free(first_chunk, this);
return -4;
}
next->state = NE_STATE_FREE;
// Only the end has changed
this_end = (uintptr_t)this->end;
NE_Assert(this_start == (uintptr_t)this->start, "Unexpected error");
}
NE_Assert(size == (this_end - this_start), "Unexpected error");
NE_Assert(this->start == address, "Unexpected error");
return 0;
}
void *NE_Alloc(NEChunk *first_chunk, size_t size)
{
if ((first_chunk == NULL) || (size == 0))
{
NE_DebugPrint("Invalid arguments");
return NULL;
}
// Force sizes multiple of NE_ALLOC_MIN_SIZE
const size_t mask = NE_ALLOC_MIN_SIZE - 1;
if ((size & mask) != 0)
size += NE_ALLOC_MIN_SIZE - (size & mask);
NEChunk *this = first_chunk;
for ( ; this != NULL; this = this->next)
{
// Skip non-free chunks
if (this->state != NE_STATE_FREE)
continue;
size_t this_size = (size_t)this->end - (size_t)this->start;
// If this chunk doesn't have enough space, simply skip it.
if (this_size < size)
continue;
// If we have exactly the space requested, we're done.
if (this_size == size)
{
this->state = NE_STATE_USED;
return this->start;
}
// If we have more space than requested, split this chunk:
//
// | THIS | NEXT |
// +-----------------+------+ Before
// | NOT USED | USED |
//
// | THIS | NEW | NEXT |
// +------+----------+------+ After
// | USED | NOT USED | USED |
NEChunk *new = ne_split_chunk(this, size);
if (new == NULL)
return NULL;
// Flag this chunk as used and the new one as free
this->state = NE_STATE_USED;
new->state = NE_STATE_FREE;
return this->start;
}
// No more chunks... Not enough free space.
return NULL;
}
void *NE_AllocFromEnd(NEChunk *first_chunk, size_t size)
{
if ((first_chunk == NULL) || (size == 0))
{
NE_DebugPrint("Invalid arguments");
return NULL;
}
// Force sizes multiple of NE_ALLOC_MIN_SIZE
const size_t mask = NE_ALLOC_MIN_SIZE - 1;
if ((size & mask) != 0)
size += NE_ALLOC_MIN_SIZE - (size & mask);
// Find last chunk
NEChunk *this = first_chunk;
while (this->next != NULL)
this = this->next;
// Traverse list from end to beginning
for ( ; this != NULL; this = this->previous)
{
// Skip non-free chunks
if (this->state != NE_STATE_FREE)
continue;
size_t this_size = (size_t)this->end - (size_t)this->start;
// If this chunk doesn't have enough space, simply skip it.
if (this_size < size)
continue;
// If we have exactly the space requested, we're done.
if (this_size == size)
{
this->state = NE_STATE_USED;
return this->start;
}
// If we have more space than requested, split this chunk:
//
// | THIS | NEXT |
// +------------------+------+ Before
// | NOT USED | USED |
//
// | THIS | NEW | NEXT |
// +-----------+------+------+ After
// | NOT USED | USED | USED |
// The size of this chunk has to be the current one minus the requested
// size for the new chunk.
NEChunk *new = ne_split_chunk(this, this_size - size);
if (new == NULL)
return NULL;
// Flag the new chunk as used
new->state = NE_STATE_USED;
return new->start;
}
// No more chunks... Not enough free space.
return NULL;
}
int NE_Free(NEChunk *first_chunk, void *pointer)
{
if (first_chunk == NULL)
{
NE_DebugPrint("Invalid arguments");
return -1;
}
NEChunk *this = first_chunk;
// Look for the chunk that corresponds to the given pointer
while (1)
{
// Check if we have reached the end without finding the chunk
if (this == NULL)
return -2;
// If this is the chunk we're looking for, exit loop
if (this->start == pointer)
break;
this = this->next;
}
// If the specified chunk is free or locked, it can't be freed.
if (this->state != NE_STATE_USED)
return -3;
// Chunk found. Free it.
this->state = NE_STATE_FREE;
// Now, check if we can join this free chunk with the previous or the
// next one
NEChunk *previous = this->previous;
NEChunk *next = this->next;
// Check the previous one
if (previous && previous->state == NE_STATE_FREE)
{
// We can join them
//
// | PREVIOUS | THIS | NEXT |
// +----------+----------+------+
// | NOT USED | NOT USED | ???? |
//
// | PREVIOUS | NEXT |
// +---------------------+------+
// | NOT USED | ???? |
if (next)
{
// First, join the previous and the next
next->previous = previous;
previous->next = next;
}
else
{
previous->next = NULL;
}
// Expand the previous one
previous->end = this->end;
// Delete current chunk
free(this);
// Change the active chunk to try to join it with the next one.
this = previous;
}
// Check the next one
if (next && next->state == NE_STATE_FREE)
{
// We can join them
//
// | THIS | NEXT | NEXT NEXT |
// +----------+----------+-----------+ Before
// | NOT USED | NOT USED | USED |
//
// | THIS | NEXT NEXT |
// +---------------------+-----------+ After
// | NOT USED | USED |
NEChunk *next_next = next->next;
if (next_next)
{
// Next Next should be used or locked. If not,
// something bad is happening here.
NE_Assert(next_next->state != NE_STATE_FREE,
"Possible list corruption. (2)");
// First, join this chunk and the next next chunk
next_next->previous = this;
this->next = next_next;
}
else
{
this->next = NULL;
}
// Expand this node one
this->end = next->end;
// Delete next chunk
free(next);
}
return 0;
}
int NE_Lock(NEChunk *first_chunk, void *pointer)
{
if (first_chunk == NULL)
{
NE_DebugPrint("Invalid arguments");
return -1;
}
NEChunk *this = first_chunk;
while (1)
{
if (this->start == pointer)
{
// Check if we are trying to lock a chunk that isn't in use
if (this->state != NE_STATE_USED)
return -2;
this->state = NE_STATE_LOCKED;
return 0;
}
// Couldn't find a chunk at the specified address
if (this->next == NULL)
return -3;
this = this->next;
}
}
int NE_Unlock(NEChunk *first_chunk, void *pointer)
{
if (first_chunk == NULL)
{
NE_DebugPrint("Invalid arguments");
return -1;
}
NEChunk *this = first_chunk;
while (1)
{
if (this->start == pointer)
{
// Check if we are trying to unlock a chunk that isn't locked
if (this->state != NE_STATE_LOCKED)
return -2;
this->state = NE_STATE_USED;
return 0;
}
// Couldn't find a chunk at the specified address
if (this->next == NULL)
return -3;
this = this->next;
}
}
int NE_MemGetInformation(NEChunk *first_chunk, NEMemInfo *info)
{
if ((first_chunk == NULL) || (info == NULL))
{
NE_DebugPrint("Invalid arguments");
return -1;
}
info->free = 0;
info->used = 0;
info->total = 0;
info->locked = 0;
NEChunk *this = first_chunk;
for ( ; this != NULL; this = this->next)
{
size_t size = (uintptr_t)this->end - (uintptr_t)this->start;
switch (this->state)
{
case NE_STATE_FREE:
info->free += size;
info->total += size;
break;
case NE_STATE_USED:
info->used += size;
info->total += size;
break;
case NE_STATE_LOCKED:
// Locked memory isn't added to the total
info->locked += size;
break;
default:
return -2;
break;
}
}
info->free_percent = (info->free * 100) / info->total;
return 0;
}