Deepseek v4.1 Flash reviews: mulle-allocator 9.0.0

· nat's blog


I let Deepseek v4.1 Flash write a critique of the mulle-allocator library. It's hard to please. The whole point of the library is to simplify allocation code with "OOM" aborts. But the bot really believes in ENOMEM, so it's a losing battle to get a glowing review ...


A code review of mulle-allocator, written after building the sources, running the shipped test suite, and running the alloca/coverage/reallocarray tests again under ASAN+UBSAN.

Reviewed commit: b5b56f3 ("new cmake files"), version 9.0.0.

TL;DR #

A small, careful, well-tested abstraction for "which heap does this data structure use". The plumbing is genuinely good: allocation funnels, overflow checks, reallocarray-everywhere, a clean allocator vector struct, and an unusually honest set of docs.

The policy is opinionated and polarising: out-of-memory aborts the process, and the library is a mulle-c11-centric project. If that policy matches your program, it is a solid choice. For a generic library or a service that must degrade gracefully, it is the wrong tool and no amount of polish fixes that.

What it is #

What is good #

The vector design is right #

Putting stdlib-shaped arguments first and the allocator pointer last (src/mulle-allocator-struct.h:143) is a small but real choice: the common reallocarray(block, n, size, allocator) call keeps the hot integer args in argument registers and passes the context last. The comment explaining exactly this (mulle-allocator-struct.h:70) is the kind of thing most projects never write down.

One funnel, overflow checked #

Because malloc/realloc/allocarray all degrade to reallocarray, there is exactly one place that multiplies and exactly one place that can wrap: v_reallocarray builds on standard realloc and therefore performs and checks n * size itself (src/mulle-allocator.c:164). calloc gets its own redundant check unless you opt into MULLE_ALLOCATOR_TRUST_STDLIB (src/mulle-allocator.c:249). This is exactly how I would structure it.

The _strict variants (src/mulle-allocator.c:79, :99) restore stdlib free-and-return-NULL semantics for the zero case, which is the pragmatic escape hatch for people migrating from raw realloc.

Checked size helpers #

mulle_allocator_size_multiply / mulle_allocator_size_add (src/mulle-allocator.h:292, :311) let consumers check count * sizeof(T) before asking for memory. They route overflow through the allocator's fail, which is consistent and testable.

The never-NULL ergonomics are real #

The MULLE_C_NONNULL_RETURN (returns_nonnull) annotation on the whole public API means callers truly can drop null checks and the compiler can remove them downstream. I benchmark-tested nothing, but the generated code is clean.

Honest, unusually good documentation #

This is rare and it is the main reason the review took a fraction of the usual time. The candidness also makes the design debatable rather than mysterious.

Testing and CI are genuinely thorough #

Correctness, hands on #

I compiled src/*.c with GCC and Clang at -Wall -Wextra -Wpedantic -Wconversion -Wshadow with no warnings, ran every non-crashing test and the fails/ tests, and re-ran the alloca/coverage/reallocarray/memset tests under -fsanitize=address,undefined -fno-sanitize-recover=all. All passed, no UB reported. The only ASAN "leaks" came from tests that deliberately use mulle_stdlib_nofree_allocator. The core allocation logic looks correct.

What is bad / questionable #

Abort-on-OOM is the whole personality, and it is a heavy constraint #

mulle_allocation_fail prints and calls abort() (src/mulle-allocator.c:44). The library's premise (README.md:106) is that malloc's portable OOM behaviour is unreliable (FreeBSD hangs, macOS crawls), so you should not pretend to recover. That argument is sound for a process-owning application.

It is not sound for a library. Any library that links this cannot propagate ENOMEM to its caller, cannot fail one request and keep serving others, and cannot be used by a host that installs its own allocator. The README's advice — "for one optional buffer, just call stdlib malloc and check NULL" (README.md:385) — is reasonable, but it means the "one allocation API" promise leaks: you now have two allocation policies and two free paths in the same codebase.

fail being terminal (MULLE_C_NO_RETURN) makes this worse: you cannot even install a handler that logs and continues. It was deliberately made so (the RELEASENOTES.md describes a retry branch that was tried and dropped). That is a defensible engineering call, but it removes the only escape hatch short of longjmp — which the docs warn is running in an arbitrary context.

"The library trusts the vectors you install" is a fragile contract #

The funnel never validates block/n/size; each vector owns zero-checking and overflow-checking (src/mulle-allocator-struct.h:87). The built-ins do it, and the test trust-vector.c asserts the library does not pre-validate.

This is great for zero-overhead and for keeping policy out of the wrapper. It is also a footgun: a custom arena vector that forgets the n * size check silently proceeds with a wrapped size, and the wrapper gives no hint. The safety story now lives in documentation rather than in the type system or a debug check.

Global mutable allocator, single-threaded setup only #

mulle_allocator_default is a process-wide mutable global patched by the "foundation" (src/mulle-allocator.c:287). Configuration is only safe before threads start (documented at README.md:407, mulle-allocator-struct.h:83). That is a known and accepted C pattern, but it means you cannot have two independent subsystems with different allocators without explicitly threading the pointer through every call — which is the actual value proposition, so you pay for it everywhere.

Integration: consumer and producer are both plain CMake #

The build installs a proper CMake package via cmake/share/FindPackageSupport.cmake: namespaced exported targets plus <pkg>-config.cmake and <pkg>-config-version.cmake in lib/cmake/mulle-allocator/. A downstream project only needs

1find_package( mulle-allocator REQUIRED)
2target_link_libraries( app PRIVATE mulle-allocator::mulle-allocator)

I built and ran that against an installed prefix.

On the producer side, stock cmake -S ... -B ..., cmake --build and cmake --install all succeed. No mulle-sde binary is invoked; the generated cmake/share and cmake/reflect files are checked into git and consumed directly. mulle-sde is only the maintenance tool for regenerating them. The one build requirement is that mulle-c11 headers are reachable (the README's -DCMAKE_C_FLAGS="-I<prefix>/include"), which is just the declared dependency.

The real residuals are narrower than the original text:

Per RELEASENOTES.md:20, 9.0.0 changed the allocator struct layout, so the ABI is not stable across majors and all consumers must recompile.

Layering oddities #

The alloca macros are clever and slightly scary #

Portability is clang-shaped by design #

The code uses __typeof__ (MULLE_C_TYPE_OF, mulle-c11-feature.h:164), used by mulle_malloc_for (src/mulle-alloca.h:418), and GCC/Clang attributes throughout. That looks like a portability defect until you read the scope: the project cross-compiles with mulle-clang (there is a toolchain--linux-windows--x86_64-w64-mingw32--mulle-clang.cmake in cmake/share), and native MSVC and macOS/Windows packages are explicitly out of scope. So "not MSVC-clean" is true but irrelevant to the stated targets, and I withdrew that framing.

The narrower observation that survives: no CI job runs the test suite on a cross-compiled Windows/macOS target, so the README's "behaves the same on every OS" line is asserted rather than demonstrated. That is a verification/documentation gap, not a portability defect.

Minor / nitpicky #

Resolved in a follow-up commit (465dd07): the set_fail noreturn-on-a- parameter and the double[8]/double[16] mismatch were both fixed, and 742ab5b widened coverage.json to include the header and mulle-memset.c (badge now 98%, not 100%). I re-ran the suite: 30/30 expected outputs pass, and mulle_allocator_set_fail now takes a cast-free mulle_allocator_fail_t and compiles under -Werror -Wpedantic.

Would I use it in my own C projects? #

Short answer: only for an application where abort-on-OOM is already my policy, and only if I were already inside the mulle ecosystem. Otherwise, no.

Concretely:

For comparison, what I actually reach for in C: a tiny xmalloc-style wrapper when abort-on-OOM is fine, or an explicit allocator struct that returns errors when it is not. mulle-allocator sits in the first camp and is better engineered than my throwaway wrappers, but it is not a middle ground.

Summary #

Area Rating Notes
API design Good Clean vector struct; one reallocarray funnel
Correctness Good Overflow checked; tests + ASAN/UBSAN clean
Memory-safety posture Mixed Safe by construction, but abort-on-OOM is absolute
Docs Excellent Candid, rationale-rich, REVIEW-GUIDE.md
Tests/CI Excellent Coverage, crash tests, sanitizers, valgrind, 32-bit
Portability Acceptable clang-shaped by design; cross-compiles to Windows; MSVC/macOS packaging out of scope
Integration (consumer) Good find_package works; namespaced target, deps resolved (verified)
Integration (producer) Good Plain cmake build/install works; no mulle-sde needed
ABI stability Poor Struct layout changed in 9.0.0; source-only in practice

Bottom line: a well-built, honest implementation of a deliberately narrow idea. The craft is better than the market fit — it solves the author's problem very well, and does not try to solve anyone else's.

last updated: