Workflow
Three common starting points are covered below. Reference sections follow.
Scenario 1 — Simple standalone extension
A single C object exposed as a Python extension. Good starting point for wrapping an algorithm, DSP primitive, or performance-critical inner loop.
1. Scaffold
just-makeit new my_dsp \
--object gain \
--arg-type float \
--return-type float \
--state gain:float:1.0
cd my_dsp
--arg-type and --return-type set the C types for step()'s input and
output. Omit both and they default to float _Complex.
2. Implement
Open native/inc/gain/gain_core.h and fill in the gain_step stub:
static inline float
gain_step(const gain_state_t *state, float x)
{
return state->gain * x;
}
gain_steps() — the block processor — is already in gain_core.c and loops
over this automatically. You do not edit the Python binding (gain_ext.c).
3. Build and test
make # cmake configure + build (Release)
make test # CTest (C lifecycle) + pytest (Python API)
The .so lands in src/my_dsp/ so import my_dsp works immediately from
the project root — no install step needed during development.
4. Use from Python
import sys
sys.path.insert(0, "src") # not needed after pip install
import numpy as np
from my_dsp import Gain
g = Gain(gain=2.0)
# single sample
y = g.step(1.0) # → 2.0
# block
x = np.ones(1024, dtype=np.float32)
y = g.steps(x) # → float32 ndarray, all 2.0
# getters / setters
g.set_gain(0.5)
g.get_gain() # → 0.5
# reset to declared defaults
g.reset()
# context manager
with Gain(gain=2.0) as g:
y = g.steps(x)
5. Install
pip install . # build wheel + install
pip install -e . # editable install (Python-only edits take effect immediately)
6. Optional: performance annotations
Once the algorithm is working and tested:
just-makeit perf
make
Patches step() with JM_FORCEINLINE JM_HOT, writes jm_perf.h and
jm_simd.h, and records the setting so future object and add calls
inherit it. See Performance annotations for the full reference.
Scenario 2 — Python package with multiple extensions
Multiple C objects in one project, all accessible from a single Python package.
1. Scaffold the first object
just-makeit new dsp_toolkit \
--object gain \
--arg-type float \
--return-type float \
--state gain:float:1.0
cd dsp_toolkit && make
2. Add a second object
just-makeit object ema \
--arg-type float \
--return-type float \
--state alpha:double:0.1 \
--state prev:float:0.0
object writes all C and Python files for the new standalone object and updates:
- root
CMakeLists.txt—add_subdirectory+target_sources($<TARGET_OBJECTS:…>) - umbrella header
native/inc/dsp_toolkit.h—#include "ema/ema_core.h" src/dsp_toolkit/__init__.py— splices infrom .ema import Emaand adds"Ema"to__all__, preserving any existing user edits
After adding ema, __init__.py looks like:
"""dsp_toolkit — Gain."""
from .gain import Gain
from .ema import Ema
__all__ = ["Gain", "Ema"]
No manual edits required.
3. Implement both objects
gain_step (read-only state):
static inline float
gain_step(const gain_state_t *state, float x)
{
return state->gain * x;
}
ema_step (writes back to state — drop const):
static inline float
ema_step(ema_state_t *state, float x)
{
float y = (float)state->alpha * x
+ (float)(1.0 - state->alpha) * state->prev;
state->prev = y;
return y;
}
4. Build and test
make && make test
CTest runs test_gain_core and test_ema_core. pytest runs the full
generated suite for both objects.
5. Use from Python
import sys
sys.path.insert(0, "src")
import numpy as np
from dsp_toolkit import Gain, Ema
signal = np.ones(20, dtype=np.float32)
gain = Gain(gain=2.0)
ema = Ema(alpha=0.3)
for x in signal:
y = ema.step(gain.step(x))
6. Add more objects
just-makeit object dc_block --state r:double:0.995
Each object repeats the same pattern: new C files, updated CMake, updated
__init__.py. make picks up the new object automatically.
7. Install
pip install .
The wheel bundles all compiled DSOs (gain.cpython-*.so, ema.cpython-*.so,
…) alongside the Python package.
Scenario 3 — Grouped types in a single subpackage module
Use this when you want multiple related Python types to share one .so and
import from a common subpackage path (from my_filters.filter import Fir, Biquad).
Each type still has its own independent C library; the module is the Python grouping unit only.
1. Scaffold the project and module together
just-makeit new my_filters --module filter
cd my_filters
--module is repeatable — --module osc --module env scaffolds two modules
in one command. Each module is an empty slot; types are added with
just-makeit object.
Alternatively, scaffold the project first and add the module separately:
just-makeit new my_filters
cd my_filters
just-makeit module filter
3. Add types
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 "b2:double:0.0" \
--state "a1:double:0.0" \
--state "a2:double:0.0" \
--state "w1:double:0.0" \
--state "w2:double:0.0"
Each just-makeit object call:
- Creates the C library (_core.h, _core.c, C test, C benchmark)
- Fully regenerates the module's filter_ext.c, CMakeLists.txt, and __init__.py
After both objects:
# src/my_filters/filter/__init__.py — generated
from .filter import Fir, Biquad
__all__ = ["Fir", "Biquad"]
Types within a module may have different --arg-type/--return-type. Here
Fir processes float complex and Biquad processes float.
4. Implement
Edit native/inc/fir/fir_core.h and native/inc/biquad/biquad_core.h to
fill in the _step stubs, exactly as in Scenarios 1 and 2.
5. Build and test
make && make test
CMake builds one .so (filter.cpython-*.so) inside
src/my_filters/filter/, linking both fir_core and biquad_core OBJECT
libraries. CTest runs test_fir_core and test_biquad_core.
6. Use from Python
import sys
sys.path.insert(0, "src")
import numpy as np
from my_filters.filter import Fir, Biquad
fir = Fir(gain=1.0)
bq = Biquad(b0=1.0)
Both types are fully independent — separate create/destroy lifecycles,
each with its own step, steps, reset, and context manager support.
7. Add a third type later
just-makeit object iir --module filter --state "gain:float:1.0"
filter_ext.c, CMakeLists.txt, and __init__.py are all regenerated from
the complete object list. Fir and Biquad are unaffected.
8. Install
pip install .
The wheel contains one .so for the filter subpackage rather than one .so
per type.
Standalone object vs module object — when to use which
just-makeit object (no --module) |
just-makeit module + just-makeit object --module |
|---|---|
Each type gets its own .so |
All types share one .so subpackage |
from my_pkg import Gain, Ema |
from my_pkg.filter import Fir, Biquad |
| Good for unrelated algorithms | Good for a cohesive type family |
Simpler; each type is independent at the .so level |
One import namespace for the group |
Both workflows produce a lib<project>.so C library that supports
cmake --install, pkg-config, and CMake find_package.
Project layout (full)
After scaffolding with one object and running just-makeit perf:
my_dsp/
├── CMakeLists.txt
├── Makefile
├── just-makeit.toml
├── pyproject.toml
├── cmake/
│ └── my-dsp.pc.in # pkg-config template
├── native/
│ ├── benchmarks/
│ │ └── bench_gain_core.c # C benchmark
│ ├── inc/
│ │ ├── clib_common.h # common C99 types
│ │ ├── pyex_common.h # Python extension includes
│ │ ├── my_dsp.h # umbrella header
│ │ ├── jm_perf.h # JM_FORCEINLINE / JM_HOT / JM_UNROLL …
│ │ ├── jm_simd.h # width-portable SIMD macros
│ │ └── gain/
│ │ └── gain_core.h # object API ← implement step() here
│ ├── src/
│ │ └── gain/
│ │ ├── CMakeLists.txt
│ │ ├── gain_core.c # steps() loop + any multi-sample logic
│ │ └── gain_ext.c # Python binding ← do not edit
│ └── tests/
│ └── test_gain_core.c # CTest lifecycle test
└── src/
└── my_dsp/
├── __init__.py
├── gain.pyi # type stub
├── benchmarks/
│ ├── __init__.py
│ └── bench_gain.py # pytest-benchmark suite
└── tests/
├── __init__.py
└── test_gain.py # pytest
Extending an object's state
just-makeit add --object gain --state drive:double:1.0
Regenerates the six state-sensitive files for gain from the updated state
list. just-makeit.toml is updated only after the files are written
successfully. When the project has a single standalone object, --object may be
omitted.
Benchmarking
make bench # C benchmark (timing loop) + Python pytest-benchmark
make bench-save # save baseline tagged with current git describe
make bench-compare # compare against last saved baseline
The C benchmark in native/benchmarks/bench_gain_core.c runs a raw timing
loop with no pytest overhead — useful for measuring SIMD uplift.
C library distribution
Every just-makeit project is also a first-class C library.
flowchart TD
SRC["**your C source**\ngain_core.c · ema_core.c · …"]
SRC --> CLIB["**libmy_dsp.so**\ncombined shared library"]
SRC --> PY["**Python package**\ngain.cpython-*.so\nema.cpython-*.so"]
CLIB --> C["**C / C++ / Rust / …**\npkg-config · find_package"]
PY --> PYUSER["**Python**\npip install .\nfrom my_dsp import Gain, Ema"]
Each object's core logic compiles once (CMake OBJECT library) and links into both artifacts.
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local
cmake --install build
gcc $(pkg-config --cflags my-dsp) main.c $(pkg-config --libs my-dsp) -lm -o main
Linux linker note:
--cflagsand--libsmust be split with the source file between them. GNU ld uses--as-neededby default on Debian/Ubuntu, which silently drops any shared library that appears before the object files that reference it. Putting-lmy_dspaftermain.censures the linker sees the undefined symbols first.
find_package(my_dsp REQUIRED)
target_link_libraries(my_app PRIVATE my_dsp::my_dsp_lib m)
See Installing your C library for end users for the full guide: prerequisites, custom prefixes, rpath, and verification.
Configuration
just-makeit config # show project + object registry
just-makeit config version 0.2.0 # update version
just-makeit.toml is the source of truth for all scaffolded state.
See the Roadmap for the full plan.