Skip to content

three_face — one C core, three faces (fully generated)

Demonstrates the combined target just-makeit is architected for: a single C core exposed three ways, all calling the same gaintool_gain_step() — and jm app generates every face, with no hand-written parsing or I/O loop:

Face Artifact Generated by
Standalone C binary CLI build/gaintool jm app --target c (argv parser + read→step→write loop + add_executable linking gain_core)
Python CLI (console entry) python -m gaintool.cli / gaintool-py on PATH jm app --target console (argparse + numpy I/O loop + [project.scripts])
Python module API from gaintool import Gain native jm extension
(bonus) shareable script gaintool-script.py jm app --target pep723

gaintool scales a stream of float32 samples by --gain. The same gain_core.c is compiled once as a CMake OBJECT library and linked by the binary, the C test, and the Python extension — so all three faces behave identically (the test asserts every face agrees to within 1e-5).

The Doxygen @brief on gaintool_gain_create() in the sacred native/inc/gaintool/gain/gain_core.h header drives the generated gain.pyi class docstring. Replace the scaffold's @brief Create a gain instance. there with your own sentence, then run jm apply: the .pyi class docstring is re-derived from it.

Run it

just-makeit example three_face   # scaffold → build → run all faces
# three_face: PASSED

The example builds in a temporary directory and deletes it on exit. To run the faces by hand, build your own copy:

jm new gaintool --object gain --state gain:float:1.0f --arg-type float --return-type float
cd gaintool   # implement gaintool_gain_step(): return state->gain * x;
jm app --target c --name gaintool --object gain
jm app --target console --name gaintool-py --object gain
jm app --target pep723 --name gaintool-script --object gain
make

Then drive each face:

./build/gaintool --gain 2.0 < in.f32 > out.f32       # C binary
cd src   # the package and its built .so
python -m gaintool.cli --gain 2.0 < ../in.f32 > ../out.f32         # Python CLI
python -c "from gaintool import Gain; print(Gain(2.0).step(1.5))"  # module: 3.0

How it works

jm app derives a --<state-var> flag per ctor state var (so --gain feeds gaintool_gain_create(gain)), adds --input/--output, and emits a read→step()→write loop over the object's sample dtype — generating the C argv parser and the Python argparse from the same object model. Extra flags can be declared with jm app --flag name:type[:default[:help]] (persisted as [[app.flags]]) and appear in both parsers.

Each jm app run adds one [[app]] row to the manifest, keyed by its --name, which is why the three faces are named apart. jm apply re-renders every row, and jm remove app <name> takes one out.

Four shapes are generated: scalar (x → y, what gaintool uses), blockwise (x[] → y[]), consumer (x → void), and generator (void → y). no_step objects and anything else fall back to an <<IMPLEMENT>> stub. The app_shapes example (jm example app_shapes) builds the blockwise and generator faces, a module-function app (--function) and a subcommand app (--command) as C binaries.

History

The first version of this example had to hand-write the C main() and the Python cli.py because jm app only scaffolded plumbing. Those hand-written bodies became the spec for the generator; this example now relies entirely on generated code (its diff against the old version is the proof the generator reproduces what was hand-written).

The follow-ons that version listed have all since shipped: [[app.commands]] subcommands (jm app --command), binding jm app to module functions (jm app --function), and blockwise/steps() I/O loops.