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 Juno_MemoryBlkInit()
:
- 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_types.h:23
#define JUNO_MEMORY_BLOCK(name, type, length)
Macro to declare a static memory block and its associated free stack.
Definition memory_types.h:17
Step 2: Initialize the Memory Allocator
Create and initialize a memory allocator to manage the block:
gptMyMemoryBlock,
gptMyMemoryMetadata,
sizeof(MY_DATA_T),
10,
pfcnErrorHandler,
NULL
);
return -1;
}
JUNO_STATUS_T Juno_MemoryBlkInit(JUNO_MEMORY_BLOCK_T *ptMemBlk, void *pvMemory, JUNO_MEMORY_BLOCK_METADATA_T *pvMetadata, size_t zTypeSize, size_t zLength, JUNO_FAILURE_HANDLER_T pfcnFailureHandler, JUNO_USER_DATA_T *pvUserData)
Initializes a memory block for allocation. Sets up a memory block with an associated free stack for m...
Definition juno_memory.c:32
@ JUNO_STATUS_SUCCESS
Definition status.h:8
enum JUNO_STATUS_TAG JUNO_STATUS_T
Union for a generic memory allocation. Accommodates various allocation types, currently including blo...
Definition memory_types.h:89
JUNO_MEMORY_BLOCK_T tBlock
Block-based allocation structure.
Definition memory_types.h:91
Step 3: Allocate Memory
Allocate memory for your data type:
return -1;
}
MY_DATA_T *ptData = (MY_DATA_T*)tMemory.
pvAddr;
ptData->id = 1;
ptData->value = 3.14f;
JUNO_STATUS_T Juno_MemoryGet(JUNO_MEMORY_ALLOC_T *ptMem, JUNO_MEMORY_T *ptMemory, size_t zSize)
Generic memory allocation function. Allocates memory by using the appropriate allocation method based...
Definition juno_memory.c:193
Structure for an allocated memory segment. Describes the allocated memory with a pointer to the start...
Definition memory_types.h:60
void * pvAddr
Pointer to the allocated memory.
Definition memory_types.h:62
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)
Definition memory_types.h:27
#define JUNO_REF(name)
Definition memory_types.h:26
size_t iRefCount
The reference count for this memory.
Definition memory_types.h:66
Step 5: Release Memory
Release memory when you're done with it:
printf(
"Reference count: %zu\n", tMemory.
iRefCount);
printf("Error: Cannot free memory with active references\n");
}
JUNO_STATUS_T Juno_MemoryPut(JUNO_MEMORY_ALLOC_T *ptMem, JUNO_MEMORY_T *ptMemory)
Generic memory free function. Releases memory back to the allocator and clears the memory descriptor.
Definition juno_memory.c:234
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);
const JUNO_MEMORY_API_T * Juno_MemoryApi(void)
Retrieves the generic memory API structure.
Definition juno_memory.c:283
API for generic memory allocation operations.
Definition memory_api.h:19
JUNO_STATUS_T(* Get)(JUNO_MEMORY_ALLOC_T *ptMem, JUNO_MEMORY_T *pvRetAddr, size_t zSize)
Allocates memory using the specified memory allocation method.
Definition memory_api.h:26
JUNO_STATUS_T(* Put)(JUNO_MEMORY_ALLOC_T *ptMem, JUNO_MEMORY_T *pvAddr)
Frees an allocated memory block.
Definition memory_api.h:41
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:
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_types.h:64
JUNO_MEMORY_BLOCK_T
Structure representing a block-based memory allocator:
{
};
Structure for memory allocation header. Contains general information including the type of allocation...
Definition memory_types.h:53
Structure representing a block-based memory allocator. Manages a fixed-size memory area along with as...
Definition memory_types.h:74
size_t zTypeSize
Size of each block element.
Definition memory_types.h:78
size_t zLength
Total number of blocks available.
Definition memory_types.h:79
uint8_t * pvMemory
Pointer to the allocated memory area.
Definition memory_types.h:76
size_t zFreed
Current count of freed blocks in the free stack.
Definition memory_types.h:81
JUNO_MEMORY_BLOCK_METADATA_T * ptMetadata
Array of metadata for each block.
Definition memory_types.h:77
size_t zUsed
Current count of allocated blocks.
Definition memory_types.h:80
JUNO_MEMORY_ALLOC_HDR_T tHdr
Header indicating the allocation type.
Definition memory_types.h:75
JUNO_MEMORY_ALLOC_T
Union for a generic memory allocation that currently supports block-based allocation:
{
};
JUNO_MEMORY_ALLOC_HDR_T tHdr
Common header for any allocation type.
Definition memory_types.h:90
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:26
void JUNO_USER_DATA_T
Definition status.h:25
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
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
size_t zNewSize
);
JUNO_STATUS_T Juno_MemoryUpdate(JUNO_MEMORY_ALLOC_T *ptMem, JUNO_MEMORY_T *ptMemory, size_t zNewSize)
Updates the current memory allocation to a new size (realloc).
Definition juno_memory.c:172
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
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
API Access
Retrieves the memory API structure for function pointer-based access.
Usage Example
The following example demonstrates how to use the Juno Memory Module to implement a simple single linked list with reference counting:
#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() {
nodeMemory,
nodeMetadata,
sizeof(SINGLE_LINKED_LIST_NODE_T),
100,
FailureHandler,
NULL
);
return -1;
}
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:22
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:
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.