Skip to content

Latest commit

 

History

327 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 (typeof tricks, 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.chained_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 readings

This 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 sources

4. 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/readings

Output:

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.py rendered readings_auto.h / readings_auto.c — plain C, no runtime — plus readings.cmake and readings.state (digests of what was generated).
  • add_autoc_module() in CMakeLists.txt registered the generator as a proper build step: it re-runs readings.py whenever the definitions change, so generated code never goes stale.
  • readings-auto is an object library; your executable just links it and includes the header.
  • The workflow from here: add more types to the module (a Map for calibration constants, an intrusive hash set for deduplication, a Record for 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 Vector fixed-capacity stack-allocated sequence, direct-access range, zero heap allocations
autoc.circular_buffer Static, Dynamic fixed-capacity stack or dynamic heap circular ring buffer
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.mapping Mapping abstract hash-map interface (shared base)
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.rb_set Set red-black tree — ordered set, guaranteed O(log n) height, <= 3 rotations on delete
autoc.rb_map Map ordered map over the red-black tree set
autoc.avl_set Set AVL tree — ordered set, strictly balanced height <= 1.44 log2(n), fastest lookups
autoc.avl_map Map ordered map over the AVL tree set
autoc.flat_set Set contiguous sorted dynamic array with binary search lookup, cache-friendly layout
autoc.flat_map Map ordered map over a contiguous sorted array of key-value pairs (AoS layout)
autoc.tree_map Map generic binary search tree map parameterized by tree set backend
autoc.tiered_vector Vector chunked append-optimized direct-access buffer — amortized O(1) push, stable addresses, O(chunks) teardown
autoc.priority_queue Queue 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.bit_set Set 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 sources

The 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages