A self-contained C library for building and querying hierarchical key-value trees in memory. Nodes are addressed by slash-separated paths; each node holds named string attributes. No external dependencies.
CFI is well-suited to structured message or protocol data: anything where you have repeated, named sub-elements (headers, parts, recipients) that you want to address by path and enumerate without knowing the shape upfront.
make # builds cfi.a + example binaries
make check # run the test suite
make install # install cfi.a and headers to /usr/local (override PREFIX=)CI runs on every push against Ubuntu and macOS.
/* add three recipients as indexed child nodes under /headers */
char *ctx;
ctx = CfiIndexer("to", 0);
CfiAdd(h, ctx, 1); /* create /headers/to[0] and go there */
CfiSet(h, "name", "Bob Smith");
CfiSet(h, "address", "bob@example.com");
CfiGoto(h, "/headers");
free(ctx);
/* ... repeat for to[1], to[2] ... */
/* enumerate recipients without knowing the count upfront */
char **children;
CfiLook(h, "/headers", CFI_LOOK_CONTEXT, &children);
for (int i = 0; children[i]; i++) {
if (strncmp(children[i], "to[", 3) != 0) continue;
char path[128];
snprintf(path, sizeof(path), "/headers/%s/name", children[i]);
printf("%s\n", CfiValue(h, path, 1));
}
for (int i = 0; children[i]; i++) free(children[i]);
free(children);
/* serialize the whole message to JSON */
char *json = CfiToJson(h, "/");A fully runnable version of this example is in examples/email.c.
Build it with make examples (or make all) and run ./examples/email.
Paths are slash-separated strings: /, /mail, /mail/headers/to[2].
- Absolute paths start with
/. - Relative paths resolve from the current default context (
CfiGoto). - Indexed nodes use bracket notation:
CfiIndexer("recipient", 2)→"recipient[2]"(heap-allocated; caller frees).
| Function | Who owns the returned memory |
|---|---|
CfiGet / CfiValue |
Library — do not free. Copy if you need to keep the value across a mutating call. |
CfiLook |
Caller — free each list[i], then free(list). |
CfiIndexer |
Caller — free(result). |
CfiToJson |
Caller — free(json). |
| All string parameters passed in | Library copies them; caller retains ownership. |
Traverse a subtree without managing lists manually:
typedef struct { int nodes; int attrs; } Counts;
int my_cb(void *h, const char *path, const char *name,
const char *value, int flags, void *ud)
{
Counts *c = ud;
if (flags == CFI_WALK_NODE) c->nodes++;
if (flags == CFI_WALK_ATTRIBUTE) c->attrs++;
return 0; /* non-zero stops traversal */
}
Counts c = {0, 0};
CfiWalk(h, "/", my_cb, &c);/* export subtree → JSON string (caller frees) */
char *json = CfiToJson(h, "/");
/* import JSON into a new handle */
void *h2 = NULL;
CfiCreate(&h2, NULL);
CfiFromJson(h2, "/", json);
free(json);
/* save subtree to a file */
CfiSave(h, "/", "/var/lib/myapp/state.json");
/* load from file into an existing handle (at any context) */
CfiLoad(h2, "/", "/var/lib/myapp/state.json");
/* deep-copy a subtree between handles */
CfiCopy(src, "/section", dst, "/section");JSON format: {"name":"…","attrs":{…},"children":[…]} — field order is fixed;
only string values are used (matching the CFI attribute model).
| Function | Description |
|---|---|
CfiCreate(handle, syntax) |
Create an empty tree. Pass NULL for no schema. |
CfiDestroy(handle) |
Free the tree and all its contents. |
CfiGoto(handle, context) |
Set the default context for relative paths. |
CfiAdd(handle, context, go_there) |
Add a node; optionally move default there. |
CfiDelete(handle, context) |
Remove a node and all its descendants. |
CfiSet(handle, context, value) |
Set (or replace) an attribute. NULL value deletes. |
CfiExtend(handle, context, value) |
Append to an attribute (creates if absent). |
CfiGet(handle, context, &value) |
Read an attribute; *value = NULL if absent. |
CfiValue(handle, context, safe) |
Like CfiGet but returns the string directly. |
CfiLook(handle, context, type, &list) |
List children and/or attributes. |
CfiCount(handle, context, type) |
Count children and/or attributes. |
| Function | Description |
|---|---|
CfiIndexer(name, index) |
Format "name[index]" (heap-allocated; caller frees). |
CfiSetLong(handle, context, value) |
Store a long as a string attribute. |
CfiValueLong(handle, context) |
Read an attribute and return it as long. |
CfiWalk(handle, context, cb, ud) |
Recursive traversal with a callback. |
CfiToJson(handle, context) |
Serialize subtree to JSON string (caller frees). |
CfiFromJson(handle, context, json) |
Import JSON string into tree at context. |
CfiSave(handle, context, path) |
Write subtree to a JSON file. |
CfiLoad(handle, context, path) |
Read a JSON file into tree at context. |
CfiCopy(src, src_ctx, dst, dst_ctx) |
Deep-copy a subtree. |
#include "cfi_error.h"
CfiSetErrorOutput(NULL); /* silence all error output */
CfiSetErrorOutput(stderr); /* explicit stderr (the default) */
CfiSetErrorOutput(some_file); /* redirect to a FILE* */
const CfiLastError *e = CfiGetLastError();
/* e->code, e->message, e->function */| Code | Meaning |
|---|---|
CFI_OK |
Success |
CFI_NOMEMORY |
Allocation failure |
CFI_HANDLE_INVALID |
NULL or corrupt handle |
CFI_CONTEXT_NOSUCH |
Path invalid for the schema |
CFI_CONTEXT_NOTFOUND |
Path not found |
CFI_CONTEXT_FOUND |
Node already exists |
CFI_CONTEXT_AMBIGUOUS |
Path ambiguous |
CFI_CONTEXT_INDEXREQUIRED |
Index missing for multi-valued node |
CFI_PARAMETER_INVALID |
Bad argument |
CFI_ATTRIBUTE_NOTFOUND |
Attribute not found |
CFI_ERROR |
Internal error |
Error state (CfiGetLastError, CfiSetErrorOutput) is thread-local: each thread has its own independent last-error record and error output destination, initialised to the defaults on first use. This matches the errno model — threads do not interfere with each other's error reporting.
Individual handles are not thread-safe. Do not read or write the same handle from multiple threads without external synchronisation (e.g. a mutex). Creating separate handles per thread, or serialising access to a shared handle, are both valid patterns.
Requires C11 (-std=c11) for _Thread_local support.
MIT — see LICENSE.