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¶
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.