M*LIB: generic type-safe containers for C, in headers only
M*LIB is a library of generic and type safe containers / data structures in pure C language (C99 / C11) for a wide collection of container (comparable to the C++ STL).
At a glance
- What is it?
- M*LIB is a pure C library of generic and type-safe containers comparable to the C++ Standard Library, implemented entirely in headers with no C file to compile. It is BSD-2-Clause licensed, the current release is V0.8.2 from 2026-07-10, and the last push was on 2026-07-22.
- Who is it for?
- M*LIB fits a C project that needs real containers, hash maps, trees, JSON and lock-free structures without moving to C++ or pulling in a runtime, and the headers-only distribution makes it cheap to vendor into an embedded build. It does not fit a codebase that is happy with the C++ Standard Library, since M*LIB is an equivalent rather than an improvement on it.
- Can I use it commercially?
- Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The C++ Standard Library, in headers, with no C file
M*LIB is a C library enabling the definition and use of generic and type-safe containers in C, and the library's own framing is that it is an equivalent of the C++ Standard Library providing vector, deque, forward_list, set, map, multiset, multimap, unordered_set, unordered_map, stack, queue, shared_ptr, string, variant and option to standard ISO C99 or C11.
The delivery mechanism is the part that sets it apart from every other container library for C. M*LIB is only composed of a set of headers. There is no C file, and the installation is quite simple: you put the header in the search path of your compiler and it works. There is no dependency except some other headers of M*LIB and the libc.
That has a practical meaning for C programmers specifically. Every other C library with a similar scope needs a build step, a configure run, a static library to link and a version to track. M*LIB does not, so it drops into a build system that already has a hundred vendored dependencies without asking anything of it.
The header listing at the top of the repository matches the container list exactly, one `m-` prefixed header per data structure: `m-array.h`, `m-list.h`, `m-i-list.h` for the intrusive list, `m-deque.h`, `m-queue.h`, `m-dict.h`, `m-rbtree.h`, `m-bptree.h`, `m-prioqueue.h`, `m-tree.h`, `m-tuple.h`, `m-variant.h`, `m-buffer.h`, `m-snapshot.h`, `m-shared-ptr.h`, `m-worker.h`, `m-string.h`, `m-bstring.h`, `m-bitset.h`, `m-algo.h`, `m-funcobj.h`, `m-serial-json.h` and `m-serial-bin.h`, with `m-core.h` for the core preprocessing and `m-generic.h` for the uniform interface. There is also `m-lib.pc.in`, a pkg-config template, and `m-lib.spec`, a packaging spec.
The author is Patrick Pelissier, and the project is BSD-2-Clause licensed.
How generics without macros keep the type checking
The technical heart of the library is how it achieves genericity in a language that has none, and the readme is explicit about the method.
The genericity is not done directly by macro, which usually prevents type safety, but indirectly by making them define inline functions with the proper prototypes. That enables the user's calls to have proper error and warning checks.
This is the crux, and it is worth spelling out why it matters. The usual approach to generic containers in C is a macro that takes a type and expands a struct with the type substituted, or a `void*` based container with casts at every boundary. The macro approach means the container's internals are not type checked against the type you asked for, and the `void*` approach means the type is checked only by convention. M*LIB's approach is to instantiate the container as an inline function with a concrete prototype, so when you push an `int` into a container of `char*` the compiler sees a mismatched argument and complains.
The consequence is the one the readme states plainly. Due to the unfortunate weak nature of the C language for pointers, type safe means that at least a warning is generated by the compiler in case of a wrong type passed as container arguments to the functions.
That is a weaker guarantee than C++ gives, and the project is honest about it rather than overselling. But it is a guarantee that the `void*` approach does not give at all, and it is why the library can claim to build fully recursive container objects, `container-of[...]` of a container-of type T, while keeping compile time type checking.
Two supporting rules make the safety claim work. As few casts as possible are used within the library, since casts are the evil of safety, and the library can be used with the greatest level of warnings by a C compiler without any aliasing warning. The second is the reason a header-only generic library can be safe at all: if the implementation cast freely, the compiler would not be able to see through it.
Ownership, and the three ways an object enters a container
The containers have exclusive ownership of the objects they encapsulate, and the readme describes three ways an object can be pushed in.
By creating a copy of the data, which is the default behaviour with `INIT_SET`.
By stealing as much from the given source, which is move semantics with `INIT_MOVE`.
By constructing the object directly in the container using its own constructor, which is the emplace methods with `EMPLACE`.
The encapsulated objects can have their own constructor, copy, move and destructor operators, or they can be a basic C type like `int`, and both are fully supported. That is the same object model the C++ Standard Library has, expressed in a language without member functions, and it is what makes the recursive container case work.
The `m-try.h` header and the exception handling section of the table of contents extend the same model. M*LIB supports the exception error model by providing its own implementation of the try and catch mechanism, and this mechanism is compatible with RAII programming, so an object that owns a resource releases it when the scope ends even if an error unwinds past it. For a C project that has to manage lifetimes manually, having a working RAII story in a header-only library removes an entire class of leak bugs.
The memory model is in the same place. By default M*LIB uses the `malloc`, `realloc` and `free` functions to handle the memory pool, and that behaviour can be customized, fully supporting an arena allocator. Its default policy is to abort the program if there is a memory error, and that behaviour can also be customized globally, in the memory context customization section of the documentation. An aborting allocator is a defensible default for a library that is trying to be safe, and a team that wants to handle allocation failure gracefully has a documented switch rather than a fork.
Debug mode, sanitizers and a differential test against the STL
The safety story is not only about types, and the readme lists five means of ensuring it, of which two are worth reading in full.
In debug mode, defensive programming is extensively used and the contracts of the function are checked, ensuring that the data are not corrupted. Strict buffer overflows are checked in this mode through bound checking or the intrinsic properties of a Red-Black tree, for example, and the buffer overflow checks can still be kept in release mode if needed.
The testing claim is the more interesting one. The library is tested on the main targets using Continuous Integration with a coverage of the test suite of more than 99 percent. The test suite itself is run through the multiple sanitizers defined by GCC and CLANG, namely Address, undefined, leak and thread. And the test suite also includes a comparison of equivalent behaviours of M*LIB with the C++ STL using random testing or fuzzer testing.
That last item is the one that distinguishes this project. A differential test against the C++ Standard Library, using random inputs and comparing behaviour, catches the class of bug that unit tests miss: a container that returns a different ordering, a sort that is not stable when it should be, a find that returns the wrong element on a duplicate key. Writing a reference implementation in another language and fuzzing against it is a stronger correctness argument than any amount of hand-written test cases, and it is the reason a C library with this much surface area can claim confidence.
The static analysis story is equally thorough. Multiple static analysers, like scan-build, the GCC fanalyzer or CodeQL, are run on the generated code and the results analysed.
And there is a design rule that explains a lot of the library's behaviour: do not rewrite the C library and just wrap around it, for example do not rewrite sort but stable sort, and do not make users pay the cost of what they do not need. That second rule is what produces the header-only, per-feature design, where an application only pays for the containers it actually uses.
What is in the box, and how the build packages it
The table of contents is long and the top-level listing matches it, so the library's scope is easy to state precisely.
The sequence containers are array, list, the intrusive list, the double end queue and the fixed size queue and stack. The associative containers are the dictionary, the Red/Black tree and the B+ tree. The miscellaneous ones are the priority queue, the generic tree, the tuple and the variant. The thread containers are the shared fixed size queue, the atomic shared register, the shared pointers and the worker threads. The dataset section is string, byte string and bitset. Then there are the generic algorithms and the function objects, the JSON and binary serialisation, the uniform interface, the core preprocessing, and the C11 compatibility headers for thread and atomic.
The Makefile shows how it is packaged. The package name is `m-lib-$(VERSION)` with the version set to 0.8.2, and the header list is spelled out explicitly in the Makefile, so the distribution tarball contains exactly the headers and nothing else. The install prefix is `/usr/local` with a `DESTDIR` for packaging, and there are separate install modes for the program and the data files at mode 644, with a separate switch for whether the documentation is installed.
The `doc/` directory holds the API documentation and the `example/` directory has a Makefile plus a large set of numbered examples, `ex-array00.c` through `ex-array04.c`, `ex-bptree01.c` through `ex-bptree04.c`, `ex-buffer01.c` through `ex-buffer03.c`, `ex-alloc1.c` through `ex-alloc3.c`, `ex-algo02.c` through `ex-algo04.c`, `ex-astar.c` for a pathfinding example, `ex-curl.c` and more. The numbering by container is a documentation choice that makes the examples easy to find, and the `tests/` directory is the test suite that the sanitizers and the coverage measurement run against.
The version history is worth noting for what it says about the project's pace. The release tags are V0.8.2, described as The independent release, V0.8.0, The second break release, and V0.7.4, The Longest May Release, with the last push on 2026-07-22.
Where M*LIB is the wrong choice
The first case is a project that could simply use C++. The readme describes M*LIB as an equivalent of the C++ Standard Library, and there is no strict mapping, because both the STL and M*LIB have their exclusive containers. If your build environment has a C++ compiler, the STL gives you the same containers with real type safety, real exception handling and a standard library, and the argument for M*LIB reduces to a preference for C syntax or a constraint on the toolchain.
The second case is a project that cannot accept a 0.x API. The version line is 0.8.x and the release notes name breaking releases explicitly, the second break release being V0.8.0. A library that is not yet at 1.0 will keep changing, and for a foundation layer under an application that is fine to accept, and for a foundation layer you need to maintain for years it is a recurring cost.
The third case is a project that wants a small, specific dependency. M*LIB is a large library of containers, and the library's own rule is that you should not pay the cost of what you do not need, which is true at the level of which containers you use but not at the level of which headers you vendor. The distribution is a single tarball of all the headers, and a project that needs one array and one hash map is pulling in the whole set.
The fourth is a codebase with a strict dependency policy that forbids header-only C libraries with a large surface. There is no C file, which means no compiled artifact to audit, and the code is inlined into every translation unit that uses it, which changes the size of your binary and the compile time of your build in a way a normal dependency does not.
One last note on the type safety claim, because it is the one most likely to be over-read. The guarantee is a compiler warning, not a compile error, and warnings can be suppressed, and the library is explicit that the guarantee weakens where the C language's weak pointer typing forces it. A team that has decided to compile with warnings as errors, which is a good idea anyway, gets close to the C++ guarantee. A team that does not, gets much less than the marketing phrase suggests.
Editorial conclusion
M*LIB fits a C project that needs real containers, hash maps, trees, JSON and lock-free structures without moving to C++ or pulling in a runtime, and the headers-only distribution makes it cheap to vendor into an embedded build. It does not fit a codebase that is happy with the C++ Standard Library, since M*LIB is an equivalent rather than an improvement on it. Before you adopt it, read the STL-to-M*LIB mapping in the project wiki, because the readme states there is no strict mapping between the two container sets, and note that the current release line is 0.8.x, so the API is not yet frozen.
Frequently asked questions
How is M*LIB installed?
It is only a set of headers with no C file, so the installation is putting the headers in your compiler's search path. The Makefile builds a distribution tarball named m-lib-$(VERSION) and installs to /usr/local by default, with a DESTDIR for packaging.
How does M*LIB achieve type-safe generics in C?
Not through macros, which usually prevent type safety, but by making them define inline functions with the proper prototypes, so user calls get proper error and warning checks. The guarantee is a compiler warning on a wrong type passed as a container argument, and the library uses as few casts as possible so the compiler can see through the implementation.
What containers does M*LIB provide?
Array, list, intrusive list, deque, fixed size queue and stack, dictionary, Red/Black tree, B+ tree, priority queue, generic tree, tuple, variant, shared fixed size queue, atomic shared register, shared pointers, worker threads, string, byte string, bitset, generic algorithms, function objects, and JSON and binary serialisation.
How is M*LIB tested?
On the main targets with continuous integration and test suite coverage above 99 percent, run through the GCC and CLANG sanitizers for Address, undefined, leak and thread, with a comparison of equivalent behaviours against the C++ STL using random or fuzzer testing, and with several static analysers including scan-build, the GCC fanalyzer and CodeQL.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/p-p-h-d-mlib)Was this useful?