nanobind

repository·master·Indexed 25 days ago

https://github.com/wjakob/nanobind

A tiny and efficient C++ library for creating C++/Python bindings. It focuses on minimizing compilation time, binary size, and runtime overhead compared to pybind11 and Cython. It supports the Python Stable ABI (Python 3.12+) and provides integration for Bazel and CMake, including tools for generating typing stubs via stubgen.

Tokens
59.9K
Snippets
154
Records
295
Agent score
86%

What's inside nanobind

  1. Overview of nanobind

    master

    nanobind is a lightweight C++ library designed to expose C++ types to Python and vice versa. It is intended as a high-performance alternative to Boost.Python and pybind11, using a near-identical syntax.

    Key advantages include:

    • Faster Compilation: Up to ~4× faster than pybind11.
    • Smaller Binaries: Up to ~5× smaller than pybind11 and 3-12× smaller than Cython.
    • Better Runtime Performance: Up to ~10× lower runtime overhead compared to pybind11.
    • Stable ABI Support: Can target the Python Stable ABI (starting with Python 3.12), which reduces the need to ship per-Python version plugins.
  2. Introduction to nanobind

    master
    nanobind is a lightweight C++ library designed to expose C++ types to Python and vice versa. It is designed as a more efficient alternative to pybind11 and Boost.Python, offering faster compilation times, smaller binary sizes, and lower runtime overhead. It uses a syntax very similar to pybind11.
  3. Compare nanobind performance with other binding libraries

    master

    nanobind is designed for high efficiency in C++/Python bindings. Compared to other common tools, it offers the following advantages:

    • Compilation Time: Up to ~4× faster than pybind11 and 1.6-4.4× faster than Cython.
    • Binary Size: Produces significantly smaller binaries, with ~5× improvement over pybind11 and 3-12× improvement over Cython.
    • Runtime Performance: Offers ~3× lower overhead for simple functions and ~10× lower overhead when passing classes compared to pybind11. It also outperforms cppyy by ~1.6-2.1×.

    Note that while cppyy uses dynamic JIT compilation and is highly flexible, nanobind produces self-contained, static bindings that are easy to redistribute via PyPI using tools like cibuildwheel or scikit-build.

  4. Understand nanobind performance improvements

    master

    nanobind is designed for high efficiency through several architectural optimizations:

    • Compact Objects: C++ objects are co-located with Python objects, reducing per-instance overhead from ~56 bytes (pybind11) to ~24 bytes.
    • Compact Functions & Types: Binding information is co-located with Python function and type objects to reduce pointer chasing and hash table lookups.
    • Fast Hash Table: Uses tsl::robin_map instead of std::unordered_map for internal associative structures.
    • Vector Calls: Utilizes PEP 590 vector calls for faster function dispatch without heap allocation.
    • Library Component: Unlike header-only libraries, nanobind uses a precompiled support library (libnanobind) to avoid redundant compilation of dispatch loops.
    • Smaller Headers: Minimizes STL usage. Type casters for basic types (e.g., std::string) require explicit opt-in via specific headers like #include <nanobind/stl/string.h>.
    • Free-threading Support: Optimized for Python 3.13+ (no GIL) using a localized locking scheme for better multi-core scaling.
    • Lifetime Management: Efficient internal structures allow bound types to avoid being weak-referenceable, saving one pointer per instance.
  5. Understand Immortalization in nanobind

    master

    To avoid reference counting bottlenecks in multi-threaded programs, nanobind uses immortalization for functions (nanobind.nb_func, nanobind.nb_method) and type bindings.

    Immortal objects do not require reference counting, which reduces contention across processor cores. The trade-off is that these objects leak when the interpreter shuts down. Consequently, free-threaded nanobind extensions disable the internal leak checker to prevent false positive warning messages.

  6. Create generic types with type variables

    master

    To implement Python-style generic types (e.g., Wrapper[T]) in C++, follow these steps:

    1. Create a Type Variable: Use nb::type_var("T") and assign it to a module attribute.
    2. Enable Generics: Pass nb::is_generic() to the nb::class_<T> constructor. This adds __class_getattr__ to allow syntax like Wrapper[int] at runtime.
    3. Declare Base Class in Stubs: Since C++ classes cannot inherit from Python types, use nb::sig("class Wrapper(typing.Generic[T])") to tell static type checkers that the class derives from typing.Generic[T].
    4. Use Type Variables in Methods: Use the type variable name in nb::sig for constructors and methods to enable inference.
    #include <nanobind/typing.h>
    
    struct Wrapper {
        nb::object value;
    };
    
    NB_MODULE(my_ext, m) {
        // 1. Instantiate placeholder
        m.attr("T") = nb::type_var("T");
    
        // 2. Create generic type and lie to stubs about Generic[T] inheritance
        nb::class_<Wrapper> wrapper(m, "Wrapper", nb::is_generic(),
                                   nb::sig("class Wrapper(typing.Generic[T])"))
            .def(nb::init<nb::object>(),
                 nb::sig("def __init__(self, arg: T, /) -> None"))
            .def("get", [](Wrapper &w) { return w.value; },
                 nb::sig("def get(self, /) -> T"));
    }
  7. Create a Python extension module with NB_MODULE

    master

    Use the NB_MODULE(name, variable) macro to define the entry point for your Python extension.

    • name: The name of the module (unquoted). This must match the name provided to nanobind_add_module() in your CMake configuration.
    • variable: A variable of type nanobind::module_ that you will populate with your bindings.

    Example usage:

    NB_MODULE(example, m) {
        m.doc() = "Example module";
    
        // Add bindings here
        m.def("add", []() {
            return "Hello, World!";
        });
    }
    NB_MODULE(example, m) {
        m.doc() = "Example module";
    
        // Add bindings here
        m.def("add", []() {
            return "Hello, World!";
        });
    }
  8. Use the nb::ndarray class for n-dimensional arrays

    master

    nanobind provides the nb::ndarray<...> class to exchange n-dimensional arrays with frameworks like NumPy, PyTorch, TensorFlow, JAX, CuPy, and MLX using zero-copy exchange via the buffer protocol or DLPack.

    To use nd-arrays, include the following header:

    #include <nanobind/ndarray.h>
    #include <nanobind/ndarray.h>
  9. Bind C++ function templates

    master

    C++ function templates cannot be bound directly because they are generic and must be instantiated with concrete types at compile time. To expose them to Python, you must bind each specific instantiation separately, either by providing the same name (creating overloads) or by using unique names.

    // Option 1: Overloading the same name
    m.def("process", &process<int>);
    m.def("process", &process<std::string>);
    
    // Option 2: Using distinct names
    m.def("process_int", &process<int>);
    m.def("process_string", &process<std::string>);
    m.def("process", &process<int>);
    m.def("process", &process<std::string>);
  10. Build extensions for free-threaded Python

    master

    To build for free-threaded CPython (3.13+), you must first define an eligible toolchain in MODULE.bazel using rules_python, and then pass the @rules_python//python/config_settings:py_freethreaded flag set to yes during the build.

    # MODULE.bazel setup
    bazel_dep(name = "rules_python", version = "1.0.0")
    
    python = use_extension("@rules_python//python/extensions:python.bzl", "python")
    python.toolchain(python_version = "3.13")
    # Build command
    bazel build //path/to:my_ext --@rules_python//python/config_settings:py_freethreaded=yes
  11. Manage shared pointers and holders in nanobind

    master

    nanobind does not use holder types (like std::shared_ptr<T>) in the class declaration. Instead, instance data is stored within the PyObject itself, or a small wrapper stores a pointer to the data.

    Key changes from pybind11:

    • Do not specify holder types in nb::class_<T>.
    • To bind functions that exchange std::shared_ptr or std::unique_ptr, include the appropriate STL headers.
    • If using std::enable_shared_from_this<T>, ensure you pass objects across the Python/C++ boundary as std::shared_ptr<T> rather than raw T* to avoid issues with uninitialized shared pointers.
    • To prevent destruction of instances (replacing py::nodelete), use never_destruct when binding the class.