Skip to main content

nativegate — make legacy C++ and Fortran AI-ready and microservice-ready

CI PyPI Python MCP server (Glama)

Expose existing C++ and Fortran code to Python as deployable microservices — without hand-writing bindings, and without touching the numerics.

Point it at a header or a Fortran source and it generates the pybind11/f2py bindings, the CMake build, an installable Python package, a FastAPI service, smoke tests, a numerical regression baseline, and a Dockerfile.

pip install "nativegate[clang,build]"   # from PyPI; needs cmake/ninja + a C/C++/Fortran toolchain for the build step

ngate quickstart libraries/geometry/geometry.hpp --name demo --build
pip install services/demo/dist/*.whl
from demo import Geometry

Geometry().hypotenuse(3.0, 4.0)   # 5.0

...plus a FastAPI service over the same binding:

ngate serve demo
curl -X POST "http://localhost:8000/hypotenuse?a=3&b=4"

Built for re-hosting the kind of code that runs an engineering business: 1990s C++ over F77, fixed-form decks with COMMON blocks and INCLUDE files, PVT correlations nobody wants to rewrite and nobody can afford to get wrong. The worked example of that in this repo is services/petro_api, generated from the Fortran decks in libraries/petro/.

Install

cd tools/nativegate
./scripts/bootstrap.sh            # creates .venv, installs everything

Or piecemeal, into an environment of your own:

pip install -e ".[clang,build,test,docs]"
Extra What it adds
clang libclang — the C++ AST parser. Without it, C++ falls back to a much weaker regex reader
build scikit-build-core, pybind11, numpy, fastapi, uvicorn — needed to build generated services
test pytest, httpx
docs mkdocs + material

Python 3.10+. ngate build also needs a system toolchain that pip cannot install: cmake, ninja, a C++ compiler, and gfortran for Fortran services.

Global install (ngate on your PATH everywhere)

To get the ngate command in every shell/project without activating a venv, install this checkout editable with pipx:

python3 -m pip install --user pipx
python3 -m pipx ensurepath        # adds ~/.local/bin to PATH, restart your shell after
pipx install -e tools/nativegate   # run from the repo root

-e keeps it editable, so changes under nativegate/ take effect immediately — no reinstall needed. Verify with ngate --version from any directory.

If pyproject.toml's dependencies list changes (e.g. a new package is added), pipx install -e won't pick it up on its own — the venv pipx made already exists. Re-run the install to rebuild it:

pipx reinstall nativegate

Using it in another project

nativegate is an ordinary pip package with a console entry point. Nothing in it refers to this repo: services/ and libraries/ resolve against your current working directory, so "use it elsewhere" means install it into that project's environment and run it from that project's root.

cd /path/to/other-project
python3 -m venv .venv
.venv/bin/pip install "nativegate[clang,build,test] @ git+https://github.com/ravikings/nativegate.git@<sha>#subdirectory=tools/nativegate"

.venv/bin/ngate quickstart src/mylib.hpp --build

quickstart scaffolds services/mylib itself — no separate init needed unless you also want the empty libraries//infrastructure/ directories. --build compiles the wheel in the same run. Progress prints as a live checklist (scaffold → copy source → generate bindings → build):

✔ Scaffold service
✔ Copy native source
✔ Generate bindings & package
✔ Build wheel

Done — services/mylib is ready.

Pin @<sha> (or a tag). This is a code generator: an unpinned upgrade can change the bindings under a service that was working, and you want that to be a deliberate commit rather than a surprise on someone's laptop.

Installing from a local checkout works the same way, and is what you want while changing the tool itself:

pip install -e "/path/to/haliburtion/tools/nativegate[clang,build,test]"

Three things worth knowing before rolling it out to a team:

  • Extras are not optional in practice. Without clang the C++ front end silently falls back to the regex reader and your macro-defined and #included symbols quietly vanish — set parser: clang in nativegate.yaml so a mis-provisioned machine fails loudly instead. Without build, ngate build cannot run; without test, ngate test cannot.
  • Run from the project root. libraries: and include_paths: in nativegate.yaml resolve from there too.
  • Generated services do not depend on nativegate. services/<name>/ is a self-contained wheel plus a FastAPI app — hand it to a team that has never installed the generator and it still builds.

Use it as an agent skill

The tool ships a Claude Code skill at tools/nativegate/skill/SKILL.md, so a coding agent reaches for ngate instead of hand-writing a binding. This repo already exposes it — .claude/skills/nativegate is a symlink to that directory, so editing SKILL.md updates the installed skill with no copy step.

To install it in another project:

mkdir -p .claude/skills
cp -r /path/to/nativegate/tools/nativegate/skill .claude/skills/nativegate
# or, to track the tool: ln -s /path/to/nativegate/tools/nativegate/skill .claude/skills/nativegate

The skill covers what the agent has no way to infer from the CLI's --help: that suggest exists so it should not read headers itself to pick a starting file, that a regex reader line means the parse is missing symbols and must not be trusted, that skipped-binding reasons should be relayed rather than summarized, and that generated files are rewritten every run so hand-edits are lost. It also carries the production-readiness gap list, so the agent raises the process-wide call lock and the single-tenant boundary unprompted rather than handing over a service as if it were ready for anything.

Why an agent should use it: the token argument

For a human the saving is hours. For an agent it is context, and context is the binding constraint on whether the task finishes at all.

Hand-writing the binding for one 20-method C++ class means emitting the pybind11 translation unit, CMakeLists.txt, pyproject.toml, the package __init__, a FastAPI app and a test module — on the order of 500 lines, call it 7–10k output tokens if every line lands correctly on the first attempt. They do not. Each compile or link error costs another round of reading the error, re-reading the file, and re-emitting a corrected version; three or four rounds is normal for template-adjacent code, and include/-vs-src/ layout and undefined-symbol-at-import failures are exactly the kind of thing that burns several. A realistic figure is 20–40k tokens per file, most of it output, and all of it resident in context afterwards — crowding out the actual engineering question the agent was asked.

The same work through nativegate is one command and a four-line checklist: roughly 300–500 tokens, and the generated files never enter context because the agent has no reason to read them. Those are estimates from the size of the generated artifacts, not a measured benchmark — but the ratio is not a close call, and it is structural rather than a matter of prompting the agent better.

Three effects beyond the raw count:

  • The repair loop disappears, not just shrinks. The generator emits code that already compiles for the constructs it accepts, and refuses the ones it cannot bind with a reason. The agent never enters the read-error → re-emit → rebuild cycle that dominates hand-written binding cost, and never spends a round discovering that a header does not compile.
  • golden verify is a verification signal the agent cannot fake. Left to itself, an agent asked whether a re-hosted library still works writes tests from its own reading of the source — which validates its interpretation, not the numbers. golden record / golden verify gives a real pass/fail on the answers for a couple of hundred tokens.
  • Correctness stops depending on how much context is left. A hand-written binding degrades as the window fills — later methods get less attention than earlier ones. Generated output is identical at the start of a session and at 80% context.

Which file do I point it at?

For a codebase you did not write, suggest parses every header it can find and ranks them by how cleanly they would bind — preferring self-contained files, because a good first service is one that drags nothing else in:

$ ngate suggest libraries/petro/cpp

      file                        binds                     skipped  notes
  ~   include/Units.hpp           11 fn, 1 class, 9 method        1  Units.cpp
  ~   include/WellModel.hpp       1 class, 21 method                 WellModel.cpp; needs FortranBridge.hpp
  ~   include/FluidModel.hpp      1 class, 18 method, 1 struct       FluidModel.cpp; needs FortranBridge.hpp
  ~   include/Simulator.hpp       1 class, 24 method             10  Simulator.cpp; needs DeckReader.hpp, FluidModel.hpp +2
  ✘   include/FortranBridge.hpp   2 fn, 3 struct                 26  no .cpp found

Start with libraries/petro/cpp/include/Units.hpp:

  ngate quickstart libraries/petro/cpp/include/Units.hpp --name units --build

✔ binds everything it declares and needs no other header; ~ binds more than it skips; ✘ binds nothing usable. Dependencies outrank skip count in the ranking — one skipped array method costs you that method, while one #include of another subsystem costs you that subsystem's whole build.

If the header line says regex reader rather than clang AST, install the parser first (pip install "nativegate[clang]") and re-run: the fallback reader silently misses free functions and anything behind a macro, so the ranking will be scored on an incomplete picture.

The loop

Run everything from the repo root — commands resolve services/<name>/ relative to your working directory.

ngate init                            # optional: empty libraries/, infrastructure/ dirs
ngate suggest src/                    # which file should I start with?
ngate quickstart src/pvt.hpp --build  # scaffold + expose + generate + build, one shot
ngate inspect src/pvt.hpp             # what the parser sees, before generating
ngate generate pvt                    # re-run codegen after editing nativegate.yaml
ngate build pvt                       # pip wheel . -> scikit-build-core -> CMake
ngate test pvt                        # the generated pytest suite
ngate golden record pvt               # pin the numbers (commit golden.json)
ngate golden verify pvt               # prove a rebuild returns the same answers
ngate lock pvt                        # pin deps by version + SHA-256
ngate docker pvt --build              # multi-stage image, non-root, healthcheck
ngate k8s pvt                         # -> infrastructure/kubernetes/pvt.yaml
ngate gateway platform-api --service pvt

Every generated service and gateway also serves an MCP endpoint at /mcp, so the same native routines are callable as tools by Claude or any other MCP client. It is the same routes — FastMCP derives one tool per route from the router's OpenAPI schema — so the tool surface cannot drift from the HTTP surface, and the COMMON-block lock still applies.

What it parses

C++ — via Clang (libclang), the same front end that compiles the code. The preprocessor runs, so #included types, #defined names, #ifdef branches and macro-mangled extern "C" bridge declarations resolve. Handles classes, structs, overloads, public inheritance, constructors, typedefs and using aliases, enums, namespaces, std::string, static methods, abstract classes, = default / = delete. Also: std::vector<T> of scalars, const char* inputs, operators bound as Python special methods (__add__, __eq__, __getitem__, …), and a raw T* paired with a length argument bound as a numpy buffer — including on class methods.

Refused with a reason, never silently: templates (a typedef does not help), a non-const std::vector<T>& (pybind11 would discard what you write into it), a non-const char* (an output buffer with no length convention), a raw pointer with no length argument to pair it with, pointer returns of class types (ownership), and types only forward-declared. A header that does not compile is refused outright rather than half-bound — clang recovers from an unknown type by pretending it was int, and binding that produces a service whose signatures disagree with the C++ it calls.

extern "C" functions whose bare T* arguments are single scalars passed by reference (the Fortran-linkage convention) can be opted in per function with clang.scalar_ref_functions. It is opt-in and never inferred, because an array whose extent lives in a COMMON block or PARAMETER looks identical in a C prototype. It applies to free functions under the clang backend only.

Fortran — you name the routines you want, and a large legacy deck costs only what you expose. Fixed-form F77 and free-form F90+, INCLUDE expansion, kind-parameter resolution (real(dp) → real(8)), inferred argument intent, module-nested routines, array arguments, and .F90/.F sources run through gfortran -cpp first.

Two newer Fortran features are narrower than they sound, so they are worth stating precisely. Derived types are bound by generating a flattening shim, but only for a subroutine, in free-form source, parsed by the fparser2 backend, where the routine is inside a module, the type is defined in the same file, and every component is a scalar real/integer/logical. CHARACTER outputs are bound only when the declaration fixes the length (CHARACTER*80); an assumed-length output is demoted to an input and reported, because f2py builds it and then silently returns an empty string. That screening currently runs on the fixed-form path only.

Everything the parser recognises but cannot bind is reported at generate time with the reason — the failure mode that costs the most time is a header with thirty methods producing a module with twenty-six and nothing saying which four are missing.

Verification

For re-hosted engineering code, "it builds and imports" is not the acceptance criterion. Three gates sit beside each other, each answering a different question that the others structurally cannot:

asks compares against catches
golden.json did it change? its own past compiler/flag drift, a regenerate picking a different overload, a refactor of the native source
oracle is it faithful? the legacy binary transposed arrays, wrong intent, narrowing, units, argument-order swaps, a wrong first recording
invariants.json is it possible? mathematics wrong away from the sample point, NaN at domain edges, hidden process-global state

Run every layer at once, in the order CI runs them — oracle first (a faithful binding is a precondition for the other two meaning anything), then golden, then invariants — with each layer reported by name so one layer's failure never masks another's:

ngate verify pvt
# oracle: passed (9 covered, 0 skipped)
# golden: passed
# invariants: passed

Layer 1 — golden.json: did the answer change?

ngate golden record pvt  # services/pvt/golden.json — commit this
ngate golden verify pvt  # 4 entry point(s) unchanged (0 not covered).

golden.json records every bound entry point, the inputs it was called with, and the answer. Edit the inputs to values your engineers recognise (a real pressure, a real API gravity); they survive future re-records. The generated tests/test_golden.py replays the same calls inside the service's own suite, so CI catches drift with no extra wiring. Changing a conversion constant from 14.5037738 to 14.5038 fails the check — a change no compiler, type checker or unit test would flag.

Its limit is structural: golden only asserts about the one input tuple it recorded, and only proves the binding is unchanged, never that the first recording was right.

Layer 2 — oracle: is the binding faithful to the legacy code?

ngate oracle check pvt
# 9 covered, 0 skipped

oracle check generates a driver in the original language from golden.json's recorded entries (never from a fresh sample plan), compiles only the driver translation unit, links it against the extension's own built objects (never a second compilation of the library sources), runs both paths in the same build, and compares every observable value bitwise — 16 hex digits of IEEE-754, no tolerance, because the same machine code produced both sides. It needs a compiler; it needs no committed file, and regenerates the driver on every run so a stale one can never pass. A failing comparison is classified (argument order, transposition, float32 narrowing, a missing in-place output, ...) so the fix is obvious from the message rather than from a difference alone.

Layer 3 — invariants.json: is it possible?

ngate invariants verify pvt
# 6 function(s) checked, 1 uncovered (services/pvt/invariants.json):
#   solution_gor: pass (6 propert(y/ies), 33 point(s))
#   [uncovered] tubing_bhp: no range declared for parameter(s) rate

Declare what a function's answer must satisfy in nativegate.yaml:

state:
  setup: [pvt_set_fluid]     # replayed before every property evaluation
  mutating: [pvt_set_fluid]  # this routine's job IS to change state
  error_flag: last_error

invariants:
  solution_gor:
    - bounds: {min: 0.0}
    - monotone: {in: pressure, direction: nondecreasing}

ranges:
  pressure: [14.7, 10000.0]  # swept, 33 points, inclusive

invariants verify checks two kinds of property over a fixed, declared lattice (a per-parameter sweep, never random, never a cartesian explosion): structural properties (finite, total, no_error_flag, idempotent, order_independent — derived from the IR plus the state: declaration, no domain knowledge needed) and declared properties (bounds, monotone, sum_to_one — a closed, non-eval'd vocabulary authored by a human). A failing declared property reports the first failing lattice point and bisects to the tightest bracket it can prove. A swept parameter with no declared range is not skipped silently — it is recorded under uncovered, and a run whose checked block comes out empty is a hard failure, never a quiet pass: an invariants file that checks nothing must not look like one that does.

Layout

nativegate/
  cli.py            the CLI (click)
  ir.py             language-neutral IR — the seam every layer meets at
  config.py         nativegate.yaml
  golden.py         numerical regression harness
  discovery.py      language/dialect detection
  preprocess.py     INCLUDE expansion and gfortran -cpp for .F90/.F
  suggest.py        ranks candidate sources
  locking.py        dependency pinning for requirements.lock
  parsers/
    cpp.py              front door: picks a C++ backend, reports which ran
    cpp_ast.py          Clang AST parser (primary)
    cpp_regex.py        token/brace reader (fallback, no libclang needed)
    fortran.py          front door: picks a Fortran backend
    fortran_fparser.py  fparser2 parse tree (primary)
    fortran_regex.py    targeted per-routine reader (fallback)
    fixed_form.py       F77 fixed-form helpers (IMPLICIT, CHARACTER lengths)
  generators/       pybind11, f2py, CMake, Python package, FastAPI router,
                    middleware, error handling, tests, golden test,
                    Dockerfile, gateway, MCP server, Kubernetes, pyproject

Parsers normalise source into ir.ModuleIR; generators consume it without knowing which language it came from. Both C++ backends emit the identical IR, which is why swapping the regex reader for a real compiler front end changed nothing below parsers/. The Fortran side has since gone the same way: the fparser2 backend replaced the regex reader as the default, and the regex reader is still there for machines without the package.

Developing

.venv/bin/python -m pytest tests -q                           # 694 tests
NATIVEGATE_CPP_PARSER=regex .venv/bin/python -m pytest -q      # C++ fallback backend
NATIVEGATE_FORTRAN_PARSER=regex .venv/bin/python -m pytest -q  # Fortran fallback backend
.venv/bin/python -m mkdocs serve                              # docs at :8000

The suite runs green under both C++ backends — that is the contract that lets either one be selected. Beyond unit tests, both language paths are verified by really building them: generated CMake fed to actual cmake/clang++/ gfortran, the resulting extension imported, the FastAPI service exercised over HTTP. Two real bugs (f2py module nesting, the missing-.cpp link failure) were found that way and not by inspection.

Docs

docs/, or mkdocs serve:

Before you ship

Honest positioning: this is usable today for internal, single-tenant deployments, where a team is putting an HTTP or Python interface on legacy code it already trusts. It is not ready for internet-facing or multi-tenant use.

The compile-and-serve path is real and verified. Generated services do now carry API-key auth (api.auth: api_key), rate limiting, a request size cap, request IDs and access logging, an exception handler, /healthz and /readyz with SIGTERM draining, and Kubernetes manifests via ngate k8s.

What remains, and why it is not just a to-do item:

  • Fortran endpoints are serialised process-wide. Each holds one lock across the native call, because COMMON blocks are process-global storage and two concurrent requests that configure different states would otherwise read back each other's numbers silently. Throughput scales with processes, not threads — do not plan capacity as if these endpoints were concurrent. C++ services are not locked (a fresh instance per request covers the usual case), which also means a C++ library holding file-scope static state has no protection here and the parser cannot detect that it needs any.
  • Stateful libraries get a session-shaped API — "configure, then read". That is fine when one tenant owns the process and wrong when it does not.
  • MAX_ARRAY_ITEMS is a memory guard, not a bounds check. Emitted for services that take array arguments. The IR records that a parameter is an array, not the extent the routine actually declares, so one configurable cap (NATIVEGATE_MAX_ARRAY_ITEMS, default 65536) stands in for all of them. A real extent check waits on the fparser2 front end.
  • A segfault still takes the worker down. No sandbox, no per-call timeout, no per-call isolation.
  • No CI/CD templates; infrastructure/docker/ is an empty skeleton.

Read Is this production-ready? and DEFECTS.md before a paying workload touches this. Those are honest gap lists, not a sales pitch.

Refusals are part of the contract

When the parser recognises a symbol but will not bind it, that is a decision, not a gap it forgot to fill: a non-const char* output buffer has no length, a pointer return has no ownership, a derived-type Fortran result has no mapping. Each refusal is printed at generate time with its reason, and the generated service publishes them at GET /_unexposed so they stay visible to whoever is calling the API. An empty mapping there means this build refused nothing; a 404 means the service predates the route and cannot tell you.

Metadata

Release files for nativegate 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nativegate 0.1.1
File Size Uploaded
nativegate-0.1.1.tar.gz 459.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nativegate 0.1.1
File Interpreter ABI Platform
nativegate-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 760.0 kB

Release files / nativegate-0.1.1.tar.gz

Download URL nativegate-0.1.1.tar.gz
Size 459.5 kB
Tags Source
SHA-256 checksum
How to use checksums
bd2965a95d94cdd8b3a2f4d225eae3c459b6587c3e8955c809794b4bb8695425
BLAKE2b-256 checksum
How to use checksums
87dc1f6db32f9cca9d04f49ef1257d0d55cd6cedce69f3d77498ab44c6f7aed4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release files / nativegate-0.1.1-py3-none-any.whl

Download URL nativegate-0.1.1-py3-none-any.whl
Size 300.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d7729c843cb7b16d48ef7cab74bab5de0080b2e0e46c341c2aa7f705dc8084f8
BLAKE2b-256 checksum
How to use checksums
84a26db7b976f7086c97eb3da8e3d5b5db116c9f8a879e22789cf2b5f28c569a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page