The Juno Memory Module provides a deterministic, efficient memory management solution for embedded systems, designed to operate without dynamic memory allocation. This module is a core component of the LibJuno library, focused on memory safety and deterministic behavior in resource-constrained environments.
Overview
The memory module implements a block-based memory allocation system with reference counting support, allowing for safe and controlled memory management. It is particularly suitable for embedded systems where dynamic memory allocation is undesirable due to fragmentation concerns, indeterministic behavior, or resource limitations.
Memory Block Visualization
To help understand how memory blocks work in Juno, here's a visualization of the memory structure:
┌─────────────────────────────────────────┐
│ JUNO_MEMORY_BLOCK │
│ ┌─────────┬─────────┬─────────┬───────┐ │
│ │ Block 0 │ Block 1 │ Block 2 │ ... │ │
│ └─────────┴─────────┴─────────┴───────┘ │
└─────────────────────────────────────────┘
Memory Array
┌─────────────────────────────────────────┐
│ JUNO_MEMORY_BLOCK_METADATA │
│ ┌─────────┬─────────┬─────────┬───────┐ │
│ │ Meta 0 │ Meta 1 │ Meta 2 │ ... │ │
│ └─────────┴─────────┴─────────┴───────┘ │
└─────────────────────────────────────────┘
Metadata Array
┌───────────────────────────┐
│ JUNO_MEMORY_BLOCK_T │
│ │
│ - Type header │
│ - Pointer to memory array │
│ - Pointer to metadata │
│ - Type size │
│ - Length │
│ - Used blocks counter │
│ - Freed blocks counter │
│ - Failure handler │
└───────────────────────────┘
Block Control Structure
When calling JunoMemory_BlockApi()
:
- You pass in a pre-allocated memory array (
JUNO_MEMORY_BLOCK
)
- You pass in a pre-allocated metadata array (
JUNO_MEMORY_BLOCK_METADATA
)
- The function initializes the control structure that tracks:
- Which blocks are in use
- Which blocks were freed and can be reused
- How to handle errors
Getting Started
This quick-start guide will help you understand and use the Juno Memory Module in a few simple steps:
Step 1: Declare Memory Blocks
First, declare a memory block and its metadata for your data type:
typedef struct {
int id;
float value;
} MY_DATA_T;
#define JUNO_MEMORY_BLOCK_METADATA(name, length)
Macro to declare a static array for memory metadata.
Definition memory_api.h:45
#define JUNO_MEMORY_BLOCK(name, type, length)
Macro to declare a static memory block and its associated free stack.
Definition memory_api.h:40
Step 2: Initialize the Memory Allocator
Create and initialize a memory allocator to manage the block:
JUNO_MEMORY_ALLOC_T tMemAlloc = {0};
&tMemAlloc,
gptMyMemoryBlock,
gptMyMemoryMetadata,
sizeof(MY_DATA_T),
10,
pfcnErrorHandler,
NULL
);
return -1;
}
JUNO_STATUS_T JunoMemory_BlockApi(JUNO_MEMORY_ALLOC_T *ptJunoMemory, void *pvMemory, JUNO_MEMORY_BLOCK_METADATA_T *ptMetadata, size_t zTypeSize, size_t zLength, JUNO_FAILURE_HANDLER_T pfcnFailureHandler, JUNO_USER_DATA_T *pvFailureUserData)
Initializes a memory block for allocation. Sets up a memory block with an associated free stack for m...
Definition juno_memory_block.c:194
@ JUNO_STATUS_SUCCESS
Definition status.h:24
enum JUNO_STATUS_TAG JUNO_STATUS_T
Step 3: Allocate Memory
Allocate memory for your data type:
tStatus = ptApi->Get(&tMemAlloc, &tMemory, sizeof(MY_DATA_T));
return -1;
}
MY_DATA_T *ptData = (MY_DATA_T*)tMemory.
pvAddr;
ptData->id = 1;
ptData->value = 3.14f;
Structure for an allocated memory segment. Describes the allocated memory with a pointer to the start...
Definition memory_api.h:65
void * pvAddr
Pointer to the allocated memory.
Definition memory_api.h:67
Step 4: Share Memory with Reference Counting
Use reference counting to safely share memory:
MY_DATA_T *ptSharedData = (MY_DATA_T *)
JUNO_REF(memoryRef)->pvAddr;
printf(
"Reference count: %zu\n", tMemory.
iRefCount);
#define JUNO_NEW_REF(name)
Create a new reference for memory.
Definition memory_api.h:50
#define JUNO_REF(name)
Define a reference for memory.
Definition memory_api.h:48
size_t iRefCount
The reference count for this memory.
Definition memory_api.h:71
Step 5: Release Memory
Release memory when you're done with it:
printf(
"Reference count: %zu\n", tMemory.
iRefCount);
tStatus = ptApi->Put(&tMemAlloc, &tMemory);
printf("Error: Cannot free memory with active references\n");
}
Step 6: Using the Memory API (Alternative Approach)
For a more dynamic approach, you can use the Memory API interface:
tStatus = ptMemApi->Get(&tMemAlloc, &tMemory, sizeof(MY_DATA_T));
tStatus = ptMemApi->Put(&tMemAlloc, &tMemory);
Common Pitfalls to Avoid
- Not checking status codes: Always check return values for errors.
- Freeing memory with active references: This will fail with
JUNO_STATUS_REF_IN_USE_ERROR
.
- Not initializing memory structures: Always initialize
JUNO_MEMORY_T
with {}
before passing to functions.
- Using the wrong size: Always pass the correct size for your data type.
Features
- Block-based memory allocation: Pre-allocated memory blocks for deterministic behavior
- Reference counting: Prevents premature deallocation of shared memory resources
- Type-safe allocation: Memory blocks are typed for better memory safety
- Failure handling: Comprehensive error reporting through configurable failure handlers
- No heap fragmentation: All memory is pre-allocated, eliminating fragmentation concerns
- Minimal dependencies: Designed to work without the standard C library when necessary
Core Concepts
Memory Blocks
Memory blocks are the foundation of Juno's memory management. These are statically allocated arrays of a specific type and size, declared using macros:
Memory Allocation
Memory is allocated from a pre-defined block using the Juno_MemoryGet
function or through the memory API:
JUNO_STATUS_T tStatus = ptApi->Get(&tMemAlloc, &tMemory,
sizeof(MY_STRUCT_T));
Reference Counting
The module provides reference counting to safely share memory between different components:
JUNO_MEMORY_T *ptMemoryRef = Juno_MemoryGetRef(&tExistingMemory);
Juno_MemoryPutRef(ptMemoryRef);
Memory Deallocation
Memory is returned to the allocator using the Juno_MemoryPut
function when it's no longer needed:
Data Types
JUNO_MEMORY_T
The fundamental structure representing an allocated memory segment:
{
};
size_t zSize
Size of the allocated memory, in bytes.
Definition memory_api.h:69
JUNO_MEMORY_BLOCK_T
Structure representing a block-based memory allocator:
struct JUNO_MEMORY_BLOCK_TAG
{
JUNO_MEMORY_ALLOC_HDR_T tHdr;
uint8_t *pvMemory;
size_t zTypeSize;
size_t zLength;
size_t zUsed;
size_t zFreed;
};
JUNO_MEMORY_ALLOC_T
Union for a generic memory allocation that currently supports block-based allocation:
union JUNO_MEMORY_ALLOC_TAG
{
JUNO_MEMORY_ALLOC_HDR_T tHdr;
};
struct JUNO_MEMORY_BLOCK_TAG JUNO_MEMORY_BLOCK_T
Definition memory_api.h:53
API Reference
Initialization
void *pvMemory,
size_t zTypeSize,
size_t zLength,
);
void(* JUNO_FAILURE_HANDLER_T)(JUNO_STATUS_T tStatus, const char *pcCustomMessage, JUNO_USER_DATA_T *pvUserData)
Definition status.h:44
void JUNO_USER_DATA_T
Definition status.h:43
Initializes a memory block for allocation. Parameters:
ptMemBlk
: Pointer to the memory block structure to initialize
pvMemory
: Pointer to the pre-allocated memory used for allocations
pvMetadata
: Pointer to the metadata array for tracking memory blocks
zTypeSize
: Size in bytes of each element in the block
zLength
: Total number of elements in the block
pfcnFailureHandler
: Optional callback function to handle failures
pvUserData
: Optional user data passed to the failure handler
Memory Operations
JUNO_MEMORY_ALLOC_T *ptMem,
size_t zSize
);
Allocates memory from the specified allocator. Parameters:
ptMem
: Pointer to the memory allocation structure
ptMemory
: Pointer to store allocation details
zSize
: Size of memory to allocate in bytes
JUNO_MEMORY_ALLOC_T *ptMem,
size_t zNewSize
);
Updates an existing memory allocation to a new size. Parameters:
ptMem
: Pointer to the memory allocator
ptMemory
: The memory to update
zNewSize
: The new size for the memory
JUNO_MEMORY_ALLOC_T *ptMem,
);
Releases memory back to the allocator. Parameters:
ptMem
: Pointer to the memory allocation structure
ptMemory
: Pointer to the memory to free
Reference Management
Gets a reference to memory, incrementing its reference count. Parameters:
ptMemory
: The memory to reference
- Returns: The same memory pointer with increased reference count
Releases a reference to memory, decrementing its reference count. Parameters:
ptMemory
: The memory reference to release
Usage Example
The following example demonstrates how to use the Juno Memory Module to implement a simple single linked list with reference counting:
#include "juno/memory/memory.h"
#include "juno/memory/memory_types.h"
#include <stdio.h>
typedef struct SINGLE_LINKED_LIST_NODE_TAG {
struct SINGLE_LINKED_LIST_NODE_TAG *ptNext;
int iData;
} SINGLE_LINKED_LIST_NODE_T;
typedef struct {
SINGLE_LINKED_LIST_NODE_T *ptHead;
size_t zLength;
} SINGLE_LINKED_LIST_T;
printf("Error: %s\n", pcMsg);
}
int main() {
JUNO_MEMORY_ALLOC_T tMemAlloc = {};
&tMemAlloc,
nodeMemory,
nodeMetadata,
sizeof(SINGLE_LINKED_LIST_NODE_T),
100,
FailureHandler,
NULL
);
return -1;
}
const JUNO_MEMORY_API_T *ptMemApi = tMemAlloc.tBase.ptApi;
SINGLE_LINKED_LIST_T tList = {
.pfcnFailureHandler = FailureHandler
};
for(int i = 0; i < 5; i++) {
tStatus = ptMemApi->Get(&tMemAlloc, &tNodeMem, sizeof(SINGLE_LINKED_LIST_NODE_T));
break;
}
SINGLE_LINKED_LIST_NODE_T *ptNewNode = (SINGLE_LINKED_LIST_NODE_T*)tNodeMem.
pvAddr;
ptNewNode->iData = i;
ptNewNode->tMemory = tNodeMem;
ptNewNode->ptNext = tList.ptHead;
tList.ptHead = ptNewNode;
tList.zLength++;
}
SINGLE_LINKED_LIST_T tRefList = {
.pfcnFailureHandler = FailureHandler
};
if(tList.ptHead) {
JUNO_NEW_REF(nodeRef) = Juno_MemoryGetRef(&tList.ptHead->tMemory);
tRefList.ptHead = (SINGLE_LINKED_LIST_NODE_T*)
JUNO_REF(nodeRef)->pvAddr;
tRefList.zLength = tList.zLength;
}
printf("Original list:\n");
SINGLE_LINKED_LIST_NODE_T *ptCurrentNode = tList.ptHead;
while(ptCurrentNode) {
printf("Node data: %d (ref count: %lu)\n",
ptCurrentNode->iData,
ptCurrentNode->tMemory.iRefCount);
ptCurrentNode = ptCurrentNode->ptNext;
}
printf("\nAttempting to free the original list:\n");
while(tList.ptHead) {
SINGLE_LINKED_LIST_NODE_T *ptTemp = tList.ptHead;
tList.ptHead = tList.ptHead->ptNext;
tStatus = ptMemApi->Put(&tMemAlloc, &ptTemp->tMemory);
printf("Node still has references, can't free yet\n");
}
}
printf("\nReleasing reference and freeing memory:\n");
if(tRefList.ptHead) {
Juno_MemoryPutRef(&tRefList.ptHead->tMemory);
tStatus = ptMemApi->Put(&tMemAlloc, &tRefList.ptHead->tMemory);
printf("Memory successfully freed\n");
}
}
return 0;
}
@ JUNO_STATUS_REF_IN_USE_ERROR
Definition status.h:38
Error Handling
The memory module reports errors through status codes of type JUNO_STATUS_T
:
JUNO_STATUS_SUCCESS
: Operation completed successfully
JUNO_STATUS_MEMALLOC_ERROR
: Failed to allocate memory (block is full)
JUNO_STATUS_MEMFREE_ERROR
: Failed to free memory (invalid address or double free)
JUNO_STATUS_NULLPTR_ERROR
: Null pointer provided to function
JUNO_STATUS_INVALID_REF_ERROR
: Invalid memory reference
JUNO_STATUS_REF_IN_USE_ERROR
: Memory cannot be freed because references exist
JUNO_STATUS_INVALID_TYPE_ERROR
: Invalid memory allocator type
JUNO_STATUS_INVALID_SIZE_ERROR
: Invalid size parameter (e.g., zero size)
Best Practices
- Always check status codes: Memory operations can fail, especially in resource-constrained environments.
- Use reference counting: When sharing memory between components, use the reference counting API.
- Define appropriate block sizes: Size your memory blocks based on your application's needs to avoid waste.
- Implement failure handlers: Use failure handlers to catch memory issues early.
- Clear returned memory: Although the allocator zeros memory blocks, initialize your data structures explicitly.
Understanding Reference Counting Macros
The reference counting macros (JUNO_REF
and JUNO_NEW_REF
) can be confusing at first. Here's a detailed explanation of how they work:
JUNO_NEW_REF Macro
The JUNO_NEW_REF
macro creates a new pointer to hold a reference to a memory object:
JUNO_NEW_REF(myMemoryRef) = Juno_MemoryGetRef(&originalMemory);
This is equivalent to:
JUNO_MEMORY_T *REFmyMemoryRef = Juno_MemoryGetRef(&originalMemory);
JUNO_REF Macro
The JUNO_REF
macro is used to access the reference you created with JUNO_NEW_REF
:
MY_DATA_T *data = (MY_DATA_T *)
JUNO_REF(myMemoryRef)->pvAddr;
This is equivalent to:
MY_DATA_T *data = (MY_DATA_T *)REFmyMemoryRef->
pvAddr;
Example Usage Pattern
Here's a complete example of using these macros:
tStatus = ptApi->Get(&tMemAlloc, &tMemory, sizeof(MY_DATA_T));
MY_DATA_T *ptSharedData = (MY_DATA_T *)
JUNO_REF(dataRef)->pvAddr;
When to Use Reference Counting
Use reference counting when:
- Sharing memory between multiple components
- Passing memory to a function that does not need ownership
- Storing memory in a data structure outside the original allocation scope
Reference counting prevents memory from being freed while it's still in use, avoiding a common class of memory errors.