The C++/Fortran-to-Python boundary isn't an unsolved problem.
pybind11
has covered C++ well for a decade; f2py, shipped with NumPy,
has covered Fortran for longer than that. If your library is small — a
handful of functions, one header — writing those bindings by hand is a
reasonable afternoon.
It stops being reasonable around function thirty. Petroleum reservoir libraries, seismic processors, finite-element solvers — the kind of code nativegate targets — routinely expose hundreds of routines across a dozen files, half of them Fortran 77 fixed-form decks with implicit typing. At that scale the binding code itself becomes the maintenance burden: every signature change upstream is a signature change in three more places (the binding, the type stub, the test), and nothing catches the drift until an import fails at runtime with a symbol-not-found error nobody can immediately place.
What actually gets generated
ngate generate <name> reads the parsed IR — function
signatures, parameter types, class methods for C++; subroutine and
function declarations for Fortran — and produces:
- pybind11
.cppbinding source for C++, or anf2pysignature file for Fortran - the CMake (or
meson-via-f2py) build wiring to compile it - an installable Python package layout, with type stubs
- a FastAPI service exposing each bound routine as a REST endpoint, plus the same routines as MCP tools
- a generated test suite, including a numeric regression check — more on that below
None of this is novel technology. It's the same pybind11/f2py output an experienced engineer would hand-write — just produced from what the source already declares, consistently, in the time it takes to run a command instead of the time it takes to review someone's manual transcription of forty function signatures.
The part hand-written bindings usually skip
A binding that compiles isn't proof it's correct — it's proof the types
line up. Whether WELLIB still returns the pressure it
returned before someone touched the flash-calculation routine three
files away is a different question, and it's the one that actually
matters when the "legacy" code has been quietly correct in production
for twenty years.
That's what ngate golden record is for. It runs the
built package — not the source, the compiled extension — against
representative inputs and pins the outputs it returns, at
rtol=1e-9 by default. ngate golden verify replays
that same call against a fresh build and fails if any value moved outside
tolerance. The same check ships in the generated
tests/test_golden.py, so it runs automatically under
ngate test and in CI — no separate regression suite to
maintain by hand.
That's the actual value proposition: not that generating bindings is faster than writing them (it is, but that's not the interesting part) — it's that the interface, the packaging, and a numeric regression check all come from one command instead of three separate things someone has to remember to keep in sync.