pybind11

repository·master·Indexed 12 days ago

https://github.com/pybind/pybind11

A lightweight, header-only C++ library that enables seamless interoperability between C++11 and Python. It allows developers to expose C++ classes, functions, and data structures to Python with minimal boilerplate using compile-time introspection. Features include automatic conversions for std::chrono and Eigen, as well as support for custom type casters.

Tokens
63.9K
Snippets
183
Records
240
Agent score
96%

What's inside pybind11

  1. Overview of pybind11

    master

    pybind11 is a lightweight, header-only C++ library designed to expose C++ types in Python and vice versa. It is primarily used to create Python bindings for existing C++ codebases.

    Key characteristics include:

    • Header-only: No need to link against additional libraries; everything is contained in a few header files.
    • Minimal Boilerplate: Uses compile-time introspection to infer type information, reducing the amount of manual binding code required.
    • Lightweight: A compact implementation that depends only on the C++ standard library and a Python implementation (CPython 3.9+, PyPy, or GraalPy).
    • Efficiency: Uses C++11 move semantics and constexpr for precomputed function signatures to produce smaller binaries and faster execution.
  2. Benchmark pybind11 against Boost.Python

    master

    The pybind11 documentation provides a synthetic benchmark comparing its compilation time and module size against Boost.Python. The benchmark uses a Python script (docs/benchmark.py) to generate C++ files containing an increasing number of dummy classes (from 1 to 2048) with randomly generated method signatures.

    Key findings from the benchmark:

    • Compilation Time: pybind11 is generally faster due to including fewer headers. For a large-scale test (2048 classes, 8192 methods), pybind11 was approximately 1.2x faster than Boost.Python.
    • Module Size: pybind11 produces significantly smaller binaries. For the largest test case, the Boost.Python binary was 2.17 times (9.1 MiB) larger than the pybind11 output.
  3. Avoiding deadlocks in concurrent C++ code

    master

    Deadlocks occur when multiple threads attempt to acquire multiple mutexes in different orders. To prevent this, ensure that all threads acquire mutexes in the same, fixed order.

    If the mutexes involved are determined at runtime (e.g., locking different rows in a database), use a deadlock avoidance algorithm. In C++, std::lock provides such an algorithm, which typically uses non-blocking try_lock operations to avoid circular dependencies.

    Additionally, be aware of mutex types:

    • Recursive mutexes: Allow the same thread to lock the mutex multiple times (requires balanced unlocking).
    • Non-recursive mutexes: More efficient, but locking a mutex already held by the same thread results in undefined behavior.
  4. Handle inheritance and virtual functions with trampoline classes

    master

    When exposing C++ classes with virtual methods to Python, you must use a trampoline class to allow Python to override those methods.

    Key Requirements:

    1. Override every virtual method: For every method you want Python to be able to override, the trampoline class must provide an override using PYBIND11_OVERRIDE or PYBIND11_OVERRIDE_PURE.
    2. Inheritance chain: If you have a hierarchy (e.g., Animal -> Dog -> Husky), every level in the hierarchy that is registered with pybind11 requires its own trampoline class, even if that specific level doesn't introduce new virtual methods. This is because the trampoline is needed to bridge the virtual dispatch for the entire chain.
    3. Trailing commas: When using PYBIND11_OVERRIDE for functions with no arguments, you must include a trailing comma (e.g., PYBIND11_OVERRIDE(type, Class, name, )) to ensure portable implementation.

    Optimization: Template Trampolines

    To avoid duplicating override logic across multiple trampoline classes in a deep hierarchy, you can use template trampoline classes. This allows you to define the override logic once in a base template and reuse it for derived classes.

    // Example of a template trampoline approach
    template <class AnimalBase = Animal>
    class PyAnimal : public AnimalBase, public py::trampoline_self_life_support {
    public:
        using AnimalBase::AnimalBase;
        std::string go(int n_times) override { PYBIND11_OVERRIDE_PURE(std::string, AnimalBase, go, n_times); }
        std::string name() override { PYBIND11_OVERRIDE(std::string, AnimalBase, name, ); }
    };
    
    template <class DogBase = Dog>
    class PyDog : public PyAnimal<DogBase>, public py::trampoline_self_life_support {
    public:
        using PyAnimal<DogBase>::PyAnimal;
        std::string go(int n_times) override { PYBIND11_OVERRIDE(std::string, DogBase, go, n_times); }
        std::string bark() override { PYBIND11_OVERRIDE(std::string, DogBase, bark, ); }
    };
    
    // Registration
    py::class_<Animal, PyAnimal<>, py::smart_holder> animal(m, "Animal");
    py::class_<Dog, Animal, PyDog<>, py::smart_holder> dog(m, "Dog");
  5. Use NOPYTHON mode for complete control

    master

    By setting PYBIND11_NOPYTHON, you can completely disable Python integration within pybind11. This is useful when you want to integrate pybind11 into an existing system (like Scikit-Build) and want to manage Python discovery and linking manually.

    In this mode:

    • pybind11_add_module and pybind11_extension are unavailable.
    • pybind11 targets will not contain any Python-specific behavior.
  6. Local vs Global Exception Translators

    master

    Understanding the scope of exception translators is critical for consistent behavior across multiple modules:

    • Global Translators (py::register_exception_translator): Applied across all modules in the Python session. They are applied in reverse order of registration. If multiple modules register global translators for the same exception, the module imported last will have its translator applied first (it 'wins').
    • Local Translators (py::register_local_exception_translator): Applied only to the module they are defined in.

    Best Practice: If you are developing multiple pybind11 modules that share exception types and you need consistent, predictable error handling, use local translators to avoid import-order dependency issues.

  7. Performance considerations for C++ <-> Python callbacks

    master

    Passing functions between C++ and Python involves instantiating wrapper code to translate invocations. This adds computational overhead.

    Performance Warning: Frequent roundtrips (e.g., C++ calling Python, which then calls C++, repeatedly) can significantly decrease performance due to the accumulation of wrapper overhead.

    Optimization: Pybind11 can optimize calls involving stateless functions (function pointers or lambdas without captured variables). If a stateless function is passed as an argument to another C++ function exposed in Python, pybind11 extracts the underlying C++ function pointer to avoid the C++ $\leftrightarrow$ Python roundtrip overhead.

  8. Update Enum and Hash behavior in v2.6

    master

    Several changes were made to how Enums and Hashing work in v2.6:

    • Enum __str__: Enums now have a pre-defined __str__ method. To override it, add the py::prepend() tag when defining "__str__".
    • Hashing: If __eq__ is defined but __hash__ is not, __hash__ is now set to None (matching CPython behavior). To make a class hashable, add __hash__ (consider using the py::hash shortcut).
  9. Understand default and manual smart pointer holders

    master

    When defining a class with py::class_<T>, you can specify a holder type to manage object references. Note that a single type T (and its derivatives) can only use one holder type.

    1. std::unique_ptr<T> (The Default)

    If no holder is provided, py::class_<T> defaults to std::unique_ptr<T>.

    • Pros: Works as expected for most simple cases.
    • Cons: Does not support passing a std::unique_ptr from Python back to C++ (e.g., as a function argument). It also involves a reinterpret_cast for base/derived classes which is technically undefined behavior.

    2. std::shared_ptr<T> (Manual)

    You can explicitly use std::shared_ptr:

    py::class_<Example, std::shared_ptr<Example>>(m, "Example");
    • Cons: Because only one holder can be used per type, if you choose std::shared_ptr, you cannot pass std::unique_ptr from C++ to Python. This can lead to runtime segmentation faults.

    Comparison Summary

    Featurestd::unique_ptr (Default)std::shared_ptrpy::smart_holder
    Two-way conversionLimitedLimitedFull
    SafetyModerateLower (risk of segfaults)High
  10. Use factory functions with virtual function trampolines

    master

    When using factory functions with classes that require a trampoline (alias class) for overriding virtual functions in Python, you have three strategies:

    1. Rvalue-reference constructor: Add a constructor to your alias class that takes a base value by rvalue-reference (Example &&base). pybind11 will use this to move the value returned by the factory into the alias instance.
    2. Dual factory functions: Provide two factory functions to .def(py::init(...)). The first is used when no alias is required (standard usage), and the second is used when an alias is required (when the class is inherited from in Python).
    3. Always return an alias: Specify a single factory function that always returns an alias instance (similar to py::init_alias<...>()).
    #include <pybind11/factory.h>
    class Example {
    public:
        virtual ~Example() = default;
    };
    class PyExample : public Example, public py::trampoline_self_life_support {
    public:
        using Example::Example;
        PyExample(Example &&base) : Example(std::move(base)) {}
    };
    
    py::class_<Example, PyExample, py::smart_holder>(m, "Example")
        // 1. Returns an Example pointer. If PyExample is needed, it's moved via the rvalue constructor.
        .def(py::init([]() { return new Example(); }))
        // 2. Two callbacks: first for no alias, second for when alias is needed
        .def(py::init([]() { return new Example(); },
                      []() { return new PyExample(); }))
        // 3. *Always* returns an alias instance
        .def(py::init([]() { return new PyExample(); }));