A modern C11 library that borrows the parts of Rust, Zig, C++, and Python that actually pay off, and stitches them into plain C without bringing along a runtime, code generator, or template compiler. Generic containers, formatted I/O, arbitrary-precision arithmetic, JSON and key-value parsing, cross-platform system utilities — all opt-out via build-time feature flags so you compile only what you use.
Disclaimer: This library is not related to the MISRA C standard or guidelines. The name comes from the author's surname, Siddharth Mishra, nicknamed "Misra" among friends.
- The Perspective
- A Quick Taste
- What You Get
- Build and Install
- Feature Flags
- Six Core Ideas
- 1. Allocators are user-owned values, never global state
- 2.
Scopeis lexical RAII, in plain C - 3. Every object carries a magic; type-confusion dies at the dispatch
- 4. Macros +
_Genericgive you generics without a template compiler - 5. Fallible by default,
Must-variants for the unrecoverable - 6. One header, configured at build time
- Container Tour
- Formatted I/O
- Parsers
- System Utilities
- Contributing
- License
C is the language closest to the hardware, but its standard library shows its age. The library APIs have hidden globals, mute allocator failures, throw away length information, and expect the programmer to keep track of every lifetime by hand. The ergonomic answers other languages settled on — borrow checkers, scope-bound destructors, generics, explicit allocators, fallible-by-default APIs — got there for a reason.
MisraStdC takes those ideas and asks: how much of each works in C11 without adding a runtime? It turns out a lot.
- From Rust: explicit allocators threaded through every constructor, lexical scope-bound lifetimes, fallible-by-default APIs, structured format strings.
- From Zig: allocators as plain values, comptime-style type dispatch
(
_Generic), feature flags that flip whole subsystems on and off. - From C++: RAII patterns — adapted to C's preprocessor, with one honest
C-level caveat about
return/goto. - From Python: a single import (
#include <Misra.h>) is enough.
Nothing in this library hides at runtime. Every macro expands to inlined struct
literals or named runtime helpers you can step through. Every allocator is a
plain stack-allocated struct, no void *state indirection, no hidden globals.
If the compiler can't see the allocator it's about to use, that's a compile
error. If a type lookup goes wrong, that's a LOG_FATAL from a magic check at
the dispatch boundary, not a corrupted heap three function calls later.
#include <Misra.h>
int main(void) {
Scope(alloc, DefaultAllocator) {
typedef Vec(int) IntVec;
IntVec primes = VecInit(); // bound to MisraScope
int values[] = {2, 3, 5, 7, 11, 13};
VecMustInsertRangeR(&primes, values, 0, 6);
VecForeachIdx(&primes, p, idx) {
WriteFmtLn("primes[{}] = {}", idx, p);
}
VecDeinit(&primes);
} // allocator destroyed automatically
}That snippet uses one allocator (DefaultAllocator = a binned per-descriptor
heap built on top of PageAllocator), one generic container, type-safe
formatted I/O, and an iteration macro. No malloc, no printf, no global
state. Every object is on the stack except primes.data, which is reclaimed
when the Scope ends.
- Five concrete allocators, all user-owned, all per-descriptor:
HeapAllocator(binned),PageAllocator(raw OS pages),ArenaAllocator(bump),SlabAllocator(growable fixed-slot pool), andBudgetAllocator(caller-buffer, fixed-budget, no-growth). - Generic containers built on a shared runtime:
Vec(T),List(T),Map(K,V),Graph(T),BitVec,Str(=Vec(char)),Strs(=Vec(Str)). - Arbitrary-precision arithmetic:
Inton top ofBitVec,Floaton top ofInt, with radix-string conversion, modular arithmetic, primality, decimal-exact add/sub/mul/div. - Type-aware formatted I/O with Rust-style
{}placeholders; oneWriteFmt(...)works forint,f64,Str,Int,Float,BitVec, C strings — all dispatched at compile time via_Generic. - JSON and key-value config parsers as opt-in features.
- Cross-platform system utilities: subprocess control, directory listings, mutexes, environment access, current-process info, current-executable path.
- Feature flags that drop whole subsystems from the static library and
from the installed header set. Disabling
bitvec,list,map,graph,int,float,parser_json, etc. removes their.cfiles fromlibmisra_std.aand their.hfiles from the install prefix.
git clone --recursive https://github.com/brightprogrammer/MisraStdC.git
cd MisraStdC
meson setup builddir
ninja -C builddir
ninja -C builddir test # run the test suite
ninja -C builddir install # install to the configured prefixFor development with sanitizers (the default test build):
meson setup builddir -Db_sanitize=address,undefined -Db_lundef=falseIf you only need Vec, Str, Io, and the default heap allocator, turn the
rest off:
meson setup builddir-min \
-Dalloc_arena=false -Dalloc_slab=false -Dalloc_budget=false \
-Dbitvec=false -Dlist=false -Dmap=false -Dgraph=false \
-Dint=false -Dfloat=false \
-Dfile=false -Diter=false \
-Dsys_dir=false -Dsys_proc=false \
-Dparser_json=false -Dparser_kvconfig=false
ninja -C builddir-min
ninja -C builddir-min installThe resulting libmisra_std.a contains only the ten foundation translation
units, and the install prefix contains only forty headers — about a third of
the default build. Adding int automatically pulls in bitvec; adding
graph pulls in vec (already foundation); adding parser_kvconfig pulls in
map. Dependencies between features resolve transitively at configure time.
| Flag | Adds | Auto-pulls |
|---|---|---|
alloc_arena |
ArenaAllocator |
— |
alloc_slab |
SlabAllocator (growable fixed-slot pool) |
— |
alloc_budget |
BudgetAllocator (caller-buffer, fixed-budget) |
— |
alloc_stats |
Per-Allocator byte / call / peak counters |
— |
bitvec |
BitVec packed bit container |
— |
list |
List(T) doubly-linked list |
— |
map |
Map(K,V) hash map |
— |
graph |
Graph(T) directed graph (uses Vec runtime helpers) |
— |
int |
Arbitrary-precision integer Int |
bitvec |
float |
Arbitrary-precision decimal Float |
int → bitvec |
file |
ReadCompleteFile(...) and file helpers |
— |
iter |
Generic Iter(T) iteration helpers |
— |
sys_dir |
DirGetContents(...) and friends |
— |
sys_proc |
ProcCreate(...) / spawn / wait |
— |
parser_json |
JSON read/write | — |
parser_kvconfig |
Key-value config parser | map |
Every enabled feature also defines MISRA_HAVE_<NAME> (= 1) in the
generated Misra/Config.h. User code can #if MISRA_HAVE_BITVEC to compile
against a partial install.
The foundation — always built, can't be opted out — is:
Sys, Sys/Mutex, Std/Log, Std/Memory, Std/Allocator (core + Page +
Heap = DefaultAllocator), Std/Container/Vec, Std/Container/Str,
Std/Io. LOG_FATAL formats its message through Str + Io, so those
two are foundation by transitive necessity.
The feature-flag system is not theatre — disabled features really do leave
the static library. Three measurements per configuration, all gcc 15.2,
x86_64, Linux, meson setup --buildtype=minsize -Db_sanitize=none:
- Archive raw —
libmisra_std.astraight out of the build, including debug section metadata, relocation tables, and unresolved-symbol entries. - Archive stripped — same archive after
strip --strip-unneeded. - Consumer stripped — a tiny program that uses
Vec,Scope, andWriteFmtLnlinked against the archive and stripped. This is the realistic number for "how much of the library actually ends up in my binary."
| Configuration | Archive raw | Archive stripped | Consumer stripped |
|---|---|---|---|
| Foundation only | 501 KB | 222 KB | 131 KB |
Default (everything, alloc_stats on) |
1 549 KB | 662 KB | 247 KB |
A Vec-using consumer ends up at 127 KB against the foundation-only
build, 243 KB against the full build — the linker pulls in only the .o
files your code references, regardless of how big the archive gets. Disabling
a feature you don't need really does keep its code out of your binary.
Every dynamically-sized object in MisraStdC stores a single Allocator *
— a pointer to a shared allocator, not a private one. The library
owns no process-wide heap, no thread-local fallback, no implicit "default"
instance; the caller picks the allocator and decides who shares it.
A typed allocator is a struct with state inline on the stack:
DefaultAllocator alloc = DefaultAllocatorInit(); // ~160 B on the stack
Vec(int) a = VecInit(&alloc);
Vec(int) b = VecInit(&alloc); // shares the same pool
Vec(int) c = VecInit(&alloc);
...
VecDeinit(&a); VecDeinit(&b); VecDeinit(&c);
DefaultAllocatorDeinit(&alloc);What that pointer says is two things, and both are about the allocator, not the object:
- Where memory comes from. Every allocation / realloc / free routes through this allocator. Objects sharing one allocator share its backing pool — page reuse, slot reuse, free-list locality for free.
- When the memory becomes invalid. When the allocator dies (or its
Scopeends), every object still pointing at it has danglingdata. The allocator must outlive every object that uses it.
So the natural mental model: one allocator per logical lifetime. Per request, per file parse, per game tick, per session. Everything created in that work-unit shares an allocator and dies with it. This is not a small optimisation — it is the deliberate substitute for tracking per-object lifetimes by hand.
The library ships five backends:
PageAllocator— rawmmap/VirtualAlloc. The foundation under every growing allocator, no libc heap.HeapAllocator— power-of-two size-class bins (16–2048 B) plus a page-passthrough for large allocations.DefaultAllocatoris a typedef for this. Best fit when you need per-objectfree(long-lived caches, arbitrary delete patterns).ArenaAllocator— bump cursor over page-backed chunks.AllocatorFreeis a no-op; everything is released together onArenaAllocatorDeinit. Best fit when "everything dies together" — parsers, per-request work, per-frame scratch.SlabAllocator— fixed-size slots with an intrusive free list, grows by pulling more page-backed slabs on demand. Best fit for homogeneous workloads (e.g. a list of fixed-size nodes).BudgetAllocator— caller hands in a fixed memory region at init; slots are carved out of it and never replenished. Best fit for freestanding contexts or hard caps.
The library defines a small _Generic whitelist (ALLOCATOR_OF) so any of
these can pass anywhere an Allocator * is expected.
Pointer-escape pitfall. Because containers only hold an Allocator *,
they trivially survive being copied or returned, but their backing pages
do not. The rule is:
Never return or store a container whose backing allocator lives on your stack frame. The container header is fine, but its
datawill dangle the moment the allocator's stack frame is reclaimed. If a value needs to outlive the caller, allocate it through an allocator that outlives the caller too — usually one passed in by the caller, or a longer-lived per-subsystem allocator.
A common variant: putting a Vec into a Map by ownership transfer
across a Scope boundary. The Map lives longer than the Scope, so
the inserted Vec's data points at pages that are about to be unmapped.
Deep-copy-on-insert (VecInitWithDeepCopy(...), MapInitFull(...) with
copy callbacks) avoids this — the destination container rebuilds the
storage through its own allocator.
Each Allocator base carries an AllocatorStats struct that the dispatch
layer updates on every allocate / reallocate / deallocate:
DefaultAllocator alloc = DefaultAllocatorInit();
Allocator *a = ALLOCATOR_OF(&alloc);
Vec(int) v = VecInit(a);
for (int i = 0; i < 10000; i++) VecMustPushBackR(&v, i);
AllocatorStats s = AllocatorGetStats(a);
WriteFmtLn("allocs={}, frees={}, in_use={} B, peak={} B",
s.allocations, s.deallocations, s.bytes_in_use, s.peak_bytes_in_use);The seven fields are bytes_requested, bytes_in_use, peak_bytes_in_use,
allocations, reallocations, deallocations, and failed_allocations.
Counters live on the Allocator base, so every typed backend gets
accounting for free — no per-allocator implementation cost. The whole
machinery is gated by the alloc_stats feature flag (default on); when
disabled the struct shrinks and the dispatch path drops the counter
updates entirely.
Manually pairing *AllocatorInit() and *AllocatorDeinit(&...) at every
exit point is the kind of bookkeeping nobody enjoys. Scope is a macro
that turns a block of code into an allocator lifetime:
Scope(alloc, DefaultAllocator) {
Vec(int) v = VecInit(); // bound to MisraScope (the internal pool)
Vec(int) w = VecInit(alloc); // bound to the named user pool
Str line = StrInitFromZstr("hello");
...
VecDeinit(&v);
VecDeinit(&w);
StrDeinit(&line);
} // both allocators destroyed automaticallyScope(name, AllocType) introduces two stack-resident typed allocators:
name— the user-visible pool. Pass it to helpers explicitly when you want allocations to land in your named slot.MisraScope— an internal pool that every zero-argument*Init()macro picks up implicitly. Library scratch and your named allocations stay separate by default.
When control leaves the block, both pools are destroyed.
ScopeWith(alloc) is the helper-side counterpart: borrow a caller-owned
Allocator * and expose it as MisraScope inside the block, without taking
ownership. ExitScope is an alias for break and runs the cleanup.
The one C-level caveat: return and goto that leave a Scope skip the
cleanup. There's no portable workaround in C (GCC/Clang's
__attribute__((cleanup)) works but MSVC has nothing equivalent). Use
ExitScope to break out cleanly before returning.
Every container (Vec, Str, BitVec, List, Map, Graph, Int,
Float) and every typed allocator carries an 8-byte magic value stamped at
init time. Each runtime helper validates the magic on entry. The cost is a
single 64-bit comparison; the upside is that:
- Passing an uninitialized object (
Vec v = {0}; VecPush(&v, 1);) aborts with a clearLOG_FATALrather than walking through garbage pointers. - Reinterpreting one typed allocator as another (
HeapAllocator *→ArenaAllocator *) aborts at the first dispatch instead of corrupting bins. - Heap-spray and use-after-free patterns trip the validator long before they
reach
mmap-mapped pages.
Each allocator type has its own magic constant
(MISRA_HEAP_ALLOCATOR_MAGIC, MISRA_PAGE_ALLOCATOR_MAGIC, ...). Adding a
new typed allocator means defining its magic and adding it to the
ALLOCATOR_OF _Generic whitelist.
There is no separate code-generation step. The generic shape comes from C11 macros:
Vec(T),List(T),Graph(T),Map(K, V),Pair(xT, yT),Iter(T)expand to anonymous structs. Distinct expansions are distinct types — wrap with atypedefif you want to reuse the type.- Operations like
VecInsertR,VecAt,MapGet,GraphAddNodeR,IntAdd,FloatFromdispatch on the source value's type at the macro layer (_Generic) and forward to shared runtime helpers inSource/. Type information that can't be inferred is carried throughsizeof(T)and__typeof__. - The macros are designed to be expression-shaped where they return values
(so you can branch on
VecInsertL(...)) and statement-shaped where they encode flow control (VecForeach).
This means Vec(int) and Vec(struct Point) share the same runtime code
but get distinct compile-time types. No header explosion, no separately
compiled template instantiations.
The library splits its public API into two parallel forms so each caller decides where to draw the abort boundary:
- Plain form — propagating fallible API. Returns
bool(or a sentinel likeGraphNodeId == 0). The container is left unchanged on failure so the caller can retry or bubble the error up.if (!VecInsertL(&v, item, 0)) { // recover, retry, or bubble up }
Mustvariant — statement-styledo { ... } while (0)wrapper that callsLOG_FATALon failure. Use these at API boundaries where allocation failure isn't recoverable.VecMustInsertL(&v, item, 0); // aborts via LOG_FATAL on failure
Programmer errors — NULL where a pointer is required, out-of-range indices, use of an uninitialized container — always abort, regardless of which form you call. That's the magic check from idea #3 doing its job.
#include <Misra.h>is enough. Misra.h is an umbrella that recursively pulls in every module
the current build enabled, via #if MISRA_HAVE_<NAME> checks against the
generated Misra/Config.h. If you disabled parser_json at configure time,
<Misra/Parsers/JSON.h> is neither installed nor pulled in by Misra.h —
but everything else is reachable through that one include.
You can still include sub-umbrellas (<Misra/Std/Container.h>,
<Misra/Sys.h>) when you want a narrower preprocessor cost, but you never
have to.
All examples below assume you have already included <Misra.h>.
typedef Vec(int) IntVec;
int compare_ints(const void *a, const void *b) {
return *(const int *)a - *(const int *)b;
}
Scope(alloc, DefaultAllocator) {
IntVec numbers = VecInit();
VecMustReserve(&numbers, 10);
// Insert by R-value (copy) and L-value (ownership transfer).
int val = 42;
VecMustInsertL(&numbers, val, 0); // `val` is now owned by `numbers`
VecMustInsertR(&numbers, 10, 0); // copy semantics, insert at front
VecMustInsertR(&numbers, 30, 1);
int items[] = {15, 25, 35};
VecMustInsertRangeR(&numbers, items, VecLen(&numbers), 3);
VecSort(&numbers, compare_ints);
VecForeachIdx(&numbers, current, idx) {
WriteFmtLn("[{}] = {}", idx, current);
}
VecForeachPtr(&numbers, p) {
*p *= 2;
}
VecTryReduceSpace(&numbers);
VecDeleteRange(&numbers, 1, 2);
VecDeinit(&numbers);
}Two insertion styles, intentional:
...L(l-value): transfers ownership. If the container doesn't have a deep-copy callback, the source l-value is zeroed after insertion. Use for values built in a temporary that the container should now own....R(r-value): plain by-value insertion. No ownership claim, no zeroing.
The split makes ownership transfers visible at call sites instead of buried in convention.
Str is a typedef for Vec(char) with a null terminator maintained at
data[length]. Same runtime, but a richer set of macros for text:
Scope(alloc, DefaultAllocator) {
Str text = StrInit();
Str hello = StrInitFromZstr("Hello");
Str world = StrInitFromCstr(", World!", 8);
StrWriteFmt(&text, "{}{}\n", hello, world);
bool starts = StrStartsWithZstr(&text, "Hello");
bool ends = StrEndsWithZstr(&text, "!\n");
Str csv = StrInitFromZstr("one,two,three");
Strs parts = StrSplit(&csv, ",");
VecForeach(&parts, part) {
WriteFmtLn("part: {}", part);
}
StrDeinit(&text); StrDeinit(&hello); StrDeinit(&world); StrDeinit(&csv);
VecForeachPtr(&parts, part) { StrDeinit(part); }
VecDeinit(&parts);
}typedef List(int) IntList;
typedef Graph(Str) NameGraph;
Scope(alloc, DefaultAllocator) {
IntList ll = ListInit();
ListMustPushBack(&ll, 10);
ListMustPushBack(&ll, 20);
ListMustPushFront(&ll, 5);
ListForeach(&ll, n, i) {
WriteFmtLn("ll[{}] = {}", i, n);
}
ListDeinit(&ll);
// Graph with owned string node names.
NameGraph graph = GraphInitWithDeepCopy(NULL, StrDeinit);
GraphNodeId alpha = GraphAddNodeR(&graph, StrZ("Alpha"));
GraphNodeId beta = GraphAddNodeR(&graph, StrZ("Beta"));
GraphAddEdge(&graph, alpha, beta);
GraphForeachNode(&graph, node) {
WriteFmtLn("{}: out={}, in={}",
GraphNodeData(&graph, node),
GraphOutDegree(&graph, GraphNodeGetId(node)),
GraphInDegree(&graph, GraphNodeGetId(node)));
}
GraphDeinit(&graph);
}Graph(T) is built for analysis workloads: reachability, control flow,
dependency traversal. For graph-owned strings, prefer Graph(Str) plus
GraphInitWithDeepCopy(NULL, StrDeinit) and insert with
GraphAddNodeR(..., StrZ("...")) so the graph deep-copies and reclaims on
deinit. Graph(const char *) works too, but only when every stored pointer
outlives the graph (string literals, interned names).
Map(K, V) follows the same pattern; the user supplies a key hash
function and a key compare function at init time, see
Misra/Std/Container/Map/Init.h for the available constructors.
Scope(alloc, DefaultAllocator) {
// BitVec
BitVec flags = BitVecFromStr("10110", alloc);
BitVecPush(&flags, true);
Str bin = BitVecToStr(&flags);
WriteFmtLn("flags = 0b{}", bin);
StrDeinit(&bin);
BitVecDeinit(&flags);
// Int — arbitrary precision.
Int big = IntFromHexStr("deadbeefcafe", alloc);
Int squared = IntInit();
IntMul(&squared, &big, &big);
WriteFmtLn("big = 0x{x}, big^2 = 0x{x}", big, squared);
IntDeinit(&big);
IntDeinit(&squared);
// Float — arbitrary precision, exact decimal.
Float pi = FloatFromStr("3.14159265358979323846", alloc);
WriteFmtLn("pi = {}", pi);
WriteFmtLn("pi (10dp) = {.10}", pi);
FloatDeinit(&pi);
}Construction APIs that pull data in from outside the library (parse a
string, copy a byte buffer, build from a primitive) take an explicit
Allocator * parameter. The container has no existing allocator to inherit
from at that moment, so passing one is the only sensible contract.
Int operations include IntAdd, IntSub, IntMul, IntDivMod, IntPow,
IntGcd, IntLcm, IntIsPrime, IntModPow, base-2/8/10/16 string
conversion, byte import/export (LE and BE), and bit-level access through the
underlying BitVec. Float adds sign, decimal exponent, and a precision
parameter for division.
The placeholder syntax is {} or {[alignment][width][.precision][flags]}.
Type dispatch is compile-time via _Generic, so one call works for every
supported argument type.
WriteFmtLn("Hello, {}! count={}, pi={.4}", name, 42, 3.14159);The macros dispatch through IOFMT(...) which has _Generic cases for
Str, Int, Float, BitVec, const char *, char *, primitive integer
and floating-point types, and char. Anything else falls through to an
unsupported-type handler.
The catch: array types (char[6], const char[10]) are distinct from
pointer types under _Generic. Bind string literals to a pointer variable
first.
const char *title = "Mr."; // good
char name[] = "Alice"; // bad — array type
StrWriteFmt(&buf, "{}", title);Alignment (in a field width):
| Specifier | Description |
|---|---|
< |
Left-aligned (pad on right) |
> |
Right-aligned (pad on left, default) |
^ |
Center-aligned (pad on both sides) |
Endianness (when paired with raw I/O flag r):
| Specifier | Description |
|---|---|
< |
Little Endian |
> |
Big Endian (default) |
^ |
Native Endian |
Type flags:
| Flag | Description | Example output |
|---|---|---|
x |
Hexadecimal lowercase | 0xdeadbeef |
X |
Hexadecimal uppercase | 0xDEADBEEF |
b |
Binary | 0b10100101 |
o |
Octal | 0o777 |
c |
Character formatting, preserve case | raw character bytes |
a |
Character formatting, force lowercase | lowercased |
A |
Character formatting, force uppercase | uppercased |
r |
Raw byte read/write | raw bytes |
e |
Scientific notation lowercase | 1.235e+02 |
E |
Scientific notation uppercase | 1.235E+02 |
s |
Read a quoted string or single word | "hello world" |
Precision (floating-point):
{.2} // two decimal places
{.0} // no decimal places
{.10} // ten decimal placesStrReadFmt / FReadFmt / ReadFmt parse the input cursor and advance it
on success. Pass the cursor as an assignable variable, not a literal:
const char *cursor = "Count: 42, Name: Alice";
i32 count = 0;
Scope(alloc, DefaultAllocator) {
Str user = StrInit();
StrReadFmt(cursor, "Count: {}, Name: {}", count, user);
WriteFmtLn("count = {}, user = {}", count, user);
StrDeinit(&user);
}StrWriteFmt(&str, fmt, ...)/StrReadFmt(cursor, fmt, ...)FWriteFmt(file, fmt, ...)/FWriteFmtLn(...)/FReadFmt(file, fmt, ...)WriteFmt(fmt, ...)/WriteFmtLn(fmt, ...)/ReadFmt(fmt, ...)(stdout / stdin)
JSON read/write is available when parser_json is enabled:
typedef struct { float x, y; } Point;
Scope(alloc, DefaultAllocator) {
Str json = StrInitFromZstr("{\"x\": 10.5, \"y\": 20.0}");
Point p = {0};
StrIter si = StrIterFromStr(&json);
JR_OBJ(si, {
JR_FLT_KV(si, "x", p.x);
JR_FLT_KV(si, "y", p.y);
});
WriteFmtLn("point = ({}, {})", p.x, p.y);
StrDeinit(&json);
}KvConfig is a simple key = value / key: value parser with # and ;
comment support, quoted values, and last-write-wins semantics, available
when parser_kvconfig is enabled:
Scope(alloc, DefaultAllocator) {
Str text = StrInitFromZstr(
"host = localhost\n"
"port = 8080\n"
"debug = true\n"
);
KvConfig cfg = KvConfigInit();
KvConfigParse(StrIterFromStr(&text), &cfg);
Str host = KvConfigGet(&cfg, "host");
i64 port = 0;
bool dbg = false;
KvConfigGetI64(&cfg, "port", &port);
KvConfigGetBool(&cfg, "debug", &dbg);
WriteFmtLn("host={}, port={}, debug={}", host, port, dbg);
StrDeinit(&host);
KvConfigDeinit(&cfg);
StrDeinit(&text);
}Cross-platform wrappers for subprocesses, directories, mutexes, environment
access. The subprocess example demonstrates the explicit-allocator-handoff
pattern: the caller passes the same allocator to ProcCreate and
ProcDestroy.
// Verified with /bin/head: writes a value to the child, expects the same
// echoed back, prints the round-trip result.
int main(int argc, char **argv, char **envp) {
(void)argc;
Scope(alloc, DefaultAllocator) {
Proc *proc = ProcCreate(argv[1], argv + 1, envp, alloc);
ProcWriteToStdinFmtLn(proc, "value = {}", 42);
i32 val = 0;
ProcReadFromStdoutFmt(proc, "value = {}", val);
WriteFmtLn("got value = {}", val);
ProcWaitFor(proc, 1000);
ProcDestroy(proc, alloc);
}
}Contributions are welcome.
Match the existing style in the files you touch. Run the test suite
(ninja -C builddir test) and python Scripts/clang-format.py before
sending a change. The default build (all features on) is what CI runs;
verify your change works there before opening a PR.
- Fork the repository.
- Create a feature branch:
git checkout -b feature/<name>. - Commit with a short imperative subject and a body explaining the why.
- Push:
git push origin feature/<name>. - Open a Pull Request.
This project is dedicated to the public domain under the Unlicense. You may use it, modify it, redistribute it, and sell it without attribution. See LICENSE.md for the full text.