Write C++ once, bind everywhere — automatically.
Generate Python, Lua, and JavaScript bindings from C++ using C++26 reflection. Zero boilerplate, zero runtime overhead, zero binding code.
C++26 reflection ships in stock GCC 16.1+ (
g++ -std=c++26 -freflection); Bloomberg's clang-p2996 also works. The CLI auto-detects whichever is present. Reflection was voted into C++26 in June 2025 — this is standard C++, not a fork-only experiment.
struct Calculator {
double value = 0.0;
double add(double x) { return value += x; }
double subtract(double x) { return value -= x; }
};mirror_bridge generate src/ --module calc --lang allThat command is the entire binding process — no .def(), no macros in your class, no wrapper files. The same Calculator is now native in all three languages:
Python
import calc
c = calc.Calculator()
c.add(10)
print(c.value) # 10.0Lua
package.cpath = "build/lua/?.so;" .. package.cpath
local calc = require("calc")
local c = calc.Calculator()
c:add(10)
print(c.value) -- 10.0JavaScript (Node.js)
const calc = require('./build/calc.node');
const c = new calc.Calculator();
c.add(10);
console.log(c.value); // 10.0We ported Open3D's geometry module — the same API surface that takes 25,262 lines of hand-written pybind11 upstream — with 71 generated lines and one command (full case study, runnable example):
cd examples/open3d-port
mirror_bridge generate src/ --module open3d_geometry --lang all -I /usr/include/eigen3And it's not a slower convenience layer: on the identical C++ compiled with identical flags, ingesting a 1M-point cloud from Python (list of [x,y,z] → std::vector<Eigen::Vector3d>) runs 42x faster than the pybind11 binding (12.2ms vs 509ms), because the reflection-generated converter is specialized at compile time instead of dispatching through generic type casters (fair-comparison methodology).
Open3D is a case we worked, so on its own it proves only that it can be done. To make the claim falsifiable instead, CI runs the same one-line command every week against a corpus of unmodified third-party libraries and publishes the result, failures included: the compatibility matrix.
Today that page is mostly a work queue, which is the reason to publish it. The discovery half holds up — reflection reads the headers and reports the real class list without anyone naming a class by hand, 23 in jsoncpp, 41 in yaml-cpp, 108 in spdlog — and the matrix then says, per library, exactly what stops a module coming out, grouped so the most valuable thing to fix is the line with the most libraries on it. pybind11 and nanobind have nothing to put in that first column, because the class list is the thing their user writes.
No installation required — run in your browser or with Docker:
Interactive Playground - Godbolt examples showing C++26 reflection in action
GitHub Codespaces (browser-based, ~2 min):
Docker (local, one command):
docker run -it --rm -v $(pwd):/workspace ghcr.io/franciscothiesen/mirror_bridge:latestThen inside the container:
cd examples/01-hello-world
../../tools/mirror_bridge generate src/ --module greeter --lang python
python3 test_greeter.py# 1. Get the environment
./start_dev_container.sh # Choose option 1 for pre-built image
# 2. Verify
./tests/run_all_tests.sh
# 3. Try an example
cd examples/01-hello-world
../../tools/mirror_bridge generate src/ --module greeter --lang python
python3 test_greeter.py| Language | Status | Auto-Discovery |
|---|---|---|
| Python | Stable | --lang python |
| Lua | Stable | --lang lua |
| JavaScript (Node.js) | Stable | --lang js |
| Rust | Prototype | Code generation |
| Feature | Python | Lua | JavaScript |
|---|---|---|---|
| Data Members | ✅ | ✅ | ✅ |
| Methods (any arity) | ✅ | ✅ | ✅ |
| Constructors | ✅ | ✅ | ✅ |
| Containers (vector/array) | ✅ | ✅ | ✅ |
| Maps (map/unordered_map) | ✅ | ✅ | ✅ |
| Sets (set/unordered_set) | ✅ | ✅ | ✅ |
| Tuples (tuple/pair) | ✅ | ✅ | ✅ |
| Variants (std::variant) | ✅ | ✅ | ✅ |
| Nested Objects | ✅ | ✅ | ✅ |
Iteration, len(), in |
✅ | ❌ | ❌ |
Pickling / copy.deepcopy |
✅ | ❌ | ❌ |
| Enums | ✅ | ||
| Inheritance | ✅ | ✅ | ✅ |
| Method Overloading | ✅ | ||
| Smart Pointers | |||
| Exception Handling | ✅ | ✅ | ✅ |
| Cross-Module Types | ✅ | ❌ | ❌ |
| Zero-Copy Buffers | ✅ | ❌ | ❌ |
| Async/Await | ✅ | ❌ | ✅ |
| std::optional | ✅ | ✅ | ✅ |
| std::expected | ✅ | ✅ | ✅ |
✅ Full support
✅ Python data model: a class whose begin()/end() form a range over
convertible elements iterates and answers in; add size() and it gains
len(), and an empty one is falsy. A default-constructible class whose members
are all assignable pickles and deep-copies, with the member list taken from
reflection so it cannot fall behind the C++ struct; one that cannot be
reconstructed — a const or [[=readonly{}]] member, a private or
[[=exclude{}]] one the state cannot see, or a member a base class declares —
keeps refusing to pickle rather than silently dropping a field.
hash() is not generated, and what that leaves differs by class. A class with
operator== is unhashable: CPython inherits tp_hash from object only
when it also inherits tp_richcompare, so defining comparison and no hash
makes set, dict and in over a set raise
TypeError: unhashable type: 'mod.Point'. Use a list, or key on a member. A
class without operator== keeps the identity hash, so two equal-looking
objects remain two distinct keys. Hashing the member values instead would be
worse: every non-const member is assignable from Python, and a hash that
moves corrupts any set or dict already holding the object.
enum.IntEnum
carrying the C++ enumerator names and values, so Color.Red exists and
list(Color) works. Nothing about the value conversion changed, because an
IntEnum member is an int. Lua and JavaScript have no enum object to map
onto and their values still cross as bare numbers; generate says which
backend does which. An out-of-range integer is still accepted where an enum
is expected, in every backend.
std::shared_ptr binds, and a
shared_ptr return gives you a usable object. What does not cross is the
shared ownership: the return is a copy of the pointee, and a shared_ptr
parameter is refused. Tracked in
#54.
- Precompiled headers: 3-6x faster builds
- Auto-discovery: Zero binding code required
- Templates without hints (Python):
generatebinds every specialization the headers name (using Vec3f = Vector3<float>;is the declaration), instantiates function templates over the types the headers use, and lets the compiler reject what does not exist. Python sees families:geom.Vector3[float],geom.Vector3(1.0, 2.0, 3.0),geom.clamp(1.5, 0.0, 1.0),v.cast[int](). The plan is written next to the module — details - Type stubs:
.pyigeneration for Python IDE support
| Topic | Description |
|---|---|
| Getting Started | Quick start, installation, first binding |
| Guides | Workflow, PCH, multi-language, independent teams, contributing |
| Reference | CLI, API, configuration, type conversion |
| Architecture | System design and internals |
| Examples | Progressive examples from hello-world to production |
# Auto-discover and compile
mirror_bridge generate src/ --module my_mod --lang python
# With precompiled header (faster)
mirror_bridge pch --output build/ --type release
mirror_bridge generate src/ --module my_mod --lang python --pch
# Multiple languages
mirror_bridge generate src/ --module my_mod --lang allFrom the CI-regenerated benchmark table (same C++ class, same compiler, -O3 for every framework; only the binding layer differs — absolute times shift with runner hardware, compare the ratios):
| Operation | Mirror Bridge | pybind11 | nanobind | vs pybind11 |
|---|---|---|---|---|
| Method dispatch (null call) | 73ns | 190ns | 70ns | 2.6x faster |
| Object construction | 89ns | 376ns | 111ns | 4.2x faster |
| Vector return | 316ns | 914ns | 2,650ns | 2.9x faster (8.4x vs nanobind) |
Point-cloud ingest (1M × [x,y,z] → vector<Eigen::Vector3d>) |
12.2ms | 509ms | — | 42x faster |
| Binding code | 0 lines | 18+ lines/class | 18+ lines/class | Zero boilerplate |
Honest summary: 2-4x faster than pybind11 across the board, parity with nanobind on scalar dispatch (nanobind wins some by ~10%), and far ahead of both on container traffic. Compile time is ~3.4x faster than pybind11 (567ms vs 1,938ms, no PCH for either); nanobind compiles faster still — Mirror Bridge only matches it with --pch (194ms). The full table is regenerated monthly by CI so it tracks the current code, with methodology and SWIG numbers included. The same run now gates the ratios against committed floors, so a regression fails the build instead of sitting in a table for a month — and it reports every operation where a competitor is still faster rather than rounding that away.
mirror_bridge is built to be driven by coding agents as well as humans:
- AGENTS.md: operating manual for agents working in this repo
--jsoneverywhere:generate,diff, anddoctoremit exactly one JSON object on stdout, with actionable suggestions on failure- MCP server: gives any MCP-capable agent
generate_bindings,doctor, andcheck_binding_driftas tool calls. Until the PyPI release lands, build it locally (cd packaging/pip && ./build_wheel.sh, thenpip install "$(ls dist/*.whl)[mcp]"andclaude mcp add mirror_bridge -- mirror-bridge-mcp) - Claude Code skill: drop-in
bind-cppskill teaching the full generate → fix → verify loop - llms.txt / llms-full.txt: docs index and full corpus for LLM consumption
mirror_bridge/
├── core/ # Language-agnostic reflection
├── python/ # Python C API bindings
├── lua/ # Lua C API bindings
├── javascript/ # Node.js N-API bindings
├── tools/mirror_bridge # Unified CLI
├── docs/ # Documentation
├── examples/ # Progressive examples
└── tests/ # Test suite
- Compiler: a C++26 reflection compiler — stock GCC 16+ (
-freflection) or Bloomberg clang-p2996. Both are provided via Docker; the CLI auto-detects which is installed (override withMB_CXX). - Python: 3.7+ | Lua: 5.4 | Node.js: 14+
Note: P3394 field annotations (
[[=exclude{}]],[[=readonly{}]]) work under both compilers, in the Python backend. The Lua and JavaScript backends do not read them yet: they bind every named member, so an annotated field is still exposed there.
See Contributing Guide for development setup and guidelines.
Apache License 2.0 — See LICENSE