autoc
C source code generation from Python.
Describe your C types and containers — vectors, lists, strings, hash sets and maps, records, references — in a short Python script, and autoc emits plain, dependency-free C source code: headers, implementation files and the CMake glue to regenerate them whenever your definitions change.
/* Automagically generated by autoc 3.0 */
Why
C has no standard containers, and the usual workarounds each hurt in a familiar way:
-
Macro-based generics (
typeoftricks, token pasting) are opaque, hard to debug and explode into incomprehensible errors. -
void*-based generic libraries abandon type safety and hide element semantics (who copies? who destroys?). - Hand-writing one container per type works, but is tedious, error-prone and never quite finished.
autoc takes the fourth road: the container is written once, generically, against a strict value-semantics protocol — and a generator stamps out a specialized, fully typed C implementation for each (container, element type) pair you use. You get the code you would have written by hand (arguably better), with none of the macro or void* compromises.
Generated code is ordinary C targeting ANSI C. It has no runtime library, nothing to link, no macros to consume — you #include a header and compile the sources. Headers are C++-safe (extern "C" guarded). The project validates its own output by compiling and running the generated test suites under multiple compilers (MSVC and Pelles C at present).
Quick start
Requires Python ≥ 3.13. autoc itself has zero dependencies.
1. Define a module — e.g. mymodule.py:
from autoc.module import Module
from autoc.vector import Vector
from autoc.hash_map import Map
import autoc.std as std
with Module("mymodule") as m:
m.add(Vector("int_vector", "int"))
m.add(Map("str2double", "const char*", "double"))2. Generate — run the script; autoc writes mymodule_auto.h / mymodule_auto.c (splitting into several sources when a module grows too large for one translation unit).
3. Use — from C:
#include "mymodule_auto.h"
int_vector v;
int_vector_create(&v);
int_vector_set(&v, 0, 42);
assert(int_vector_get(&v, 0) == 42);
/* iterate */
int_vector_range r;
for(r = int_vector_range_new(&v); !int_vector_range_empty(&r);
int_vector_range_move_front(&r)) {
printf("%d\n", int_vector_range_front(&r));
}
int_vector_destroy(&v);Every generated type speaks the same vocabulary of value semantics:
void T_create (T* target); /* default construction */
void T_destroy(T* target); /* release resources */
void T_copy (T* target, const T* src);
int T_equal (const T* left, const T* right);
int T_compare(const T* left, const T* right); /* when orderable */
size_t T_hash (const T* target); /* when hashable */plus the container-specific operations (set/get/view/contains for sequences and sets, put/remove for sets, get/set/view for maps, empty/size, forward ranges for iteration, …).
Tutorial: a complete project, end to end
Let's build a small but real program — a sensor log that accumulates readings into a list of ints, aggregates them and drains them — from an empty directory to a running binary.
1. Install the generator
From a checkout of this repository (Python ≥ 3.13 required):
pip install .2. Scaffold the project
mkdir readings && cd readings
python -m autoc.project readingsThis is the layout you get — a working CMake project with the generator already wired in:
readings/
├── CMakeLists.txt # build + regeneration rules
├── CMakePresets.json # debug/release presets
├── cmake/
│ └── AutoC.cmake # add_autoc_module() helper
├── readings.py # type definitions — the generator script
├── readings.c # your application
├── readings.code-workspace
├── .vscode/launch.json
└── .gitignore
The project name doubles as the module name: readings.py will generate readings_auto.h / readings_auto.c, and CMake will build them as the readings-auto library your executable links against.
3. Define the container
Replace readings.py with:
import autoc.core
import autoc.module
import autoc.cmake
from autoc.list import List
# CamelCase naming (IntListPushFront) is the default; switch to the
# snake_case style used throughout the test suite and this document:
autoc.core.decorator = autoc.core.snake_decorator
with autoc.module.Module("readings") as m:
m.add(List("int_list", "int"))
autoc.cmake.CMake(m) # emit the CMake fragment wiring the generated sources4. Use it from C
Replace readings.c with:
#include <stdio.h>
#include "readings_auto.h"
int main(void) {
int_list readings;
int_list_create(&readings);
/* readings arrive one at a time, newest first */
int samples[] = {23, 21, 25, 24, 27, 22, 26};
for(size_t i = 0; i < sizeof(samples) / sizeof(samples[0]); ++i) {
int_list_push_front(&readings, samples[i]);
}
/* aggregate via the range interface */
size_t count = int_list_size(&readings);
long sum = 0;
int max = 0;
for(int_list_range r = int_list_range_new(&readings);
!int_list_range_empty(&r);
int_list_range_move_front(&r)) {
int v = int_list_range_front(&r);
sum += v;
if(v > max) max = v;
}
printf("count=%zu sum=%ld avg=%.1f max=%d\n",
count, sum, (double)sum / (double)count, max);
/* membership test */
printf("seen 25: %s\n", int_list_contains(&readings, 25) ? "yes" : "no");
/* drain, consuming the stored values one by one */
while(!int_list_empty(&readings)) {
printf("consumed %d\n", int_list_pop_front(&readings));
}
int_list_destroy(&readings);
return 0;
}5. Build and run
cmake --preset debug
cmake --build --preset debug
./build/debug/readingsOutput:
count=7 sum=168 avg=24.0 max=27
seen 25: yes
consumed 26
consumed 22
consumed 27
consumed 24
consumed 25
consumed 21
consumed 23
(The values come back in reverse order of insertion — a singly-linked list is pushed and popped at the front.)
What just happened
- Running
readings.pyrenderedreadings_auto.h/readings_auto.c— plain C, no runtime — plusreadings.cmakeandreadings.state(digests of what was generated). -
add_autoc_module()inCMakeLists.txtregistered the generator as a proper build step: it re-runsreadings.pywhenever the definitions change, so generated code never goes stale. -
readings-autois an object library; your executable just links it and includes the header. - The workflow from here: add more types to the module (a
Mapfor calibration constants, an intrusive hash set for deduplication, aRecordfor readings with timestamps, …), rerun the build, and use the generated functions — nothing else in your C code changes.
Principles
Plain C, zero runtime. The deliverable is source code you can read, grep, step through in a debugger and ship. No library to link, no ABI to track, no preprocessor machinery to fight. Regenerating is cheap; the code is meant to be regenerated, not edited.
Type-safe by monomorphization. Each container is specialized to its element type at generation time. No void*, no casts, no element-size parameters — int_vector_set takes an int, str2double_set takes a const char* and a double. Passing conventions are chosen per element kind: primitives cross API boundaries by value, composites by pointer-to-const.
One protocol for all types. Primitives and composites are not two worlds. Every type — from int to a nested Map of records — carries the same operations (create/destroy/copy/equal/compare/hash), so containers compose freely: a List of Vectors of records just works. Primitives realize the protocol as inline expressions (target = source, left == right, a cast for hashing); composites realize it as generated functions. The difference never leaks to the call sites.
Capabilities are derived, dead code is elided. Types carry traits — constructible, destructible, copyable, comparable, orderable, hashable. Composites derive them structurally from their fields (a record is destructible if any field is; copyable only if all are), and the generator emits only what the traits permit: a Vector("int_vector", "int") contains no element-destruction code at all, and an unordered container never grows a comparison operator.
Custom types are first-class citizens. Your own composites join the system by implementing the same protocol in Python — then they work as element or field types everywhere, including inside intrusive containers. Your code is injected as C expressions/functions at well-defined hook points, wrapped so it cannot break operator precedence or naming conventions.
Intrusive variants trade value space for layout. The intrusive hash set/map store elements in one flat allocation of exactly the element type — no wrapper entries, no tag arrays — with slot states (empty/deleted) encoded as sentinel values you choose. If your element type can reserve two distinguishable states (e.g. INT_MIN/INT_MAX for an int, or low pointer values for handles), you get the most cache-friendly layout possible; if it cannot, use the regular non-intrusive containers.
Deterministic builds. Generation is integrated with CMake via add_autoc_module(): output digests are tracked, code is regenerated only when definitions actually change, and every generated artifact is declared as a proper build dependency.
Reference-counted memory as an option, not a mandate. Counted references provide automatic reference counting for shared objects — including when used as container elements — while Raw references give you plain unmanaged pointers. Both speak the same protocol as every other type.
Container catalog
| Module | Type | Notes |
|---|---|---|
autoc.vector |
Vector |
direct-access sequence, bidirectional range |
autoc.static_vector |
StaticVector |
fixed-capacity stack-allocated sequence, direct-access range, zero heap allocations |
autoc.list |
List |
singly-linked sequence, forward range |
autoc.deque |
Deque |
doubly-linked sequence, bidirectional range |
autoc.queue |
Queue |
FIFO adapter over Deque
|
autoc.stack |
Stack |
LIFO adapter over List, forward range |
autoc.string |
String |
string as an index→character map, direct-access range, variadic formatted output |
autoc.set |
Set |
abstract hash-set interface (shared base) |
autoc.map |
Map |
abstract hash-map interface (shared base) |
autoc.hash_map |
_Entry |
shared key→value entry record for map implementations |
autoc.chained_hash_set |
Set |
bucket-chaining hash set — no sentinel values, stable element references, safe default |
autoc.chained_hash_map |
Map |
bucket-chaining hash map over internal entry set |
autoc.treap_set |
Set |
treap — ordered set, O(log n) expected, iterates in sorted order |
autoc.treap_map |
Map |
ordered map over the treap set — iterates in key order, supports lexicographic comparison |
autoc.tiered_vector |
TieredVector |
chunked append-optimized direct-access buffer — amortized O(1) push, stable addresses, O(chunks) teardown |
autoc.priority_queue |
PriorityQueue |
binary heap — guaranteed O(log n) push/pop, top = greatest element, duplicate priorities allowed |
autoc.intrusive_hash_set |
Set |
flat, sentinel-based open-addressing hash set |
autoc.intrusive_hash_map |
Map |
flat, sentinel-based open-addressing hash map |
autoc.bitset |
BitSet |
fixed-size inline bit array, set algebra, popcount, zero heap allocation |
autoc.record |
Record |
user-defined field aggregates |
autoc.variant |
Variant |
tagged union / sum type over alternative types |
autoc.reference |
Raw, Counted
|
unmanaged / reference-counted handles |
autoc.range |
Input/Forward/Backward/Bidirectional/DirectAccess
|
iteration abstractions |
CMake integration
include(AutoC) # from the generated cmake/ directory
add_autoc_module(
mymodule
DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/mymodule.py
COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/mymodule.py mymodule
)
target_link_libraries(myapp mymodule-auto) # header + generated sourcesThe module bootstraps itself on first configure and re-runs the generator only when mymodule.py (or its stated dependencies) change. See test/CMakeLists.txt for a working example.
How it tests itself
The repository's test suite is written the way users write code: Python scripts in src/autoc/test/ define container instances and their unit tests, and autoc generates a C test module (test_auto.c, ~2,200 lines) that is compiled and executed — currently under both MSVC and Pelles C — as part of the build. The generator is only as good as the C it produces, so that is what gets tested.
License
BSD 2-Clause — see LICENSE.