Modular, observable, high-performance memory management for C
Quick Start Β· Why UMR Β· Engines Β· Observability Β· Integration Β· Roadmap Β· Contributing
UMR is an open-source C memory runtime that sits between your application and the OS. It gives you a familiar malloc-style API plus six specialized allocator engines, a live-allocation tracker, leak detection, ASCII heap visualization, binary snapshots, four policy presets, and a CLI tool β all in a single dependency-free C library.
Application
β
βΌ
UMR Public API umr_malloc Β· arena Β· pool Β· slab Β· buddy Β· fragment
β
βΌ
Allocation Manager βββ Policy (FAST Β· LOW_MEMORY Β· REALTIME Β· DEBUG)
β
ββββββββΌβββββββ¬βββββββββ¬ββββββββββ
βΌ βΌ βΌ βΌ βΌ
General Arena Pool Slab/Buddy Fragment
Engine Bump Fixed Object Boundary-tag
ptr size cache coalescing
β
βΌ
Intelligence Layer tracker Β· leaks Β· heatmap Β· snapshot Β· JSON
β
βΌ
OS Memory Provider mmap (Linux/macOS) Β· VirtualAlloc (Windows)
Note: engines are opt-in via their own APIs.
umr_mallocroutes through the general heap. Specialized engines are never silently selected.
Traditional malloc |
UMR |
|---|---|
| Black-box heap β no visibility | Every allocation is trackable with ID, size, and source location |
| One strategy for everything | Six purpose-built engines you choose explicitly |
| Leaks are hard to find | Built-in leak detector with per-allocation reports |
| Fragmentation is invisible | Fragment engine with quantified external/internal frag ratios |
| No runtime tuning | Four policy presets: FAST, LOW_MEMORY, REALTIME, DEBUG |
| No tooling | umr-cli, ASCII heatmap, JSON dump, binary snapshots |
| Tool | Minimum version |
|---|---|
| CMake | 3.20+ |
| C compiler | GCC 12+, Clang 15+, MSVC 2022 |
| Build backend | Ninja (recommended) or Make |
# Clone
git clone https://github.com/universalmemoryruntime/umr.git
cd umr
# Configure + build (Release)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
# Run the test suite
ctest --test-dir build --output-on-failureDebug build with memory checks, guard pages, and source-location tracking:
cmake -S . -B build-debug \
-DCMAKE_BUILD_TYPE=Debug \
-DUMR_ENABLE_DEBUG=ON \
-DUMR_USE_GUARD_PAGES=ON
cmake --build build-debug
ctest --test-dir build-debug --output-on-failureinclude(FetchContent)
FetchContent_Declare(umr
GIT_REPOSITORY https://github.com/universalmemoryruntime/umr.git
GIT_TAG v0.1.0
)
FetchContent_MakeAvailable(umr)
target_link_libraries(my_app PRIVATE UMR::umr)gcc my_app.c $(pkg-config --cflags --libs umr) -o my_app#include "umr.h"
int main(void) {
umr_init();
// Standard allocation patterns β familiar API, observable internals
char* buf = (char*)umr_malloc(64);
int* nums = (int*)umr_calloc(16, sizeof(int)); // zero-filled
buf = (char*)umr_realloc(buf, 256); // grow in place when possible
umr_free(nums);
umr_free(buf);
umr_shutdown();
}Debug macros record __FILE__ and __LINE__ in every block header
(enabled with -DUMR_ENABLE_DEBUG=ON):
void* p = UMR_MALLOC(256); // recorded: myfile.c:12
UMR_FREE(p);UMR ships six purpose-built engines. Pick the one that fits your workload.
Best for: game frame scratch, per-request server memory, parser nodes.
// One OS allocation on create. All allocs are a pointer bump β near-zero overhead.
umr_arena_t* arena = umr_arena_create(0); // 0 = default 1 MiB chunk
void* node = umr_arena_alloc(arena, sizeof(ASTNode));
void* simd = umr_arena_alloc_aligned(arena, 256, 64); // SIMD-aligned
umr_arena_reset(arena); // rewind all β no OS round-trip, chunks are reused
umr_arena_destroy(arena); // release OS memoryNested arenas (child lives inside parent's memory, destroyed with parent):
umr_arena_t* child = umr_arena_create_child(parent, 0);Best for: game entities, network connections, message objects.
umr_pool_t* pool = umr_pool_create(sizeof(Enemy), 256); // 256 objects / chunk
Enemy* e = (Enemy*)umr_pool_alloc(pool); // O(1) from freelist
// ... use e ...
umr_pool_free(pool, e); // O(1) back to freelist
printf("live: %llu\n", (unsigned long long)umr_pool_live_count(pool));
umr_pool_destroy(pool);Best for: multiple named types, kernel-style object caches, per-cache stats.
umr_slab_cache_t* cache = umr_slab_create("conn_state", sizeof(ConnState));
ConnState* c = (ConnState*)umr_slab_alloc(cache);
// ... use c ...
umr_slab_free(cache, c);
printf("live=%llu slabs=%llu\n",
(unsigned long long)umr_slab_live_count(cache),
(unsigned long long)umr_slab_count(cache));
umr_slab_destroy(cache);Best for: GPU/DMA buffers, large variable-size blocks, sub-allocating fixed OS reservations.
// Mode 1: UMR manages backing memory
umr_buddy_t* b = umr_buddy_create(8 * 1024 * 1024); // 8 MiB pool
void* tex = umr_buddy_alloc(b, 512 * 1024); // rounded to next power-of-two
void* buf = umr_buddy_alloc(b, 4 * 1024);
umr_buddy_free(b, tex); // buddy pairs merged eagerly
umr_buddy_destroy(b);
// Mode 2: caller supplies the memory (must be power-of-two size)
umr_buddy_t* b2 = umr_buddy_create_from(my_gpu_region, 4 * 1024 * 1024);
umr_buddy_destroy(b2); // does NOT free your bufferBest for: fixed-size memory budgets (embedded, GPU VRAM, network buffers) where you need quantified fragmentation control.
// Three search strategies, selectable at creation
umr_fragment_heap_t* h = umr_fragment_heap_create(256 * 1024, UMR_FRAG_BEST_FIT);
// UMR_FRAG_FIRST_FIT
// UMR_FRAG_SEGREGATED
void* a = umr_fragment_alloc(h, 1024);
void* b = umr_fragment_alloc(h, 512);
umr_fragment_release(h, a); // immediate coalescing with adjacent free blocks
// Resize in-place (shrink) or copy-and-grow (like realloc)
b = umr_fragment_resize(h, b, 2048);
// Quantified fragmentation metrics
umr_frag_metrics_t m;
umr_fragment_metrics_get(h, &m);
printf("external frag: %.1f%% largest free: %zu bytes\n",
m.external_frag_ratio * 100.0, m.largest_free_block);
umr_fragment_metrics_print(h, stdout); // full table to terminal
umr_fragment_heap_destroy(h);| Workload | Recommended engine |
|---|---|
| Bulk temporaries sharing one lifetime | Arena |
| Hot single-type alloc/free cycle | Pool |
| Multiple named types with per-cache stats | Slab |
| Variable power-of-two sizes, GPU/DMA | Buddy |
| Fixed budget, arbitrary sizes, frag visibility | Fragment |
| Everything else | umr_malloc |
The intelligence layer is the feature that separates UMR from every other allocator.
umr_stats_t s;
umr_stats_get(&s);
printf("active: %zu bytes peak: %zu bytes frag: %.2f%% live: %llu allocs\n",
s.active_bytes, s.peak_active_bytes,
s.fragmentation_ratio * 100.0,
(unsigned long long)s.live_count);
umr_stats_print(stdout); // formatted table
umr_viz_print_stats(stdout); // extended breakdown with per-engine summaryumr_tracker_enable(); // zero overhead when disabled (default)
// ... run workload ...
umr_viz_print_tracker(stdout); // tabular live allocation list
umr_leak_print(stdout); // leak report
umr_leak_clear(); // declare current state as clean baseline
umr_tracker_disable();Sampling mode for high-throughput paths:
umr_tracker_set_sample_rate(16); // record 1 in 16 allocationsumr_viz_print_heatmap(stdout, 64); // 64 chars wide=== UMR Heap Heatmap (25 spans, 1 cell = 1 span ~256 KiB) ===
Legend: '.' empty 'l' low(1-25%) 'm' mid 'h' high 'F' full 'L' large
.l..l...l...l...l.lllllLL
// Programmatic check with full report
umr_leak_entry_t entries[64];
umr_leak_report_t report = { .entries = entries, .capacity = 64 };
size_t leaked = umr_leak_check(&report);
if (leaked) {
for (size_t i = 0; i < report.count; ++i)
printf("LEAK %p %zu bytes\n",
report.entries[i].ptr, report.entries[i].size);
}
// Or one-liner to stderr
umr_leak_print(NULL); // NULL β stderrumr_shutdown() automatically reports any live allocations when the tracker is enabled.
umr_viz_dump_json(stdout); // machine-readable stats + live allocations
umr_snapshot_save("heap.snap"); // binary snapshot for offline analysisumr-cli json > heap-snapshot.json # via CLIApply a preset that tunes the runtime for your use case:
umr_policy_apply(UMR_POLICY_FAST_MODE); // default β max throughput
umr_policy_apply(UMR_POLICY_LOW_MEMORY_MODE); // aggressively decommit idle spans
umr_policy_apply(UMR_POLICY_REALTIME_MODE); // pre-warm, avoid syscalls on hot path
umr_policy_apply(UMR_POLICY_DEBUG_MODE); // full tracker + canaries + quarantineList all policies with descriptions:
umr_policy_list(stdout);Available UMR policies:
0 FAST_MODE Maximum throughput. Tracker off. Default.
1 LOW_MEMORY_MODE Minimise OS footprint. Idle spans decommitted on free.
2 REALTIME_MODE Low-latency. Common size classes pre-warmed.
3 DEBUG_MODE Full observability. Tracker on. Leak report on shutdown.
The umr-cli tool links against the library and exercises the in-process observability API directly:
umr-cli stats # extended heap stats
umr-cli leaks --enable-tracker # leak report (exit code 2 if leaks found)
umr-cli heatmap 80 # ASCII heatmap at 80 columns
umr-cli tracker --enable-tracker # live allocation table
umr-cli json # JSON snapshot to stdoutExit codes: 0 success Β· 1 usage error Β· 2 leaks detected.
| Option | Default | Description |
|---|---|---|
UMR_BUILD_SHARED |
OFF |
Build as shared library instead of static |
UMR_BUILD_TESTS |
ON |
Build the unit + thread test suite |
UMR_BUILD_EXAMPLES |
ON |
Build all example programs |
UMR_BUILD_BENCHMARKS |
OFF |
Build microbenchmarks (vs libc) |
UMR_ENABLE_DEBUG |
OFF |
Magic checks, source metadata, canaries |
UMR_USE_GUARD_PAGES |
OFF |
Inaccessible guard page after large allocs |
UMR_BUILD_CLI |
ON |
Build umr-cli tool |
UMR_INSTALL |
ON |
Generate install rules + CMake package config |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DUMR_ENABLE_DEBUG=ON \
-DUMR_USE_GUARD_PAGES=ON \
-DUMR_BUILD_BENCHMARKS=ON
cmake --build build --parallel
ctest --test-dir build --output-on-failure# ASan
cmake -S . -B build-asan \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DUMR_ENABLE_DEBUG=ON \
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake --build build-asan
ASAN_OPTIONS=detect_leaks=0:halt_on_error=1 ctest --test-dir build-asan
# TSan
cmake -S . -B build-tsan \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_C_FLAGS="-fsanitize=thread -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"
cmake --build build-tsan
ctest --test-dir build-tsanIf your Windows environment blocks freshly compiled executables, build and run through WSL:
wsl bash run_simple_test.shumr/
βββ core/ Runtime lifecycle, policy, TLS scratch heap
βββ allocator/ Engine implementations
β βββ general_allocator.c
β βββ arena.c
β βββ pool.c
β βββ slab.c
β βββ buddy.c
βββ fragment/ Fragment engine (split, merge, manager)
βββ heap/ Global heap singleton + metadata
βββ debug/ Canaries, quarantine, guard pages
βββ profiler/ Tracker, leak detector, visualizer, snapshot
βββ platform/ OS memory provider (linux / windows / macos)
βββ include/
β βββ umr.h β Public API (stable, ABI freeze post v1)
β βββ umr/internal/ β Private headers (no ABI guarantee)
βββ tests/ One test file per module + test_harness.h
βββ benchmarks/ Microbenchmarks vs libc
βββ examples/ Self-contained usage programs
βββ tools/umr-cli/ In-process CLI
βββ docs/ Architecture, vision, feature specs, roadmap
βββ cmake/ CMake package config + pkg-config templates
A single file covering all four areas β allocator, memory clearing, fragmentation, and visualization:
# On Linux / macOS
cmake --build build --target umr_simple_test
./build/umr_simple_test
# On Windows via WSL
wsl bash run_simple_test.shExpected output:
UMR Simple Test (v0.1.0)
============================================================
=== 1. Allocator ===
[PASS] malloc(0) returns NULL
[PASS] calloc bytes are zero
...
=== 3. Fragmentation (fragment heap) ===
-- strategy: BEST_FIT --
fragment metrics after checkerboard:
region=262144 live=1024 free=260800 ext_frag=0.002
=== 4. Visualization (terminal output) ===
--- umr_viz_print_heatmap (64 cols) ---
.l..l...l...l...l.lllllLL
============================================================
Results: 76 passed, 0 failed
| Version | Status | Contents |
|---|---|---|
v0.1.0 |
Current | General allocator Β· Arena Β· Pool Β· Slab Β· Buddy Β· Fragment engine Β· Tracker Β· Leak detector Β· Visualizer Β· Snapshot Β· Policies Β· CLI Β· CI |
v0.2.0 |
Planned | Performance tuning Β· Benchmark suite vs mimalloc/jemalloc Β· Thread-local caches |
v0.3.0 |
Planned | Relocatable handles (UMR_Handle) Β· Compaction experiments |
v1.0.0 |
Planned | Stable ABI promise Β· Doxygen + MkDocs site Β· Published benchmark report |
v2.x |
Future | Auto-fragmentation optimization Β· Cold memory compression Β· Remote heap debugger |
What's not in v1 by design: garbage collection, automatic pointer relocation, compiler plugins, kernel-mode operation.
| Doc | Contents |
|---|---|
| docs/vision.md | Why UMR exists and what it's trying to be |
| docs/plan.md | Phased roadmap and delivery plan |
| docs/feature.md | Feature-by-feature specification and API contracts |
| docs/architecture.md | System design, module layout, threading model |
| CONTRIBUTING.md | Build setup, coding standards, PR process |
Build the full docs site locally:
pip install mkdocs-material
mkdocs serveGenerate API reference:
doxygen DoxyfileContributions are welcome. Before opening a PR:
# 1. Configure a development build
cmake -S . -B build-dev \
-DCMAKE_BUILD_TYPE=Debug \
-DUMR_ENABLE_DEBUG=ON \
-DUMR_BUILD_TESTS=ON \
-DUMR_BUILD_BENCHMARKS=ON
# 2. Build and run all tests
cmake --build build-dev
ctest --test-dir build-dev --output-on-failure
# 3. Check for regressions
./build-dev/umr_bench_suiteSee CONTRIBUTING.md for the full guide: coding standards, naming conventions, commit style, PR template, and design principles.
- Adding a new example program in
examples/ - Improving Doxygen comments in
include/umr.h - Writing a benchmark comparing two strategies in
benchmarks/ - Extending
test_fragment.cwith edge-case merge tests
Every push and pull request runs the full matrix via GitHub Actions:
| Job | Platform | Checks |
|---|---|---|
| Build + test | Ubuntu, Windows, macOS | Release + Debug |
| AddressSanitizer | Ubuntu | Buffer overflow, use-after-free |
| ThreadSanitizer | Ubuntu | Data races |
MIT Β© 2026 UniversalMemoryRuntime β see LICENSE for the full text.