nativegate
← Blog

Generated bindings, not hand-written ones

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 .cpp binding source for C++, or an f2py signature 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.

pinning a baseline
$ ngate build pvt && pip install services/pvt/dist/*.whl $ ngate golden record pvt writes services/pvt/golden.json $ # later, after a regeneration or a source change: $ ngate golden verify pvt replays the baseline against the installed package — fails on drift

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.

Source: nativegate CLI reference, ngate golden.

See the full CLI → View on GitHub