Scaffold and build Python C extensions with CMake and just-buildit.
Project description
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file just_makeit-0.10.9-py3-none-any.whl.
File metadata
- Download URL: just_makeit-0.10.9-py3-none-any.whl
- Upload date:
- Size: 247.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22bda61b0811da199fe4cb6dc018db29d5c1f55389b935fcf5cb5f0004ddc8d6
|
|
| MD5 |
820f28ae14fb964d387a07dc377754da
|
|
| BLAKE2b-256 |
b1cec28c0e175ee6205b2d464bc92319bdff36e0b17f4437202b3c85eff85aef
|
Provenance
The following attestation bundles were made for just_makeit-0.10.9-py3-none-any.whl:
Publisher:
release.yml on just-buildit/just-makeit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
just_makeit-0.10.9-py3-none-any.whl -
Subject digest:
22bda61b0811da199fe4cb6dc018db29d5c1f55389b935fcf5cb5f0004ddc8d6 - Sigstore transparency entry: 1527949511
- Sigstore integration time:
-
Permalink:
just-buildit/just-makeit@afe1b4bd759a9146c0801c3f312d3e6fa3be834a -
Branch / Tag:
refs/tags/v0.10.9 - Owner: https://github.com/just-buildit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@afe1b4bd759a9146c0801c3f312d3e6fa3be834a -
Trigger Event:
push
-
Statement type: