A C11 embedded database core built around fixed 64-byte slots and file/buffer physical-offset addressing. It provides a persistent LIFO free-list allocator, a cross-process MPMC FIFO queue, a Robin Hood string hash index, abortable strict-serializable transactions, and on-demand schema migrations.
Small footprint, few dependencies (C standard library plus the platform mmap
API). Drops into a server, an embedded target, or a CLI tool as a lightweight
persistence layer.
Other languages: 简体中文
Many C projects need a small, controllable local store: write a few records, read them back, look them up by string key, possibly queue work across processes, occasionally want ACID transactions and crash recovery.
Endfields DB ships these as a library, not a separate service:
- No extra layer — there is no dedicated server / protocol / client.
#include "endfields.h"and callef_alloc/ef_write_payload/ef_index_get/ef_txn_begindirectly. - Physical-offset addressing — data is laid out in 64-byte aligned slots.
ef_offset_to_ptr(slot_id)is an O(1) pointer lookup, perfect formmapandef_chase-style pointer chasing. - Zero-copy — slots are mapped directly into the caller's address space. The payload bytes are the file bytes; no serialization layer in between.
- One library, many backends — the same
ef_open_exlands on a diskmmap, whileef_open_memoryruns in an embedded RAM arena. Schema, API, CRC scheme, and concurrency model are identical. - Opt-in capabilities — no index?
hash_capacity = 0keeps the file layout identical to legacy v3. Need ACID? Wrap calls inef_txn_begin/commit.
| Category | What you get |
|---|---|
| Persistence | Disk mmap (POSIX mmap / Win32 MapViewOfFile) or pure RAM arena; ef_sync for explicit flush |
| Data model | Fixed 64-byte slots, 48-byte inline payload; oversized objects spill to an OVERFLOW chained blob |
| Addressing | slot id ↔ physical offset ↔ mmap pointer; ef_chase / ef_chase_n walk the next_offset chain |
| Allocator | Persistent LIFO free list (ef_alloc_slot / ef_free_slot); ef_alloc auto-grows when the pool is empty |
| Index | Robin Hood string hash (ef_index_put/get/remove/iterate/clear), auto-rehash + manual shrink |
| Queue | Cross-process MPMC dummy-head FIFO (ef_queue_push/pop), guarded by a superblock spinlock |
| Transactions (v5) | ef_txn_begin / commit / abort; strict serializable isolation; reverse-replay undo log; crash recovery |
| Integrity | Slot header CRC32 + deferred superblock CRC; x86 PCLMULQDQ fast path |
| Migration | v1 → v2/v3 → v4 → v5 upgraded in place; writable open auto-appends the undo-log segment; read-only opens keep the old layout |
Current schema version: v5 (EF_SCHEMA_VERSION 5 in src/ef_config.h).
#include "endfields.h"
#include "ef_index.h"
struct ef_db *db = NULL;
/* File backend + Robin Hood index (hash_capacity must be a power of two, ≤ 65535) */
if (ef_open_ex_hash("data.endf", 64, 256, &db) != EF_OK || db == NULL) {
return 1;
}
/* Allocate a slot, write its payload, index it */
uint64_t id = 0;
ef_alloc(db, &id);
ef_write_payload(db, id, "hello", 5);
ef_index_put(db, "greeting", id);
/* Look it up by key */
uint64_t found = 0;
if (ef_index_get(db, "greeting", &found) == EF_OK) {
/* ... */
}
/* ACID transaction: abort to roll back any failing branch */
ef_txn_begin(db);
ef_alloc_slot(db, &id);
ef_write_payload(db, id, "draft", 5);
ef_index_put(db, "k", id);
if (/* business condition not satisfied */ 0) {
ef_txn_abort(db); /* all writes are undone */
} else {
ef_txn_commit(db); /* atomic */
}
ef_sync(db);
ef_close(db);Full API listings: src/endfields.h,
src/ef_index.h,
src/ef_txn.h.
All data lives in 64-byte aligned slots. The superblock itself is one slot:
status u32 | header_crc u32 | payload[48] | next_offset u64 → 64 bytes total
status values are defined in src/endfields.h
(EF_STATUS_USED, _FREE, _OVERFLOW, _QUEUED, _QUEUE_DUMMY, etc.).
next_offset doubles as the free-list link, the blob chain link, and the
queue link; ef_chase / ef_chase_n walk it.
Data is addressed by byte offset inside the file/buffer, not by memory pointer:
| Conversion | Purpose |
|---|---|
ef_slot_to_offset(slot_id) |
id → physical offset |
ef_offset_to_slot_id(db, offset, *out) |
offset → id |
ef_offset_to_ptr(db, offset) |
offset → a read/write pointer in the current mmap/buffer |
ef_chase(db, start, fn, ctx) |
walk one or many next_offset links |
This means slots can be remapped, files can be remapped, rehash can move the entire slot region — as long as the offsets stay consistent.
v5 (with index + transactions):
[superblock 64B][hash index capacity × 16B][slot region max_slots × 64B][undo-log segment]
v3/v4 (with index, no transactions):
[superblock 64B][hash index capacity × 16B][slot region max_slots × 64B]
v3/v4 (no index, backward compatible):
[superblock 64B][slot region max_slots × 64B]
New files default to hash_capacity = 0 (compact, no-index layout). Any
schema is automatically migrated up to v5 on a writable open, with the
undo-log segment appended and existing data left untouched.
A dummy-head list whose head, tail, and lock all live in the superblock
reserved[]. push and pop happen under the same spinlock; the dummy
sentinel slot is allocated lazily.
ef_queue_emptyis a lock-free heuristic — only reliable as a hint.- For multi-consumer shutdown, after all producers finish, use
ef_queue_drained(a lock-held check) to confirm the queue is empty. - Heavy contention returns
EF_ERR_QUEUE_BUSY; callers must retry.
String keys are hashed with FNV-1a into uint64_t; Robin Hood linear probing
is used. Only the hash is stored, never the original key.
ef_index_putauto-rehashes to the next power-of-two capacity when the load factor exceeds3/4(capped atEF_INDEX_MAX_CAPACITY = 65535).ef_index_getis a lock-free multi-reader lookup via seqlock. If a writer is in flight it returnsEF_ERR_INDEX_BUSY; callers must retry.- All writers (
put/remove/rehash/clear) serialise throughindex_write_lock. ef_index_rehashmoves the slot region, so it must not run concurrently with slot writes.
ef_txn_begin(db);
// Every mutating API (alloc/free/index_put/index_remove/queue_push/queue_pop/
// write_payload/write_blob/...) appends an inverse record to the undo log.
ef_txn_commit(db); // commit: clear undo log, release txn_lock
ef_txn_abort(db); // rollback: reverse-replay the undo log, release lockKey points:
- Strict serializable isolation: a dedicated
txn_lockis decoupled fromindex_write_lockandqueue_lock, so there is no deadlock between the three locks. - During a transaction,
ef_growreturnsEF_ERR_GROWandef_index_putreturnsEF_ERR_INDEX_BUSYwhen it would trigger a rehash. Pre-size the database beforeef_txn_begin. - Cross-process crash recovery: on open, if
txn_state == ACTIVEand the writer pid / epoch does not match the current process, the undo log is replayed automatically and the lock is released. - v4 → v5 migration is transparent to existing data:
ef_sb_migrate_v4_txn_layoutappends the undo-log segment and refreshes the superblock CRC on the first writable open.
Full details: THREADING.md. Quick reference:
| Resource | Thread safety |
|---|---|
ef_queue_push / ef_queue_pop |
✅ MPMC (across processes and threads) |
ef_index_get |
✅ Lock-free multi-reader (seqlock) |
ef_index_put / remove / rehash |
🔒 Single writer (index_write_lock); multiple threads queue |
ef_txn_begin / commit / abort |
🔒 Single transaction (txn_lock CAS); no deadlock with the locks above |
| Free list | ✅ CAS pop/push on GCC/Clang |
Slot payload / blob data / ef_chase |
|
ef_sync / ef_close |
During a transaction, ef_grow and auto-rehash are disabled; the caller is
expected to pre-allocate the needed capacity before ef_txn_begin.
Requires CMake ≥ 3.16 plus a C11-capable compiler (GCC / Clang / MSVC / MinGW).
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
ctest --test-dir build --output-on-failureWindows with MinGW:
cmake -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failurecmake -B build -DCMAKE_BUILD_TYPE=Release \
-DENDFIELDS_WARNINGS_AS_ERRORS=ON -DENDFIELDS_CI_FAST=ON
cmake --build build --parallel
ctest --test-dir build --output-on-failure --timeout 900 --no-tests=errorCI matrix (see .github/workflows/ci.yml):
Linux GCC / Clang (Release, Debug, ASan+UBSan, TSan, coverage),
macOS Clang, Windows MSVC, Windows MinGW, static analysis
(clang-tidy + cppcheck), embedded-only.
| Option | Default | Purpose |
|---|---|---|
ENDFIELDS_EMBEDDED_ONLY |
OFF | Build only the RAM backend |
ENDFIELDS_ENABLE_PREFETCH |
ON | Enable __builtin_prefetch on the chase hot path |
ENDFIELDS_WARNINGS_AS_ERRORS |
OFF | -Werror |
ENDFIELDS_SANITIZE |
OFF | ASan + UBSan |
ENDFIELDS_TSAN |
OFF | ThreadSanitizer |
ENDFIELDS_COVERAGE |
OFF | gcov / lcov |
ENDFIELDS_CI_FAST |
OFF | Short bench loops |
target_link_libraries(your_app PRIVATE endfields)
target_include_directories(your_app PRIVATE path/to/endfields-db/src)Artifacts: libendfields.a (file + memory backend),
libendfields_embedded.a (pure RAM),
and the four test executables endfields_core_test,
endfields_index_queue_test, endfields_embedded_test, endfields_bench.
Tests are split by entry point (see CMakeLists.txt):
tests/test_core.c— basic read/write, CRC, blob, grow, reopen, transaction commit/abort/persist/grow-forbiddentests/test_index_queue.c— index put/get/rehash/shrink/iterate, queue MPMC, v3→v4 / v4→v5 migration, transaction behaviour over index and queuesrc/main_embedded.c— pure RAM backendbench/endfields_bench.c— chase / queue / MPMC / hash /bench_txn_roundtrip
Run a single binary directly: ./build/endfields_core_test. Filter by
CTest name: ctest --test-dir build -R txn --output-on-failure.
endfields-db/
├── CMakeLists.txt
├── README.md # you are here
├── README.zh-CN.md # 简体中文
├── THREADING.md # concurrency / cross-process semantics
├── PROJECT_INDEX.md # codebase handover index
├── CLAUDE.md # project guide for AI assistants
├── docs/
│ └── API_GUIDE.md # full per-function API reference + recipes
├── src/
│ ├── endfields.h / .c # public API and core implementation
│ ├── ef_index.h / .c # Robin Hood index (v4 seqlock + auto-rehash + shrink)
│ ├── ef_sb_layout.h / .c # superblock reserved[] layout + v3→v4→v5 migration
│ ├── ef_txn.h / .c # transaction API and state machine (v5)
│ ├── ef_undo.h / .c # undo-log segment, record format, replay/reset (v5)
│ ├── ef_blob.c # large object chaining
│ ├── ef_grow.c # auto-grow
│ ├── ef_port.h / .c # file/memory I/O abstraction
│ ├── ef_atomic_unaligned.h # atomic helpers for mmap fields
│ ├── ef_crc.h / .c / _pclmul.c # CRC32 portable + x86 PCLMULQDQ
│ ├── ef_config.h # schema version + platform switches
│ ├── main.c / main_embedded.c # legacy test entry points (superseded by tests/)
├── tests/
│ ├── test_common.h / .c
│ ├── test_core.c
│ └── test_index_queue.c
├── bench/
│ └── endfields_bench.c
└── .github/workflows/ci.yml
- Just want to use the API: read the 30-second quickstart above,
then the full per-function reference in
docs/API_GUIDE.mdfor every public call, error code, and end-to-end recipe. The headerssrc/endfields.h,src/ef_index.h,src/ef_txn.hremain the source of truth. - Care about concurrency:
THREADING.md. - Want to understand the code organisation:
PROJECT_INDEX.md. - Need to modify
superblock.reserved[]: only touchsrc/ef_sb_layout.h/.c, and add tests for every migration path (v3→v4, v4→v5, embedded init). - Touching a concurrent path: locally run ASan + UBSan and TSan configurations; add a concurrent variant for any new feature test.
MIT License — Copyright (c) 2026 zayoka