Extend commands¶
These commands add behaviour to objects and modules that already exist in
just-makeit.toml. Run them from the project root after scaffolding.
just-makeit method <object> <method_name> [--module name] [--param name:type ...] --return-type TYPE [--variable-output] [--arg-type TYPE] [--multi-output TYPE ...]¶
Add a named execute method to an existing object.
jm method is additive and splice-free: it injects the method's
declaration into <obj>_core.h and appends a fresh C stub to <obj>_core.c,
then regenerates the glue (_ext.c, .pyi) with the new Python binding.
Existing bodies in _core.c are never re-rendered — only the new stub is
appended, ready for you to implement.
Arguments
| Argument | Description |
|---|---|
object |
Object name (must already exist in just-makeit.toml). |
method_name |
Snake-case name for the new method. |
--module name |
Module the object belongs to (required for module objects). |
--param name:type |
Named typed scalar parameter. Repeatable. |
--param name:type=default |
Optional scalar parameter (e.g. gain:double=1.0) — omit it for the default. Makes the method keyword-capable; optional params must follow required ones; plain scalars only (gh-240). |
--param name:type[] |
Named numpy array parameter. Repeatable. Generates const elem_t *name, size_t name_len in C. |
--return-type TYPE |
C type of the return value (void for no return). |
--arg-type TYPE |
C type of a single array-style input. Use void for count-only inputs. |
--variable-output |
Pre-allocate output buffer at init; return zero-copy numpy view. See below. |
--multi-output TYPE |
Add a second (or further) output array. Repeatable; produces a tuple return. |
--out-type TYPE |
Allocate a complex64 (or other) output array per call and pass *out to C. The C stub receives (... , elem_t *out) and the Python wrapper allocates and returns the ndarray automatically. The output length equals in_len / out_divisor. |
--out-divisor N |
Divide the input length by N to determine the output array length when --out-type is active (default: 1). Use 2 for methods that interpret the input as interleaved I/Q pairs (e.g. a CI8 buffer where each complex sample is 2 bytes). |
--batch |
Generate a 1:1-rate array transform. The C stub receives (state, const in_t *in, size_t n, out_t *out) (or (state, size_t n, out_t *out) for --arg-type void). The Python wrapper allocates an output array of length n per call and returns it. Use when output length equals input length and is unknown at init time. |
--varargs |
Generate a *args/**kwargs Python binding. See below. Mutually exclusive with --arg-type, --param, and --variable-output. |
--pass-capacity |
Emit the 5-arg (…, out, size_t max_out) C form for a --variable-output method (a bounds-checking C API receives the buffer capacity). |
--nogil |
Release the GIL across the pure-C kernel of a --variable-output method (numpy accessors hoisted out first), so a thread-per-shard worker scales across cores. Opt-in: sound only when the object is not shared across threads concurrently (one object per stream). See below. |
--result-field name:T |
Declare one field of a returned record struct (repeatable). The method returns a list of these record tuples; pair with --return-type <record_struct> (the record's C type) and --max-results N. |
--single |
With --result-field, return one named record (a PyStructSequence: attribute access + unpacking) instead of a list[tuple]. The C kernel returns the --return-type record struct by value (gh-244). |
--record-name NAME |
With --single, the public name of the record type (e.g. ToneMetrics), overriding the name derived from the C --return-type (gh-257). Also settable per method in the manifest as record_name = "…". |
--record-module MOD |
With --single, the __module__ of the record type (e.g. my_pkg.dsp), so type(r).__module__ / repr(r) matches the project's import path instead of the C component name. Also settable per method in the manifest as record_module = "…" (gh-261). |
--impl file::funcname |
Lift the method body from funcname in file instead of emitting a blank <<IMPLEMENT>> stub. |
--impl file::N:M |
Lift lines N..M (inclusive, 1-based) instead of a named function body. Out-of-bounds or inverted ranges error cleanly. |
--replace old::new |
String substitution applied to the body lifted by --impl. Repeatable. |
Named parameters (--param)¶
Use --param name:type when the method takes multiple distinct typed scalar
inputs. This generates named parameters in both the C stub and the Python
wrapper.
just-makeit method nco configure --module dsp \
--param freq:float \
--param phase:float \
--param mode:int32_t \
--return-type void
Generated C stub:
void
nco_configure(nco_state_t *state, float freq, float phase, int32_t mode)
{
(void)state; (void)freq; (void)phase; (void)mode;
}
Python call:
All scalar types in _CTYPE_META are supported as --param types (float,
double, int, int32_t, uint32_t, size_t, float _Complex, etc.).
Array parameters (--param name:type[]) generate a numpy array input.
The C stub receives (const elem_t *name, size_t name_len) and the Python
wrapper performs PyArray_FROM_OTF automatically:
just-makeit method resamp execute_ctrl --module resample \
--param ctrl:"float _Complex[]" \
--return-type size_t
Generated C stub:
size_t
resamp_execute_ctrl(resamp_state_t *state,
const float complex *ctrl, size_t ctrl_len)
{
(void)state; (void)ctrl; (void)ctrl_len;
return (size_t)0;
}
Python call: resamp.execute_ctrl(np.zeros(64, dtype=np.complex64))
--param and --arg-type are mutually exclusive per method.
Varargs methods (--varargs)¶
Use --varargs when a method needs fully flexible Python argument
parsing — for example, configure(rate=48000, mode="fast") where the
parameter set is open-ended or includes types that fall outside the
fixed _CTYPE_META table.
What gets generated:
-
native/src/<comp>/<comp>_<name>_core.c(sacred — never regenerated). A Python-aware C file compiled directly into the Python extension DSO (not the pure-C OBJECT library), so it may include<Python.h>freely. Contains aPyObject *function with an<<IMPLEMENT>>stub: -
<comp>_ext.c(regenerated). Anextern PyObject *declaration pulls the symbol in from the binding file, and thePyMethodDefentry usesMETH_VARARGS | METH_KEYWORDS: -
native/src/<comp>/CMakeLists.txt(surgically updated). The binding.cfile is spliced into thePython3_add_library(...)source list so cmake compiles it into the same DSO. -
.pyistub (regenerated): -
just-makeit.toml:varargs = trueis recorded under[[<comp>.methods]].
Accessing the C state inside the binding:
The self pointer is a <Comp>Object * (the Python object), not the raw
state struct. Cast it to reach the handle:
typedef struct { PyObject_HEAD; filter_state_t *handle; } Obj;
filter_state_t *state = ((Obj *)self)->handle;
The comment at the top of the generated sacred file shows this cast verbatim.
Constraint: --varargs is mutually exclusive with --arg-type,
--param, and --variable-output. Those flags all imply a specific
typed C signature; --varargs bypasses the type system entirely and
gives you raw Python argument access.
Output modes¶
Choosing the right output mode depends on whether the maximum output count is knowable at init time.
--batch — 1:1-rate array transform¶
Use --batch when output length equals input length and is unknown at init
time. The generated C stub receives (state, const in_t *in, size_t n, out_t *out) and the Python wrapper allocates and returns an output array of
length n per call.
just-makeit method nco steps_u32 --module source \
--arg-type void --return-type uint32_t --batch
just-makeit method nco steps_ctrl --module source \
--arg-type float --return-type float --batch
Generated C stubs:
void nco_steps_u32(nco_state_t *state, size_t n, uint32_t *out);
void nco_steps_ctrl(nco_state_t *state, const float *in, size_t n, float *out);
Python calls:
ph = nco.steps_u32(1024) # returns uint32 ndarray of length 1024
out = nco.steps_ctrl(ctrl) # ctrl is float32 ndarray; returns float32 ndarray
--variable-output — pre-allocated zero-copy batch¶
Use --variable-output when the maximum output count is bounded by the
object's state and knowable at init time (decimators, FIFOs with fixed
capacity). The generated code calls <method>_max_out(state) at init time,
pre-allocates a fixed output buffer, and returns a zero-copy numpy view
into that buffer on every call — no per-call malloc.
just-makeit method hbdecim execute --module resample \
--arg-type "float _Complex" --return-type "float _Complex" \
--variable-output
Generated C stubs:
/* Return maximum output samples possible given current state. */
size_t hbdecim_execute_max_out(hbdecim_state_t *state);
/* Process n_in samples; write up to _max_out results; return actual count. */
size_t hbdecim_execute(hbdecim_state_t *state,
const float complex *in, size_t n_in,
float complex *out);
Python call:
| Use case | _max_out at init |
Use --variable-output? |
|---|---|---|
Decimator, fixed ratio R, block size B |
ceil(B / R) |
Yes |
Buffer / FIFO with fixed capacity C |
C |
Yes |
| FIR filter, output ≤ input length | 0 (unknown) | Yes — lazy-alloc kicks in |
| NCO extended outputs, 1:1 rate | 0 (unknown) | Yes — lazy-alloc kicks in |
Lazy-alloc when _max_out returns 0: if {comp}_{method}_max_out() returns 0
at construction (e.g. a FIR whose tap count isn't known until _create runs), the
output buffer is left NULL — no malloc(0) hazard. On the first Python call
the wrapper re-queries max_out(); if it still returns 0 it falls back to the input
length n, then allocates. Every subsequent call takes the pre-allocated zero-copy
path. The only practical implication: make sure _max_out returns a valid bound by
the time the first call happens.
--multi-output¶
Each --multi-output TYPE adds a parallel output array, producing a tuple
return. Combine with --variable-output:
just-makeit method nco steps_u32_ovf --module resample \
--arg-type void --return-type uint32_t \
--variable-output --multi-output uint8_t
--nogil — release the GIL for thread-per-shard scaling¶
For a --variable-output execute method, --nogil wraps the pure-C kernel
call in Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS:
just-makeit method ddc execute --module ddc \
--param x:"float _Complex[]" --variable-output --pass-capacity --nogil
/* generated binding (abridged) */
const float complex *_ng0 = (const float complex *)PyArray_DATA(x_arr);
size_t _ng1 = (size_t)PyArray_SIZE(x_arr);
size_t n_out;
Py_BEGIN_ALLOW_THREADS
n_out = ddc_execute(self->handle, _ng0, _ng1, self->_execute_buf, cap);
Py_END_ALLOW_THREADS
The numpy accessors are hoisted into locals before the block so no Python C-API runs while the GIL is dropped; the buffer realloc and any error-raising stay above it, under the GIL. A worker that gives each thread its own object and output buffer then scales across cores instead of serialising on the GIL.
It is opt-in because releasing the GIL is sound only under that one-object-per-stream contract — jm cannot verify it, so you assert it by setting the flag. Generated, not hand-patched: the release is declarative and regenerates with the binding.
just-makeit property <object> <prop_name> [--module name] --type TYPE [--writable] [--field]¶
Add a read-only (or read-write) Python property to an existing object.
just-makeit property nco phase --module source --type uint32_t
just-makeit property nco phase_inc --module source --type uint32_t
just-makeit property buffer capacity --type size_t --writable
just-makeit property reader samples_read --module conv --type uint32_t --field
Like jm method, a computed property is additive and splice-free: it
injects a get_<prop>() declaration into <obj>_core.h and appends a fresh
stub to <obj>_core.c (plus set_<prop>() if --writable) for you to
implement, then regenerates the Python getter/setter glue in _ext.c. With
--field no stub is generated — it injects one TYPE prop_name; member
directly into the state struct and auto-implements the getter as
return state->prop_name. Existing _core.c bodies are never re-rendered.
Arguments
| Argument | Description |
|---|---|
object |
Object name (must already exist in just-makeit.toml). |
prop_name |
Snake-case property name. |
--module name |
Module the object belongs to (required for module objects). |
--type TYPE |
C type of the property value. |
--writable |
Also generate a setter. Without this flag the property is read-only. |
--field |
Add a TYPE prop_name; field to the state struct and auto-implement the getter as return state->prop_name. No <<IMPLEMENT>> stub is generated — the field is the implementation. Combine with --writable for a read-write struct field property. |
Removing a method or property¶
just-makeit remove <object> <method|property> regenerates the glue
(_ext.c, .pyi) without the entry, so the binding stops exposing it. It is
splice-free, so it leaves the orphaned _core.c body (and the
_core.h declaration, or the field-backed struct member) in place with a
"delete by hand" note — your code is never silently rewritten. Remove the
stub yourself once you're sure. (Removing state is structural and rebuilds
the object via the regenerate path instead.)
just-makeit function <name> --module <mod> [--param name:type ...] [--return-type TYPE] [--doc "text"]¶
Add a stateless C function to an existing module — no struct, no lifecycle, no persistent state.
Writes a C stub to the function's own sacred source file
native/src/<module>/<name>.c (never regenerated — your implementation is
safe) and injects the declaration into native/inc/<module>/<module>_core.h.
Each function thus owns one translation unit, which the module's CMakeLists
compiles into the module's OBJECT library. Then regenerates <module>_ext.c
to add a _bind_<name> Python wrapper and wire it into the PyMethodDef
array.
With --inline the function instead lives entirely as a static inline body
in <module>_core.h and gets no .c file.
The generated _bind_<name> wrapper is positional-or-keyword
(METH_VARARGS | METH_KEYWORDS): callers may pass arguments positionally or by
name (fn(input=x, n=8)). A no-parameter function stays METH_NOARGS. Keyword
capability is ~free unless keywords are actually used — see
Arguments: positional vs keyword.
Arguments
| Argument | Description |
|---|---|
name |
Snake-case function name. |
--module mod |
Module the function belongs to (required). |
--param name:type |
Named typed scalar parameter. Repeatable. |
--param name:type=default |
Optional scalar parameter — omitting it yields default (e.g. gain:double=1.0). Optional params must come after required ones; plain scalars only (gh-240). |
--param name:type[] |
Named numpy array parameter. Repeatable. Generates const elem_t *name, size_t name_len in C. |
--param name:path |
Filesystem path parameter. Python accepts str \| os.PathLike; C receives const char * via PyUnicode_FSConverter (gh-353). |
--param name:enum:<ename>[=d] |
String-choice parameter validated against the named [[enum]] SSOT; C receives the int index; optional default d is the string value (gh-353). |
--return-type TYPE |
C return type (default: void). |
--check-return |
Treat a non-zero int return as failure: raises RuntimeError(rc), returns None on success. Requires an integer --return-type (gh-363). |
--inline |
Emit a static inline body in <module>_core.h instead of a separate <name>.c. |
--doc "text" |
Python docstring for the function. |
--impl file::funcname |
Lift the function body from funcname in file instead of emitting a blank <<IMPLEMENT>> stub. |
--impl file::N:M |
Lift lines N..M (inclusive, 1-based) instead of a named function body. Ranges error cleanly. |
--replace old::new |
String substitution applied to the body lifted by --impl. Repeatable. |
Example — no parameters:
native/src/fft/fft_global_setup.c (yours to implement):
/*
* fft_global_setup.c — fft module-level function.
*/
#include "fft/fft_core.h"
/* <<IMPLEMENT: fft_global_setup>> */
void
fft_global_setup(void)
{
}
fft_core.h (declaration injected automatically):
Example — with parameters:
just-makeit function compute_window \
--module fft \
--param n:size_t \
--param beta:float \
--return-type float
native/src/fft/compute_window.c:
/*
* compute_window.c — fft module-level function.
*/
#include "fft/fft_core.h"
/* <<IMPLEMENT: compute_window>> */
float
compute_window(size_t n, float beta)
{
(void)n; (void)beta;
return (float)0.0f; /* placeholder */
}
Python call:
Array parameters work identically to jm method: append [] to the type.
just-makeit function apply_window \
--module fft \
--param data:"float _Complex[]" \
--return-type void
Path parameters (name:path) accept a str | os.PathLike from Python,
coerce it to bytes via PyUnicode_FSConverter, and forward const char * to C
— the same coercion used by the handle generator:
Python call:
from pathlib import Path
import my_pkg.dsp as dsp
dsp.load_calibration(Path("/data/cal.bin"))
dsp.load_calibration("/data/cal.bin") # str also works
Enum parameters (name:enum:<ename>[=default]) accept a choice string,
validate it against the [[enum]] SSOT in just-makeit.toml, and forward
the int index to C. Requires a [[enum]] with the matching name to be
declared in just-makeit.toml first.
just-makeit function convert_image \
--module img \
--param path:path \
--param src_cs:enum:color_space=rgb \
--param dst_cs:enum:color_space \
--return-type int \
--check-return
--check-return makes the generated binding treat a non-zero int
return value as a failure: it captures the result, raises RuntimeError on
a non-zero code, and returns None on success. Requires --return-type to
be an integer type (int, size_t, …). It is the module-function analog of
the handle generator's close_returns and composes naturally with path and
enum args.
from my_pkg import img
img.convert_image("input.png", dst_cs="lab") # → None, or raises RuntimeError
C stub (native/src/img/convert_image.c — yours to implement):