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¶
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. Optional — jm reads it from the manifest (gh-963); pass it to be explicit, and it is validated. |
--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 |
Self-sizing output: allocate a NumPy-owned array per call and trim to the returned count. 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. |
--error-on-empty |
Treat a zero count from a --variable-output kernel as a REFUSAL and raise, instead of returning an empty array. Its one return value is the length, so it has no status to carry; without this a kernel that validates its input and returns 0 produces a well-formed empty result and the caller carries on. Names the exception with --error and the text with --error-message. Mutually exclusive with none_on_empty, which reads the same zero as "nothing to report". |
--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. |
--count-default EXPR |
C expression seeding the synthesized leading count argument of a void-input --variable-output method, e.g. "state->num_taps". Its value is the zero-arg call's behaviour, and jm cannot derive it — it lives in your C (gh-1051). |
--count-name NAME |
What that synthesized argument is called (default count). See Naming the synthesized count. |
--result-field name:T[:doc] |
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). The optional third component documents that field — it reaches the PyStructSequence field and the record class in the .pyi, so it requires --single (gh-646). The result-count cap (max_results, default 64) is TOML-only for now — no --max-results CLI flag yet. |
--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). |
--record-doc "text" |
With --single, the record type's own documentation — what help(ToneMetrics) shows, and the record class's .pyi docstring. Defaults to the CPython-style synopsis (ToneMetrics(enob, sfdr_dbc)), never to nothing. A field with no :doc falls back to its trailing ///< member doc in the sacred header (gh-671). Also settable per method in the manifest as record_doc = "…" (gh-646). |
--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. |
--doc "text" |
Python docstring for the method. |
--py-return-type STR |
Override the .pyi return-type annotation (the C --return-type still drives the C signature). |
--view ClassName |
Attach the method to a view of the object instead of the object itself — adds a view-only method, or overrides a parent method by reusing its name. What --fn does or does not say decides which override it is; see Overriding a parent method. Views are a module-object feature, so the object must live in a module. |
--fn SYMBOL |
C function this method binds, when it is not the derived <comp>_<name>. For adopting existing C under its own prefix, or binding a validating variant (--fn dp_tlm_emit_checked) under the plain Python name. The Python face is unchanged. On a view it also selects a signature override (below). Persists as fn = "…" on the method. |
--error-negative |
The int return is a value unless it is negative, in which case it is an error code and the method raises. Distinct from --status-return, where the int carries nothing but status. Signed integer return types only. Persists as error_negative = "true". |
--status-return |
The int return carries only status (0 = ok): the binding raises on non-zero and the method returns None, so no status code reaches Python. Mutually exclusive with --error-negative, which is for an int that is both a value and an error channel. Persists as status_return = "true". |
--manual-stub |
This method's C binding is already hand-written in a sacred _ext_<obj>_extra.c fragment. jm declares nothing for it and only preserves its .pyi placeholder verbatim across regeneration (gh-428). Persists as manual_stub = "true". |
--error EXC |
Exception --error-negative raises (default ValueError); one of jm's error categories. Persists as error = "…". |
--error-message TEXT |
Text for that exception; jm appends (rc=%d). Persists as error_message = "…". |
--record-dtype STRUCT |
With --variable-output: return one numpy structured array whose dtype is that C struct's own layout, one row per record, and --result-field names its columns. The dtype is built at runtime by the generated C from offsetof/sizeof — jm never sees the struct, so it cannot guess C's padding (gh-788). Contrast --single, which returns one record; see Record shapes below. |
--no-bench |
Exclude this method from the generated C benchmark. |
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 — self-sizing output¶
Use --variable-output when the method's output count is not simply the
input count — decimators, FIFOs, detectors, anything that returns "however many
it produced". The generated binding allocates a NumPy-owned array of
max(<method>_max_out(state), n) per call, lets the kernel write straight into
it, and returns it trimmed to the count the kernel reported.
Each result is independent: it owns its memory, survives destroy(), and never
aliases another call's result. See Array memory
ownership for the policy and the measurements.
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 |
max_out() returning 0 is legal and means "unknown" — the binding then
sizes the allocation from the call itself. It is a sizing contract, not a
guarantee: the real bound is n_out <= max(max_out(state), n_requested),
because a generator's steps(count) writes exactly count. If your kernel
must know the capacity it was actually given, add pass_capacity = true —
which also lets the binding size the allocation from max_out() alone, but
only once max_out() takes the count and can answer for this call. See
Array memory ownership for the exact rule.
Naming the synthesized count¶
A --variable-output method with --arg-type void and no --param is the
generator shape: there is no input to size the output from, so jm synthesizes
a leading count argument. Its default value is --count-default; its name is
--count-name, and defaults to count.
just-makeit method delay ptr \
--arg-type void --return-type "double _Complex" \
--variable-output --count-default "state->n" --count-name n
Say it when your C API calls the quantity something else. The paired
<comp>_<name>_max_out() already takes its parameter name from the C
signature — deliberately, "rather than inventing a fourth name for the same
concept" (gh-607) — so leaving the method's own kwarg hard-coded put the two
halves of one generated pair at odds: ptr(count=…) beside ptr_max_out(n=…),
for the same number (gh-1074).
Declaring the count as a real --param is not the same thing, even though
it produces a byte-identical C prototype. It leaves the generator shape, and
the default goes with it:
| synthesized count | declared --param |
|
|---|---|---|
| name settable | --count-name |
yes, it is the param name |
| default | yes, --count-default |
no |
For a generator that is not a small loss: the zero-arg call's behaviour is
its default. (Until gh-1079 the declared form also gave up the out= buffer;
that shape has it back, so this table has one row fewer than it used to.)
The name reaches the binding's _kwlist, both .pyi generators, the runtime
docstring and its worked call example — one accessor, so a project cannot end
up with a stub advertising one name and a binding parsing another.
Two names are refused: anything that is not a Python identifier, and out —
which is the other slot in the same _kwlist.
Accumulating results is safe. Every call returns an independent NumPy-owned array, so holding them in a list, concatenating them, or passing them on cannot be disturbed by a later call. This used to require a weakref liveness probe over a shared buffer (gh-437); that machinery is gone, and the guarantee is now structural rather than defended at runtime (gh-604).
Record shapes¶
When a method's output is records rather than plain samples, three shapes
are available and --result-field names the fields in all three. What you get
back depends on which of --single / --record-dtype you pass:
| you pass | Python gets | the C kernel writes |
|---|---|---|
| neither | a list[tuple] |
<T> *result, size_t max_results |
--single |
one record, a named PyStructSequence |
the struct by value |
--record-dtype STRUCT |
an array of records, a structured ndarray | <STRUCT> *out |
The two are mutually exclusive — the CLI rejects them together, because "one record" and "an array of records" are different results, not two spellings of one.
# an array of records: one row per telemetry entry, as a structured ndarray
just-makeit method tlm read --module link --arg-type void \
--variable-output --record-dtype dp_tlm_rec_t \
--result-field n:uint64_t --result-field flags:uint8_t
rows = tlm.read(64)
rows["n"] # a column, no per-record Python objects
rows.dtype.itemsize # C's itemsize, padding included
The dtype is built at runtime by the generated C, from offsetof and
sizeof on the author's struct. That is not an optimisation — it is the only
correct source. jm never sees the struct definition (it lives in the sacred
header, which is yours), and numpy packs a field list where C pads it: a
{uint8_t; uint64_t} struct is 16 bytes in C and 9 as a numpy dtype built from
[(name, format), ...]. Deriving the layout from the field list would silently
mis-read every row after the first.
--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, _d0, _cap);
Py_END_ALLOW_THREADS
Every numpy accessor is hoisted into a local before the block — including
the output data pointer _d0 — so no Python C-API runs while the GIL is
dropped; the allocation and any error-raising stay above it, under the GIL. A
worker that gives each thread its own object 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¶
just-makeit property <object> <prop_name>
[--module name]
--type TYPE
[--writable] [--field] [--enum NAME]
[--value-type TYPE] [--count-fn FN] [--key-fn FN] [--value-fn FN]
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
just-makeit property reader file_type --type int --field --enum ftype
just-makeit property reader stages --type list --value-type "const char *"
just-makeit property reader keywords --type dict --value-type object
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. Optional — jm reads it from the manifest (gh-963); pass it to be explicit, and it is validated. |
--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. Requires the state struct to be a complete type — see Choosing a property kind. |
--doc "text" |
Python docstring for the getter (and setter, if --writable). |
--enum NAME |
Present the property as a string from the named [[enum]] SSOT instead of the raw int (gh-519). See below. |
--value-type TYPE |
Element type of a dict/list/tuple property (gh-543). A C type means jm emits the conversion and your accessor stays pure C; object means --value-fn returns a PyObject * itself. See Container properties. |
--count-fn FN |
Entry-count accessor for a container property. Default <obj>_num_<prop>. |
--key-fn FN |
Key accessor for a dict property. Default <obj>_<prop>_key. |
--value-fn FN |
Value accessor for a container property. Default <obj>_<prop>_value. |
--capsule NAME |
With --type capsule: publish a borrowed pointer as a PyCapsule under that name, so a peer extension can consume it by name (gh-788). The capsule does not own the pointer and carries no liveness — the getter checks the object is alive before handing it out, and the capsule's destructor is NULL by contract. Persists as capsule = "NAME". |
--view ClassName |
Attach the property to a view of the object instead of the object itself — adds a property the parent lacks, or overrides a parent property (e.g. its doc) by reusing its name. Views are a module-object feature, so the object must live in a module. |
Choosing a property kind — and what each costs your header¶
A property is backed one of five ways. The flags are not variations on a theme: they decide who writes the C and, crucially, whether your state struct has to be a complete type in the public header.
| Kind | Declared with | Who implements the read | State struct |
|---|---|---|---|
| Computed | no backing flag | you — jm declares <obj>_get_<prop>(const <obj>_state_t *) for the sacred _core.c |
may stay opaque |
| Field | --field |
jm — injects TYPE prop; and returns state->prop |
must be complete |
| Expression | --expr "…" |
jm — emits your C expression inline | complete, if the expr reads a member |
| Buffer | --buf-field name |
jm — returns a numpy view over that member | must be complete |
| Container | --type dict\|list\|tuple |
jm — generates the loop, refcounting and error paths; you supply count/key/value | may stay opaque |
Omitting --field is not "unsupported" — it means "you implement the
accessor". That is the computed kind, and it is the one to reach for when the
struct is deliberately private: a file handle, a scratch buffer, a decoded
header. jm declares
which takes a pointer to the state, so an incomplete type is fine — you can
forward-declare typedef struct rdr_state rdr_state_t; in the header and keep
the definition in your _core.c. The generated binding never touches a member.
The other three kinds read a member directly, so they need the definition. With
an opaque struct a --field property fails at compile time:
If you see that, the property wants to be computed.
A computed property composes with everything else here, including
--enum and its out-of-range check — the
accessor's return value is decoded exactly as a field's would be:
Enum-valued properties (--enum)¶
A property whose C value is an index into a [[enum]] table can present that
value to Python as its string, exactly as a kind = "handle" module's
getters have always done. C still stores the int; only the Python face
changes.
[[enum]]
name = "ftype"
values = ["raw", "wav", "blue"] # order IS the C int: raw=0, wav=1, blue=2
[[reader.properties]]
name = "file_type"
type = "int"
field = true
enum = "ftype"
>>> r.file_type
'blue'
>>> r.file_type = "wav" # only when the property is --writable
>>> r.file_type
'wav'
>>> r.file_type = "nope"
ValueError: invalid file_type 'nope' (choices: raw, wav, blue)
The stub types it as Literal["raw", "wav", "blue"], so a typo is caught by
mypy rather than at runtime.
Two things worth knowing:
- The value is range-checked. If the C side holds an index the table
cannot contain — say a format code decoded from a file header that names a
variant you do not support — reading the property raises
ValueError: file_type holds out-of-range ftype value 99 (valid: 0..2)rather than reading past the table (gh-521). enumis not the same asstring_enum:.enum = "name"references the shared[[enum]]SSOT and is what properties, handle getters, and--param name:enum:<ename>use. The inlinestring_enum:a,b,cform spells its choices in the type itself and applies to constructor init-params. Prefer the SSOT whenever the same choice set is used in more than one place — that is the whole reason it exists.
--enum cannot be combined with a buf_field property: an array of enum
strings has no decoded form, and jm rejects it with a diagnostic rather than
generating something misleading.
Container properties — dict, list and tuple¶
A property whose --type is dict, list or tuple is backed by a small
iteration protocol your core implements. jm generates the loop, the container,
the refcounting and every error path; you supply the accessors.
[[reader.properties]]
name = "keywords"
type = "dict"
count_fn = "reader_num_keywords" # size_t (const state *)
key_fn = "reader_keyword_tag" # const char * (const state *, size_t)
value_fn = "reader_keyword_value" # <value_type> (const state *, size_t)
value_type = "object"
All three accessor names default from the object and property name
(<obj>_num_<prop>, <obj>_<prop>_key, <obj>_<prop>_value), so the common
declaration names none of them. key_fn applies to dict only — a list or
tuple is keyed by position, and passing it there is an error rather than a
silently ignored flag.
Container properties are read-only. jm builds the container fresh on every read, so a setter would mutate a copy the caller never sees; expose a method that mutates the core instead.
Choosing a value_type¶
This is the one real decision, and it is about who owns the conversion.
value_type |
Conversion | Where value_fn lives |
|---|---|---|
a C type (double, const char *, …) |
jm emits it | the sacred _core.c — pure C |
object |
you emit it | a hand-written *_ext_extra.c |
Prefer a C type. Your accessor returns an ordinary C value, jm converts it, and
your core never includes Python.h:
Reach for object when the value's Python type is data-dependent and so
cannot be annotated statically. The motivating case is a BLUE extended header,
where each keyword's type comes from a code stored in the file itself — one
keyword yields a str, the next an int, the next a list[float]. There,
value_fn returns a PyObject * directly:
It must return a new reference, or NULL with an exception set. Because it
needs Python.h it cannot live in the pure-C core — put it in a hand-written
<obj>_ext_extra.c (standalone) or <module>_ext_<obj>_extra.c (module),
which jm wires in and never modifies. jm forward-declares it above the getter,
so the #include order works out.
Stubs, and why this beats hand-writing it¶
The generated .pyi annotates as dict[str, T], list[T] or tuple[T, ...]
— Any for the object escape hatch. That is half the point: a property
hand-written into a sacred fragment never reaches the stub at all, so it stays
invisible to a type checker even though the runtime is correct.
Errors you get for free¶
A key_fn or a pointer-valued value_fn that returns NULL raises a
RuntimeError naming the property, the accessor and the index. Both are real
guards, not decoration: PyUnicode_FromString(NULL) reaches strlen(NULL) and
crashes, so the unchecked form segfaults rather than raising. A partially built
container is released on every failure path.
Removing a method or property¶
just-makeit remove <kind> <name> --object <obj> [--module <mod>] [--force],
where kind is method or property (also object, module, function,
state, warning, or error for those respectively — --object/--module
only apply where relevant; warning and error are addressed differently,
see their own sections below):
just-makeit remove method configure --object nco --module dsp
just-makeit remove property phase --object nco --module dsp
This 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 via
just-makeit remove state <name> --object <obj> is structural and rebuilds
the object via the regenerate path instead.)
just-makeit view¶
just-makeit view <object> <ViewClassName>
--module name
--create-fn fn
[--init-param name:type[:default] ...]
[--exclude-property name ...] [--exclude-method name ...]
[--doc "text"]
Add a second Python class over the same generated C core (gh-504). The view
shares the object's <obj>_state_t, its _core.c, and its step(); it differs
only in the C constructor it calls, the constructor arguments it takes, and the
Python surface it exposes. Use it when one algorithm has two front doors — a
continuous mode and a burst mode, an empty accumulator and a pre-seeded one —
and a second object would mean a second copy of the C.
just-makeit view acc SeededAcc --module bank \
--create-fn acc_create_seeded \
--init-param seed:double:0.0 \
--exclude-method total
This records a [[<obj>.views]] entry, injects
acc_state_t *acc_create_seeded(double seed); into <obj>_core.h, appends an
<<IMPLEMENT>> stub for it to the sacred <obj>_core.c (so the module still
compiles before you have written a line), and regenerates the module glue with
the extra class registered. The view lands in its own binding fragment,
<mod>_ext_<viewclass>.c, next to the parent's — no second core library and
no second state struct.
Views are a module-object feature: the multi-type module machinery is what
registers the extra class, so --module is required. --create-fn is also
required and must differ from the parent's <obj>_create — a view exists
precisely to build from a different constructor.
Arguments
| Argument | Description |
|---|---|
object |
Object the view sits over (must already exist in just-makeit.toml). |
ViewClassName |
Python class name for the view. Must be unique across every class the module exposes — each object's class name and every existing view. |
--module name |
Module the object belongs to. Required. |
--create-fn fn |
C constructor the view's __init__ calls (e.g. acc_create_seeded). Required; must differ from <obj>_create. Scaffolded as a stub in the shared core. |
--init-param name:type[:default] |
The view's own constructor parameter. Repeatable; same syntax as jm object --init-param. Omit entirely to inherit the parent's constructor shape. |
--exclude-property name |
Parent property to omit from the view's Python surface. Repeatable; must name an existing property of the parent. |
--exclude-method name |
Parent method to omit. Repeatable; must name an existing [[<obj>.methods]] entry. The builtins step/steps/reset are not excludable. |
--doc "text" |
Docstring for the view class. |
Diverging, not only trimming¶
--exclude-property / --exclude-method take members away. To add a
member the parent lacks, or override one it has, pass --view <ClassName>
to jm property, jm method,
or jm warning — reusing a parent member's name
overrides it, a new name adds it:
# `runs` exists on SeededAcc only
just-makeit property acc runs --module bank --type size_t --field \
--doc "reseed count" --view SeededAcc
# same name as the parent's property -> overrides its docstring on the view
just-makeit property acc depth --module bank --type size_t --field \
--doc "seed depth" --view SeededAcc
These persist as [[<obj>.views.properties]], [[<obj>.views.methods]], and
[[<obj>.views.warnings]] under the matching view, and are merged over the
parent's when the glue is generated. See
Configuration → Schema reference.
Excluding a method drops only the view's Python wrapper and its PyMethodDef
entry — the shared C function stays in the core, so nothing dangles and the
parent keeps working.
Overriding a parent method¶
A view method that reuses a parent method's name is an override, and there
are two kinds. --fn decides which (gh-1012, gh-1011):
| you pass | you get | the view calls |
|---|---|---|
--doc only (or a matching signature) |
a doc-only override | the parent's <obj>_<name> |
--fn <symbol> |
a signature override | its own <symbol>, scaffolded into the shared core |
This is not a flag standing in for a second question. The parent's C symbol
carries the parent's prototype, so a different signature is only callable
through a different symbol — which is exactly what --fn supplies:
# doc-only: same signature, the view just words it differently
just-makeit method acc plain --module bank --view SeededAcc \
--doc "seeded plain"
# -> View 'SeededAcc' overrides the doc of 'plain' (shares acc_plain)
# signature override: its own arg_type, so its own C function
just-makeit method acc scaled --module bank --view SeededAcc \
--fn acc_scaled_seeded --arg-type float --return-type double \
--doc "seeded scale"
# -> Implement acc_scaled_seeded() in acc_core.c
Both prototypes then stand side by side in the sacred header, and only the view's fragment calls the override:
double acc_scaled(acc_state_t *state, double x); /* parent */
double acc_scaled_seeded(acc_state_t *state, float x); /* the view's */
A differing signature without --fn is refused, rather than silently
ignored as it was before 0.62.0 — the view would have called the parent's
symbol, so the declaration could only ever have been dropped:
error: view 'SeededAcc' redeclares parent method 'scaled' with a different arg_type.
Without --fn the view calls the parent's acc_scaled, which has the parent's
signature, so this could only be ignored.
Pass --fn <symbol> to bind its own C function (gh-1012), or drop the
differing key(s) for a doc-only override.
Only the keys you actually state are compared, so a hand-written manifest
entry carrying nothing but doc is a doc-only override and stays legal. And
--fn naming the parent's own symbol is refused up front, rather than left for
the C compiler to report as a conflicting redefinition:
error: view 'SeededAcc' overrides method 'scaled' with fn 'acc_scaled', which is
the symbol the parent already binds.
A signature override needs its OWN C function — give --fn a different name, or
drop it for a doc-only override.
Properties and warnings have no second kind: reusing a parent's name overrides
how it reads, as in the depth example above.
Limitation: a view cannot yet sit over a parent whose init-params use
per-array dtype-dispatch or optional-array forms (real_type, real_create_fn,
create_fn, optional); those paths embed <obj>_create directly and would
silently ignore the view's create_fn, so jm view rejects them with a
diagnostic.
The Views example builds the whole thing end to end.
just-makeit warning¶
just-makeit warning <object>
--condition FIELD
--message TEXT
[--module name] [--category NAME] [--stacklevel N]
Declare a warning that fires after construction when a boolean state field
is set — a way to flag a degenerate-but-legal configuration without failing the
constructor. The generated __init__ glue checks the field once the object is
built and, if set, calls PyErr_WarnEx with your message and category.
just-makeit warning agc underpowered \
--message "AGC gain floor reached; output may clip" \
--category RuntimeWarning
This is declarative: the condition, message, and category live in
just-makeit.toml as a [[<object>.warnings]] entry, and the check is
regenerated into _ext.c — you write no C. --condition names a bool-ish
field on the object's state struct (emitted as self->handle-><field>), so it
must already exist as state (add it with just-makeit add if needed).
Arguments
| Argument | Description |
|---|---|
object |
Object name (must already exist in just-makeit.toml). |
--condition FIELD |
Bool-valued state field that triggers the warning. Required. |
--message TEXT |
Warning text shown to the Python caller. Required. |
--category NAME |
Warning class — a Python built-in warning (UserWarning, DeprecationWarning, RuntimeWarning, …). Default UserWarning. |
--module name |
Module the object belongs to. Optional — jm reads it from the manifest (gh-963); pass it to be explicit, and it is validated. |
--stacklevel N |
PyErr_WarnEx stacklevel, so the warning points at the caller's frame. Default 1. |
--after POINT |
Where the check runs. Only __init__ (the default) is supported — anything else is refused rather than accepted and ignored, so the flag is a declared extension point, not a knob with hidden values. |
--view ClassName |
Attach the warning to a view rather than the object, so a second front door over the shared core gets its own PyErr_WarnEx. Requires --module (gh-509). |
An object may carry more than one warning — each just-makeit warning call on
a distinct --condition adds another. Re-running with the same condition
replaces that entry.
Remove one with just-makeit remove warning <condition> --object <obj> — a
warning has no name of its own, so its condition is what identifies it.
just-makeit error¶
Translate a create() failure into a specific Python exception. By default a
failed constructor raises a blanket MemoryError; this replaces that with an
exception class and message of your choosing.
just-makeit error biquad \
--category ValueError \
--message "biquad coefficients unstable (|poles| >= 1)"
Also declarative: the choice is stored as create_error /
create_error_message in just-makeit.toml and regenerated into the _ext.c
failure path. There is one failure channel — a NULL from create() cannot
say why it failed — so this applies to every create() failure, a genuine
allocation failure included. Choose a category that reads sensibly for the most
likely cause.
Arguments
| Argument | Description |
|---|---|
object |
Object name (must already exist in just-makeit.toml). |
--category NAME |
Exception class — a Python built-in exception (ValueError, RuntimeError, OverflowError, …). Required. |
--message TEXT |
Text for the raised exception. Required. |
--module name |
Module the object belongs to (required for module objects). |
--view ClassName |
Give a view its own translation instead of the object's. Views are a module-object feature, so the object must live in a module. |
Each object has a single failure translation, so re-running replaces it rather
than accumulating. Remove it with just-makeit remove error <obj> --object <obj> — error takes no name because there is only ever one per object.
Views inherit their parent's translation¶
A view is a second Python class over the same C core, so declaring the error on the parent covers both front doors — you do not need to repeat it:
just-makeit error rateconv --module resample \
--category ValueError \
--message "RateConverter: invalid parameter (need rate > 0)"
# RateConverter(...) -> ValueError
# MatchedRateConverter(...) -> ValueError, same message
This matters because a view exists precisely when the constructor takes different — usually more — parameters, so it is the constructor with the most ways to be handed something invalid.
Add --view when the view's extra parameters deserve their own wording:
just-makeit error rateconv --module resample --view MatchedRateConverter \
--category ValueError \
--message "MatchedRateConverter: invalid pulse/beta/span combination"
Only an explicit --view declaration is written to the manifest; an inherited
translation keeps tracking the parent rather than being frozen into a copy.
Note the contrast with just-makeit warning, where a
view does not inherit: a warning describes a condition the view may not
have, while an error describes the same object refusing to construct.
just-makeit function¶
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). |
--out-type TYPE |
Allocate a 1-D output array of this element type per call and append out last to the C call. |
--variable-output |
With --out-type: the function allocates its own 1-D output rather than returning a scalar — no caller buffer and no cached instance buffer. out is appended last to the C call, and the binding returns the ndarray (gh-335). A size_t-returning function is trimmed to the count it reports; a void one returns the full allocation. |
--out-size EXPR |
Length of that output, as a verbatim C expression over the function's own arguments — including each array param's generated <name>_len (e.g. x_len * factor, or a call like wfm_rrc_ntaps(sps, span)). Omit it and the length falls back to the first array parameter's length. |
--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):