--- name: cpp-bindings description: >- Use when writing Python bindings for C++ HEP code with nanobind or pybind11: exposing a C++ class or function to Python, binding STL containers (vector, map), handling numpy/awkward array interop, choosing between nanobind and pybind11, or setting up a CMake build that produces a Python extension module. --- # C++ Python Bindings (nanobind / pybind11) ## Overview nanobind and pybind11 are C++ header-only libraries for creating Python extension modules from C++ code. pybind11 is the established standard; nanobind is a leaner, faster successor from the same author (Wenzel Jakob), offering smaller binary size and faster compile times. (Current nanobind requires Python 3.9+; older releases supported 3.8.) In HEP, bindings are used to expose ROOT-based code, custom reconstruction algorithms, or legacy C++ analysis code to Python without rewriting. ## When to Use - Exposing a C++ class or function to Python without rewriting it - Binding STL containers (`std::vector`, `std::map`) or Eigen arrays to numpy - Wrapping ROOT-based reconstruction algorithms or legacy C++ analysis code - Choosing between nanobind (faster compile, leaner) and pybind11 (broader ecosystem) for a new project - Setting up a CMake build that produces an importable Python extension module ## Key Concepts | Feature | nanobind | pybind11 | | ------------- | -------------------------------- | -------------------- | | Compile time | Faster (30–50%) | Slower | | Binary size | Smaller | Larger | | STL casters | Opt-in per-type header | Bulk `stl.h` header | | ABI isolation | Versioned ABI + domain isolation | No version isolation | | Maturity | Newer (2022+) | Very mature (2015+) | | Documentation | Growing | Comprehensive | **Recommendation**: Use **nanobind** for new projects. Use **pybind11** when integrating with existing pybind11-based code or when you need the larger ecosystem of pybind11 extensions. ## Canonical Patterns ### CMake setup (nanobind) ```cmake cmake_minimum_required(VERSION 3.15) project(myhep) find_package(Python 3.9 COMPONENTS Interpreter Development.Module REQUIRED) find_package(nanobind CONFIG REQUIRED) nanobind_add_module(myhep_ext myhep.cpp) target_link_libraries(myhep_ext PRIVATE MyHEPLib) ``` ### CMake setup (pybind11) ```cmake find_package(pybind11 CONFIG REQUIRED) pybind11_add_module(myhep_ext myhep.cpp) target_link_libraries(myhep_ext PRIVATE MyHEPLib) ``` ### Bind a simple function (nanobind) ```cpp // myhep.cpp #include namespace nb = nanobind; double invariant_mass(double px, double py, double pz, double e) { double m2 = e*e - px*px - py*py - pz*pz; return m2 > 0.0 ? std::sqrt(m2) : 0.0; } NB_MODULE(myhep_ext, m) { m.def("invariant_mass", &invariant_mass, nb::arg("px"), nb::arg("py"), nb::arg("pz"), nb::arg("e"), "Compute invariant mass from four-momentum (GeV)."); } ``` ```python import myhep_ext m = myhep_ext.invariant_mass(0.0, 0.0, 0.0, 125.0) # → 125.0 GeV ``` ### Bind a class (nanobind) ```cpp #include #include namespace nb = nanobind; class Jet { public: Jet(double pt, double eta, double phi) : pt_(pt), eta_(eta), phi_(phi) {} double pt() const { return pt_; } double eta() const { return eta_; } std::string repr() const { return "Jet(pt=" + std::to_string(pt_) + ")"; } private: double pt_, eta_, phi_; }; NB_MODULE(myhep_ext, m) { nb::class_(m, "Jet") .def(nb::init(), nb::arg("pt"), nb::arg("eta"), nb::arg("phi")) .def("pt", &Jet::pt) .def("eta", &Jet::eta) .def("__repr__", &Jet::repr); } ``` ### numpy array interop (nanobind) ```cpp #include #include namespace nb = nanobind; using Array1D = nb::ndarray, nb::c_contig>; nb::ndarray, nb::c_contig> compute_pt(Array1D px, Array1D py) { size_t n = px.shape(0); auto result = new double[n]; for (size_t i = 0; i < n; ++i) result[i] = std::sqrt(px(i)*px(i) + py(i)*py(i)); nb::capsule owner(result, [](void *p) noexcept { delete[] (double*)p; }); return nb::ndarray, nb::c_contig>(result, {n}, owner); } NB_MODULE(myhep_ext, m) { m.def("compute_pt", &compute_pt); } ``` ### STL containers (pybind11) ```cpp #include #include // auto-converts std::vector ↔ list namespace py = pybind11; std::vector jet_pts(std::vector px, std::vector py) { std::vector result; for (size_t i = 0; i < px.size(); ++i) result.push_back(std::sqrt(px[i]*px[i] + py[i]*py[i])); return result; } PYBIND11_MODULE(myhep_ext, m) { m.def("jet_pts", &jet_pts); } ``` ## Gotchas - **GIL (Global Interpreter Lock)**: For CPU-bound C++ functions that don't touch Python objects, release the GIL with `nb::gil_scoped_release` to allow parallel Python threads. - **Lifetime management**: Python's garbage collector doesn't know about C++ object lifetimes. Use `nb::keep_alive` or `py::keep_alive` policies for objects that hold references. - **STL opaque types in nanobind**: nanobind does NOT auto-convert `std::vector` to Python lists by default (unlike pybind11). Include `` to enable conversion. - **ROOT dictionary conflicts**: When linking against ROOT, ROOT's auto-generated dictionaries may conflict with pybind11/nanobind. Use a separate shared library to isolate ROOT from the Python extension. - **ABI mismatch**: The Python extension must be compiled against the same Python version and ABI as the interpreter. Use `scikit-build-core` or `meson-python` to manage this. ## Interop - **numpy**: `` (nanobind) or `` (pybind11) for zero-copy array passing. - **awkward**: awkward's `ak.from_buffers` / `ak.to_buffers` can wrap C++ arrays; combine with nanobind ndarray for zero-copy workflows. - **CMake + scikit-build-core**: The recommended build backend for Python extensions that use CMake. ## Docs nanobind: https://nanobind.readthedocs.io/ pybind11: https://pybind11.readthedocs.io/