Index
Python C extensions the easy way.
just-makeit new generates a complete, working C99 extension project in one
command: core C library, thin Python binding, CMake build system, and full test
coverage — all passing before you write a single line of code.
Try it now — no tools required
# create and source venv — just-makeit is on your PATH automatically
. <(curl -fsSL https://just-buildit.github.io/just-makeit/install.sh)
# single command = complete project — ready to customize
just-makeit new my_project --object engine --state gain:double:1.0
# build and test — ALL GREEN!
cd my_project && make && make test
Installation
pip install just-makeit
just-makeit install-deps # cmake + C compiler + numpy, cross-platform
just-makeit install-deps detects your platform and installs system
dependencies (cmake, a C compiler) via the available package manager, then
creates a Python venv with numpy and just-makeit ready to use:
| Platform | Detection order |
|---|---|
| Linux | apt · dnf · pacman · zypper · apk |
| macOS | Homebrew |
| Windows | MSYS2 · winget · choco · scoop · direct download fallback |
Pass a path to use a custom venv location (default: /tmp/jm-venv on
Linux/macOS, %LOCALAPPDATA%\jm-venv on Windows):
just-makeit install-deps ~/my-venv
Quickstart
Standalone object — each type gets its own .so:
pip install just-makeit && just-makeit install-deps
just-makeit new my_project --object engine --state gain:double:1.0
cd my_project && make && make test
What you get:
my_project/
├── native/
│ ├── benchmarks/
│ │ └── bench_engine_core.c # C-level benchmark
│ ├── inc/
│ │ ├── clib_common.h # common C99 types
│ │ ├── pyex_common.h # Python extension includes
│ │ ├── my_project.h # umbrella header
│ │ └── engine/
│ │ └── engine_core.h # public C API + inline step()
│ ├── src/
│ │ ├── my_project_lib.c # combined C library stub (version symbol)
│ │ └── engine/
│ │ ├── CMakeLists.txt
│ │ ├── engine_core.c # block processor + lifecycle
│ │ └── engine_ext.c # thin Python binding
│ └── tests/
│ └── test_engine_core.c # CTest
├── cmake/
│ └── my-project.pc.in # pkg-config template
├── src/
│ └── my_project/ # Python package — import my_project
│ ├── __init__.py
│ ├── engine.pyi # type stub
│ ├── benchmarks/
│ │ ├── __init__.py
│ │ └── bench_engine.py # Python benchmark
│ └── tests/
│ ├── __init__.py
│ └── test_engine.py # pytest / unittest
├── CMakeLists.txt
├── Makefile
├── pyproject.toml
├── compile_commands.json
└── just-makeit.toml
Module subpackage — multiple types share one .so:
just-makeit new my_filters --module filter
cd my_filters
just-makeit object fir --module filter \
--state "coeffs:float[16]" --state "delay:float _Complex[16]" --state "gain:float:1.0"
just-makeit object biquad --module filter \
--arg-type float --return-type float \
--state "b0:double:1.0" --state "b1:double:0.0" --state "a1:double:0.0"
make && make test
from my_filters.filter import Fir, Biquad # one .so, one import
What you get (Python package layer):
src/
└── my_filters/
├── __init__.py
└── filter/
├── __init__.py # from .filter import Fir, Biquad
└── filter.pyi # type stub for filter.so
One .pyi per .so, named to match the compiled extension.
Commands
just-makeit provides a CLI with several commands run with
just-makeit COMMAND
| Command | Option | Description |
|---|---|---|
new <project> |
--module name |
Scaffold project + empty module subpackage; repeatable |
--object name |
Scaffold project + standalone object; repeatable | |
--state name:type[:default] |
Declare a state variable (struct field, constructor arg, getter/setter, reset) | |
--arg-type T |
C type for step() input; default float _Complex; void for generators; append [] for buffer-primary objects |
|
--return-type T |
C type for step() return; default same as --arg-type; void for sinks |
|
--perf |
Generate jm_perf.h and apply JM_FORCEINLINE JM_HOT to step() |
|
--basic |
Plain Makefile instead of CMake |
|
module <name> |
Scaffold an empty extension module (subpackage .so); add types with object |
|
object <name> |
--module name |
Target module subpackage; omit for a standalone object with its own .so |
--state name:type[:default] |
Same as new |
|
--arg-type T |
Same as new |
|
--return-type T |
Same as new |
|
--perf |
Same as new |
|
--mutable |
Remove const from the state pointer in step(); use for mutating generators |
|
--no-state |
Suppress auto-generated state, constructor args, and getter/setter scaffolding | |
--no-step |
Suppress step() and steps(); use for method-only objects |
|
--init-param name:type[:default] |
Constructor param for --no-state objects; repeatable |
|
--impl file::funcname |
Lift step() body from funcname in file |
|
--replace old::new |
String substitution on --impl body; repeatable |
|
add |
--state name:type[:default] |
Add a state variable to an existing object; repeatable |
--param name:type[:default] |
Add a constructor parameter to an existing object; repeatable | |
--object name |
Target object when the project has more than one | |
method <name> |
--param name:type |
Named scalar parameter; repeatable |
--param name:type[] |
Named numpy array parameter; C receives (const elem_t *name, size_t name_len) |
|
--arg-type T |
Single array-style input (mutually exclusive with --param) |
|
--return-type T |
C return type; void for no return |
|
--variable-output |
Pre-allocate output buffer at init; return zero-copy numpy view each call | |
--multi-output T |
Add a parallel output array of type T; repeatable | |
--out-type TYPE |
Allocate output array per call; C stub receives *out; length = in_len / out_divisor |
|
--out-divisor N |
Divide input length by N for output array length (default 1); use 2 for CI8/CI16 inputs |
|
--impl file::funcname |
Lift method body from funcname in file |
|
--replace old::new |
String substitution on --impl body; repeatable |
|
property <name> |
--type T |
C type of the property value |
--writable |
Also generate a setter; omit for read-only | |
--field |
Add T name; to the state struct and auto-implement the getter |
|
function <name> |
--module mod |
Target module (required) |
--param name:type |
Named scalar parameter; repeatable | |
--param name:type[] |
Named numpy array parameter | |
--return-type T |
C return type; default void |
|
--doc "text" |
Python docstring for the function | |
--impl file::funcname |
Lift function body from funcname in file |
|
--replace old::new |
String substitution on --impl body; repeatable |
|
script |
Print a shell script that fully reconstructs the project from just-makeit.toml |
|
perf |
Upgrade existing project with jm_perf.h performance annotations |
|
config |
[key value] |
Print config; or set key to value in just-makeit.toml |
build |
[dir] |
Configure + build C extensions and package a wheel into dir (default dist/) |
test |
Build (if needed), then run CTest + pytest | |
dry-run |
Show what would be compiled without running any build steps | |
install-deps |
[path] |
Install cmake + C compiler via system package manager; create venv at path |
example |
[name] |
Run a bundled end-to-end example; omit name to list available examples |
See State Variable Types for supported types, defaults, and C/Python mappings.
C conventions
Generated code follows a consistent lifecycle pattern:
// Constructor — parameters match your --state declarations
engine_state_t *engine_create(double gain);
// Destructor
void engine_destroy(engine_state_t *state);
// Reset — restores every variable to its declared default
void engine_reset(engine_state_t *state);
// Single sample (inlined, pass-through stub — implement your algorithm here)
static inline float complex
engine_step(const engine_state_t *state, float complex x);
// Block processor
void engine_steps(
engine_state_t *state,
const float complex *input,
float complex *output,
size_t n);
// Generator / source object (--arg-type void): no input parameter
static inline float
nco_step(const nco_state_t *state);
void nco_steps(nco_state_t *state, float *output, size_t n);
// Getter / setter for each --state variable
double engine_get_gain(const engine_state_t *state);
void engine_set_gain(engine_state_t *state, double gain);
Python API
Standalone object (just-makeit object):
from my_project import Engine
import numpy as np
obj = Engine(gain=1.0) # explicit
obj = Engine() # uses declared defaults
# single sample
y: complex = obj.step(1.0 + 0.5j)
# block processing
x = np.ones(1024, dtype=np.complex64)
y = obj.steps(x) # allocates and returns complex64 ndarray
obj.steps(x, out=y) # zero-copy: writes into y, returns y
# getters / setters
obj.get_gain()
obj.set_gain(2.0)
# reset restores declared defaults
obj.reset()
# context manager
with Engine() as e:
y = e.steps(x)
Module subpackage (just-makeit module + just-makeit object):
from my_filters.filter import Fir, Biquad # one .so, clean subpackage import
fir = Fir(gain=1.0)
bq = Biquad(b0=1.0)
Types within a module are fully independent — separate lifecycles, each with
its own step, steps, reset, getters/setters, and context manager.
Multiple state variables
just-makeit new my_project \
--object engine \
--state center_freq:double:1000.0 \
--state bandwidth:double:200.0 \
--state order:int:4
Each --state name:type:default becomes a struct field, a constructor parameter
(optional in Python, required in C), getter/setter pair, and reset target — in
both C and Python.
Integrations
- CMake —
Python3_add_librarywithWITH_SOABI;.solands insrc/for zero-install dev workflow - GNU Make — convenience wrapper with
build,test, andjust-buildtargets - NumPy buffer protocol —
steps()accepts and returns typed ndarrays matching your declared state types - pytest — tests generated covering create, step, steps, getters/setters, reset, context manager, and destroy
- CTest — C-level test for the core lifecycle
- just-buildit — PEP 517 backend;
pip install .andpip install -e .work out of the box
Packaging
The generated project uses just-buildit as its PEP 517 build backend.
# Build and install
pip install .
# Development install (no rebuild needed after editing Python files)
pip install -e .
# Build a wheel manually
just-makeit build
Examples
The examples/ directory contains step-by-step walkthroughs:
running_stats/— Welford's online mean & variance (introductory walkthrough)fir_filter/— 16-tap FIR filter processing complex I/Q signals, with perf annotationssliding_correlator/— sliding window cross-correlation against a fixed reference sequencesliding_power/— sliding window instantaneous signal power estimatorarray_processing/— all five array-processing patterns: autosteps(), methods,--variable-output, multi-output,--arg-type type[]dsp_toolkit/— two-object library (Gain + Ema); demonstrates multi-object workflow and__init__.pyauto-splicefilter_module/—Fir+Biquadin a singlefiltersubpackage.sousingmodule+objectiqfile/— cf32 ↔ q15 IQ file converter;--fieldproperties, generator object,pip install -e ., wheel buildstream_chunker/— stream re-framer with--no-stepand--variable-output; variable-size input → fixed-size output chunks
Design principles
Separation of concerns. Core C logic goes in *_core.c / *_core.h.
The Python extension in *_ext.c is a thin adapter — argument parsing, array
wrapping, and nothing more. This keeps the C library independently testable
and usable from Rust, C++, or any other language.
Full test coverage by default. Every generated project has C tests (CTest) and Python tests (pytest) from day one.
just-buildit for packaging. The generated pyproject.toml uses
just-buildit as the PEP 517
build backend, so pip install . just works.
Requirements
- Python 3.11+
- CMake ≥ 3.16
- A C99 compiler (GCC, Clang, MSVC/MinGW)
- NumPy (runtime, for generated projects)
Authors
Matthew T. Hunter, Ph.D. and Claude Code