前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python & C++ - pybind11 实现解析

Python & C++ - pybind11 实现解析

作者头像
fangfang
发布2023-10-16 15:31:09
9550
发布2023-10-16 15:31:09
举报
文章被收录于专栏:方方的杂货铺方方的杂货铺

0. 导语

IEG 自研引擎 CE 最早支持的脚本是 Lua, 在性能方面, Lua是有一定优势的. 但除此之外的工程组织, 以及现在即将面临的 AI 时代的语料问题, Lua 都很难很好的解决. 在这种情况下, 支持工程组织和语料更丰富的 Python, 就成了优先级较高的任务了. 由于Python的虚拟机以及相关的C API较复杂, 我们选择的方式是将 pybind11 - 一个Python社区知名度比较高, 实现质量也比较高的 Python 导出库与我们引擎的 C++ 反射适配的整合方式, 这样可以在工作量较小的情况下, 支持好 Python 脚本, 同时也能比较好的利用上引擎的C++反射实现. 在做好整合工作前, 我们肯定需要先较深入的了解 pybind11 的相关实现机制, 这也是本篇主要讲述的内容.


1. 为什么 pybind11 这类中间件是必要的

我们以 UE 官方的 PythonScriptPlugin 中的代码为例, 如果直接依赖 Python C API, 你实现出来的代码可能是如下这样的:

代码语言:javascript
复制
// NOTE: _T = typing.TypeVar('_T') and Any/Type/Union/Mapping/Optional are defines by the Python typing module.
    static PyMethodDef PyMethods[] = {
        { PyGenUtil::PostInitFuncName, PyCFunctionCast(&FMethods::PostInit), METH_NOARGS, "_post_init(self) -> None -- called during Unreal object initialization (equivalent to PostInitProperties in C++)" },
        { "cast", PyCFunctionCast(&FMethods::Cast), METH_VARARGS | METH_CLASS, "cast(cls: Type[_T], object: object) -> _T -- cast the given object to this Unreal object type or raise an exception if the cast is not possible" },
        { "get_default_object", PyCFunctionCast(&FMethods::GetDefaultObject), METH_NOARGS | METH_CLASS, "get_default_object(cls: Type[_T]) -> _T -- get the Unreal class default object (CDO) of this type" },
        { "static_class", PyCFunctionCast(&FMethods::StaticClass), METH_NOARGS | METH_CLASS, "static_class(cls) -> Class -- get the Unreal class of this type" },
        { "get_class", PyCFunctionCast(&FMethods::GetClass), METH_NOARGS, "get_class(self) -> Class -- get the Unreal class of this instance" },
        { "get_outer", PyCFunctionCast(&FMethods::GetOuter), METH_NOARGS, "get_outer(self) -> Any -- get the outer object from this instance (if any)" },
        { "get_typed_outer", PyCFunctionCast(&FMethods::GetTypedOuter), METH_VARARGS, "get_typed_outer(self, type: Union[Class, type]) -> Any -- get the first outer object of the given type from this instance (if any)" },
        { "get_outermost", PyCFunctionCast(&FMethods::GetOutermost), METH_NOARGS, "get_outermost(self) -> Package -- get the outermost object (the package) from this instance" },
        { "is_package_external", PyCFunctionCast(&FMethods::IsPackageExternal), METH_NOARGS, "is_package_external(self) -> bool -- returns true if this instance has a different package than its outer's package" },
        { "get_package", PyCFunctionCast(&FMethods::GetPackage), METH_NOARGS, "get_package(self) -> Package -- get the package directly associated with this instance" },
        { "get_name", PyCFunctionCast(&FMethods::GetName), METH_NOARGS, "get_name(self) -> str -- get the name of this instance" },
        { "get_fname", PyCFunctionCast(&FMethods::GetFName), METH_NOARGS, "get_fname(self) -> Name -- get the name of this instance" },
        { "get_full_name", PyCFunctionCast(&FMethods::GetFullName), METH_NOARGS, "get_full_name(self) -> str -- get the full name (class name + full path) of this instance" },
        { "get_path_name", PyCFunctionCast(&FMethods::GetPathName), METH_NOARGS, "get_path_name(self) -> str -- get the path name of this instance" },
        { "get_world", PyCFunctionCast(&FMethods::GetWorld), METH_NOARGS, "get_world(self) -> Optional[World] -- get the world associated with this instance (if any)" },
        { "modify", PyCFunctionCast(&FMethods::Modify), METH_VARARGS, "modify(self, always_mark_dirty: bool = True) -> bool -- inform that this instance is about to be modified (tracks changes for undo/redo if transactional)" },
        { "rename", PyCFunctionCast(&FMethods::Rename), METH_VARARGS | METH_KEYWORDS, "rename(self, name: Union[Name, str]=\"None\", outer: Optional[Object]=None) -> bool -- rename this instance and/or change its outer" },
        { "get_editor_property", PyCFunctionCast(&FMethods::GetEditorProperty), METH_VARARGS | METH_KEYWORDS, "get_editor_property(self, name: str) -> object -- get the value of any property visible to the editor" },
        { "set_editor_property", PyCFunctionCast(&FMethods::SetEditorProperty), METH_VARARGS | METH_KEYWORDS, "set_editor_property(self, name: str, value: object, notify_mode: PropertyAccessChangeNotifyMode=PropertyAccessChangeNotifyMode.DEFAULT) -> None -- set the value of any property visible to the editor, ensuring that the pre/post change notifications are called" },
        { "set_editor_properties", PyCFunctionCast(&FMethods::SetEditorProperties), METH_VARARGS, "set_editor_properties(self, properties: Mapping[str, object]) -> None -- set the value of any properties visible to the editor (from a name->value dict), ensuring that the pre/post change notifications are called" },
        { "call_method", PyCFunctionCast(&FMethods::CallMethod), METH_VARARGS | METH_KEYWORDS, "call_method(self, name: str, *args: Any, **kwargs: Mapping[str, object]) -> Any -- call a method on this object via Unreal reflection using the given ordered (tuple) or named (dict) argument data - allows calling methods that don't have Python glue" },
        { nullptr, nullptr, 0, nullptr }
    };

    PyTypeObject PyType = {
        PyVarObject_HEAD_INIT(nullptr, 0)
        "_ObjectBase", /* tp_name */
        sizeof(FPyWrapperObject), /* tp_basicsize */
    };

    PyType.tp_base = &PyWrapperBaseType;
    PyType.tp_new = (newfunc)&FFuncs::New;
    PyType.tp_dealloc = (destructor)&FFuncs::Dealloc;
    PyType.tp_init = (initproc)&FFuncs::Init;
    PyType.tp_str = (reprfunc)&FFuncs::Str;
    PyType.tp_repr = (reprfunc)&FFuncs::Str;
    PyType.tp_hash = (hashfunc)&FFuncs::Hash;

    PyType.tp_methods = PyMethods;

    PyType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
    PyType.tp_doc = "Type for all Unreal exposed object instances";

我们需要非常了解 Python C API, 并且这类代码的污染也比较严重, 为了导出相关功能函数, 你可能需要写非常多的辅助代码. 而这些往往都是编译期已经可以获取的内容了, 而且编译期特性的使用也不会导致性能的下降.

这种情况下, 像 pybind11, boost.python 等中间件应运而生, 而 pybind11 对比实现复杂度和依赖都非常重的 boost.python, 显然更有优势, 功能实现和特性上 pybind11 也更占优, 落差从 GitHub上两个库的热度就能看出来了:

====2016年 pybind11 cppconn 演讲时的数据====

====到2023年4月, 本文写作的时间, 差距更大了====

下面让我们先从一个 pybind11 的示例开始, 逐步了解 pybind11 的设计实现.


1.1 pybind11 的简单使用

我们先通过一些测试代码来近距离的接触 pybind11, 这里我们以一个 3D 中常用的向量 Vector3 为例, Vector3 的声明如下:

代码语言:javascript
复制
namespace gbf {
namespace math {

class Vector3 {
 public:
  double x;
  double y;
  double z;

  Vector3() : x(0.0), y(0.0), z(0.0) {}
  Vector3(double _x, double _y, double _z) : x(_x), y(_y), z(_z) {}

  ~Vector3() {}

  // Returns the length (magnitude) of the vector.
  double Length() const;

  /// Extract the primary (dominant) axis from this direction vector
  const Vector3& PrimaryAxis() const;
};

}  // namespace math
}  // namespace gbf

我们利用 pybind11 可以很方便的将 Vector3 导出到 python 指定的模块 math3d 中:

代码语言:javascript
复制
// example 模块的初始化函数
PyObject *PyInit_math3d() {
  static pybind11::module_ math3d("math3d", "pybind11 example plugin");

  pybind11::class_<gbf::math::Vector3>(math3d, "Vector3")
      .def(pybind11::init<>())
      .def(pybind11::init<double, double, double>())
      .def("Length", &gbf::math::Vector3::Length)
      .def("PrimaryAxis", &gbf::math::Vector3::PrimaryAxis)
      .def_readwrite("x", &gbf::math::Vector3::x)
      .def_readwrite("y", &gbf::math::Vector3::y)
      .def_readwrite("z", &gbf::math::Vector3::z)
    ;
  return math3d.ptr();
}

这里我们直接尝试向 Python 注册了一个 math3d 模块, 并在这个模块中导出了一个 Vector3 的类(三维矢量的简单实现), 并导出了Vector3的属性和一些成员方法.

如果正确构建了测试环境, 以下代码:

代码语言:javascript
复制
from time import time,ctime
import math3d

a = math3d.Vector3(3, 4, 5)
print('vec:', a.x, a.y, a.z)
print('primary axis:', a.PrimaryAxis())
print(f"vec len: {a.Length()}")

我们就能如上所示在 Python 中正确的访问到 math3d 模块下的 Vector3 类了.

借助 pybind11 和 Python C API, 我们可以方便的在 C++ 中创建 Python 脚本环境, 这里给出运行环境创建的一种方式:

代码语言:javascript
复制
wchar_t libraryPath[] = L"../../../data/python/Lib";
  Py_SetPath(libraryPath);

  // 将 math3d 模块的初始化函数添加到内置模块表
  PyImport_AppendInittab("math3d", &PyInit_math3d);
/ 初始化 Python 解释器
  pybind11::scoped_interpreter guard{};

  PyRun_SimpleString(
 R"(# 此处插入测试用Python脚本)"
  );

此处是直接使用内置模块的方式注册的math3d, 很多时候我们是通过预处理的dll来提供模块给 Python 用的, 引擎很多内置类型我们可以更直接的通过上面这种方式, 在代码中直接注册内置模块, 导出相关功能给 Python脚本使用.


1.2 本节小结

本节中我们通过一个简单的示例了解了 pybind11 的基本使用方法, 从示例中我们也能看到, pybind11 提供了一种简洁的语法来定义模块和在模块中注册类和函数。模块本身是导出的起点, C++ 的类和函数的都依赖于某个模块导出到 Python 中, 如上例中的 math3d 模块.

那么 pybind11 是如何实现 C++ <-> Python 交互的呢, 后面的章节中我们将逐步介绍实现相关机制的基础设施, 逐步分析 pybind11 的核心实现机制.


2. pybind11 对 Python 对象的支持

Python 本身有丰富的类型系统, pybind11 也在 C++ 中对 Python 的对象体系进行了相关的抽象, 方便在 C++ 中直接操作 Python 虚拟机的上对象.


2.1 对象体系概述

Python 中的内置类型比较丰富, 所以要完成对整个 Python 对象体系在 C++ 中的还原, 也是一件较复杂的事情, pybind11 是层次化的完成这个目标的, 整体的 pybind11 对象图如下所示:

我们也使用层次化的方式对这些实现做进一步的说明:


2.1.1 pyobject_tag

该类的存在目的是为了在 C++ 更方便的区分 pybind11 实现的 Python 对象类型和非 Python 对象类型, 利用模板: 位于 pytypes.h 中:

代码语言:javascript
复制
template <typename T>
using is_pyobject = std::is_base_of<pyobject_tag, remove_reference_t<T>>;

我们编译期就能快速的对图上定义的诸多类型进行判定, 从而对诸多 Python 对象类型使用正确的方式进行处理.


2.1.2 detail::object_api<Derived>

作用如类名, 提供对 Python 对象的统一 API外观, 部分接口定义如下: 位于 pytypes.h 中:

代码语言:javascript
复制
/** \rst
    A mixin class which adds common functions to `handle`, `object` and various accessors.
    The only requirement for `Derived` is to implement ``PyObject *Derived::ptr() const``.
\endrst */
template <typename Derived>
class object_api : public pyobject_tag {
    const Derived &derived() const { return static_cast<const Derived &>(*this); }

public:
    iterator begin() const;
    iterator end() const;

    item_accessor operator[](handle key) const;
    item_accessor operator[](object &&key) const;
    item_accessor operator[](const char *key) const;

    obj_attr_accessor attr(handle key) const;
    obj_attr_accessor attr(object &&key) const;
    str_attr_accessor attr(const char *key) const;

    /// Check if the given item is contained within this object, i.e. ``item in obj``.
    template <typename T>
    bool contains(T &&item) const;

    template <return_value_policy policy = return_value_policy::automatic_reference,
              typename... Args>
    object operator()(Args &&...args) const;

    /// Equivalent to ``obj is other`` in Python.
    bool is(object_api const &other) const { return derived().ptr() == other.derived().ptr(); }
    /// Equivalent to ``obj is None`` in Python.
    bool is_none() const { return derived().ptr() == Py_None; }
    /// Equivalent to obj == other in Python

    object operator-() const;
    object operator~() const;
    object operator+(object_api const &other) const;
    object operator+=(object_api const &other);
    object operator-(object_api const &other) const;
    object operator-=(object_api const &other);
    object operator*(object_api const &other) const;
    //... 

    PYBIND11_DEPRECATED("Use py::str(obj) instead")
    pybind11::str str() const;

    /// Get or set the object's docstring, i.e. ``obj.__doc__``.
    str_attr_accessor doc() const;

    /// Return the object's current reference count
    int ref_count() const { return static_cast<int>(Py_REFCNT(derived().ptr())); }

    handle get_type() const;
};

我们依赖这种设施可以在 C++ 中方便的访问 Python 对象, 如直接利用operator() 来完成对对象__call__方法的调用, attr()查询对应 Python 对象的属性, str() 获取字符描述等.

[!tip] 另外我们注意到该类有一个Derived模板参数, 这实际上是一种 C++ 中称作: CRTP - curiously recurring template pattern 的特殊定制方法, 通过该方法, 我们可以以纯静态的方式在父类中对子类进行访问, 高性能的完成部分依赖虚表和继承才能完成的特性.


2.1.3 handle

Python 本身的 GC 实现比较特殊, 区别于大多语言使用的方式, 除了依赖 GC 对生命周期进行管理外, 也依赖引用计数的方式对对象的生命周期进行管理, 所以在 pybind11 中也不可避免的需要对 Python 对象的引用计数进行管理 , 这部分功能主要是由 pybind11::handle 来完成的. 其中提供了: - inc_ref(), dec_ref()方法 -> 用来控制当前 Python 对象的引用计数 - ptr()方法 ->方便我们直接获取原生的PyObject对象. 在处理函数的 C++ 参数传入传出处理的时候, pybind11 很多情况下是直接使用 handle 来完成相关功能的.


2.1.4 object

大部分 Python 对象的 C++ 抽象都使用它来作为基类, 继承自 handle 的 object, 除了提供了 handle 相关的能力外, 也额外扩展了cast() 方法, 以及后面介绍的依赖其实现的 reinterpret_borrow, reinterpret_steal方法, 图中左侧大多是 Python 专有的类型, 右侧则大多是能够简单转换为 C++类型的Python类型, 以及额外的用于对C++指针进行管理的 capsule 类型, 这些都继承自 object, 每个从 object 继承的类都有贴合自身实现的类型检查机制, 这样保证我们不容易使用错误的类型对 Python 中的对象进行操作, 具体每个类型的作用这里不一一展开描述了, 下面再具体介绍一下 pybind11 中控制 Python 对象生命周期的辅助设施.


2.1.5 detail::generic_typeclass_

generic_type 继承的 class_ 用于表达 C++ 类的各种信息, 上面示例中也可以看到: 位于 pybind11.h 中:

代码语言:javascript
复制
pybind11::class_<gbf::math::Vector3>(example, "Vector3")
  .def(pybind11::init<>())
  .def(pybind11::init<double, double, double>())
  .def("Length", &gbf::math::Vector3::Length)
  .def("PrimaryAxis", &gbf::math::Vector3::PrimaryAxis)
  .def_readwrite("x", &gbf::math::Vector3::x)
  .def_readwrite("y", &gbf::math::Vector3::y)
  .def_readwrite("z", &gbf::math::Vector3::z)
;

我们通过 class_ 来注册 C++类, 它的构造函数, 成员函数, 成员变量等到 Python 中, class_ 最后会在 Python 中创建一个 PyTypeObject, 并关联 C++ 类处理需要的各种函数, 如创建对象中调用的init_instance, 析构时调用的 dealloc 等, 通过 class_ 以及内部关联的 PyTypeObject 和其上的各种定制函数, C++ 类和对象也就能被 Python 识别和使用了, 具体的细节我们在第3章中详细展开.


2.1.6 capsule

Python 中有一个对自定义指针进行管理的类型, 允许我们传入一个 const void* 的指针, 以及一个对该指针数据进行的析构函数, 构造一个 PyCapsule 类型的对象, 在 PyCapsule 需要被 GC 时, 就会自动调用我们传入的析构函数对相关数据进行析构. class_ 实现部分并未采用这种方式, 但这种轻量易用的封装方式在很多场合都能够很好的工作, 比如 tensor 中的代码: 位于 torch.h 中:

代码语言:javascript
复制
src = Helper::alloc(std::move(*src));
parent_object = capsule(src, [](void *ptr) { Helper::free(reinterpret_cast<Type *>(ptr)); });

这种类型对于一些需要关联到 Python 中的自定义数据来说, 是非常友好的. 我们在阅读 pybind11 源码时也会发现 capsule 的使用.


2.2 生命周期控制的辅助设施

reinterpret_steal<>reinterpret_borrow<> 是 Pybind11 中的两个辅助函数,用于方便我们直接在 C++ 中用非 Python C API 的相对高级的方式直接操作 Python 对象, 其中 reinterpret_steal<> 会改变持有的 Python 对象的引用计数, 而 reinterpret_borrow<> 则不会.

[!note] 注意 pybind11 的 borrow 对引用计数的处理是通过object创建时引用计数+1, 销毁时引用计数-1, 来达成的不改变原始引用计数, 而不是我们想象中的不变, 所以我们应该尽量结合栈对象使用 reinterpret_steal<>reinterpret_borrow<>, 避免预期外的引用计数改变带来的 Side Effect.


2.2.1 reinterpret_borrow<>

reinterpret_borrow<> 用于从原始 PyObject* 类型创建一个 pybind11::object,而不影响引用计数(注意这里是通过构建时引用计数 +1, 析构时引用计数 -1 来达成的)。这个函数常用于将已经持有引用计数的原始 Python 对象转换为 Pybind11 的 object 类型, 方便我们使用 pybind11 提供的一系列简单易用的接口。例如下面示例: cpp PyObject* raw_obj = ...; // 已经持有引用计数的 Python 对象 py::object obj = py::reinterpret_borrow<py::object>(raw_obj);

这里,raw_obj 是一个原始 Python 对象。通过使用 reinterpret_borrow<>,我们可以将其转换为 pybind11 的 object 类型, obj 脱离作用域后, 原始的 raw_obj 的引用计数会被还原到一开始的状态, 从而实现了一个对 Python 对象的间接式的 borrow


2.2.2 reinterpret_steal<>

reinterpret_steal<> 用于从原始 PyObject* 类型创建一个 pybind11::object,同时会接管对应 PyObject 的引用计数管理, 在对应的 pybind11::object 释放时, 相关的 PyObject 引用计数会 -1. 这个是与 borrow 有差异的地方. reinterpret_steal<> 执行的情况下, 对应的的 pybind11::object 对象脱离作用域后, Python 对象的引用计数实际上被执行了 -1 的操作.


2.2.3 Python对象支持小结

pybind11 通过提供一系列更简单易用的 Python 内置类型抽象, 在其中提供更简单易用的接口, 以及添加基础的引用计数自动处理的机制, 再结合像 reinterpret_borrow<> 以及 rinterpret_steal<> 之类的辅助设施, 让我们可以在不使用 Python C API的情况下, 也能简单高效的完成对 Python 各种基础对象的操作, 同时也为更进一步的 C++ 类导出提供了良好的底层支撑.


3. pybind11 对 C++ 类的支持

前面我们介绍了 pybind11 对 Python 对象的支持, 有了这部分能力, 我们就能基于它更容易的实现 pybind11 的核心功能 -- 将 C++ 类导出至 Python 使用. pybind11 支持 C++ 类导出到 Python 的机制我们可以通过下图简单概括:

要完成对 C++ 类的导出功能, pybind11 主要实现了两部分的核心功能: 1. Register过程: 利用C++的编译期特性, 我们在类型注册的时候, 完成 C++ 类型到 Python 类型的转换, 并且可以在Python中按名称索引对应的成员函数和属性. 这部分实现直接利用了前面一章中介绍的 pybind11::class_, 相关实现会在注册的过程中对所有的 C++ 函数和属性的 get/set 方法将完成类型擦除, 相关信息会被统一转移到类型 pybind11::cpp_function 中. 2. Runtime相关: Runtime的时候, 我们会需要在 Python对象 <-> C++对象中实现互转, 具体这部分功能由图中的两个类来完成, 在 pybind11 中, 所有的 C++ 类对象会被类型擦除到 pybind11::detail::instance 上, 我们需要注意 pybind11 这里的处理比较特殊, instance 负责对象的存储, 而图上的pybind11::detail::value_and_holder 负责完成 instance 对象的使用, 这种设计是因为对应的 instance 对象, 可能存在基类, 而很多时候我们将某个 C++ 对象当成它的基类来处理, 显然也是合法的, pybind11 需要自己处理这部分 C++ 反射相关的特性, 所以此处的设计稍显复杂, 本文我们主要关注流程和实现, 像这些非常细节的部分我们不会过多的展开. 我们接下来将分成两个小节分别展开 Register 部分和 Runtime相关的部分.


3.1 Register - C++ 类注册部分

我们先来了解 Register (注册)相关的核心类 class_: 位于 pybind11.h 中:

代码语言:javascript
复制
template <typename type_, typename... options>
class class_ {
 public:
  template <typename... Extra>
  class_(handle scope, const char *name, const Extra &...extra) {
        using namespace detail;

        type_record record;
        record.scope = scope;
        record.name = name;
        record.type = &typeid(type);
        record.type_size = sizeof(conditional_t<has_alias, type_alias, type>);
        record.type_align = alignof(conditional_t<has_alias, type_alias, type> &);
        record.holder_size = sizeof(holder_type);
        record.init_instance = init_instance;
        record.dealloc = dealloc;
        record.default_holder = detail::is_instantiation<std::unique_ptr, holder_type>::value;

        set_operator_new<type>(&record);

        /* Register base classes specified via template arguments to class_, if any */
        PYBIND11_EXPAND_SIDE_EFFECTS(add_base<options>(record));

        /* Process optional arguments, if any */
        process_attributes<Extra...>::init(extra..., &record);

        generic_type::initialize(record);

        if (has_alias) {
            auto &instances = record.module_local ? get_local_internals().registered_types_cpp
                                                  : get_internals().registered_types_cpp;
            instances[std::type_index(typeid(type_alias))]
                = instances[std::type_index(typeid(type))];
        }

  }
};

pybind11 通过 pybind11::class_<type_, ...options>(scope, name, ...) 模板类的构造函数完成对一个 C++ 类型的注册, 其中类的模板参数: type_ -> 指定要导出的主类型, 如前例中的 Vector3. options -> 为主类型指定一系列的别名.

其中构造函数的参数: scope -> 父级模块对应的句柄, 如前例中的 math3d 模块. name -> 对应 C++ 类导出到模块中的名称, 如前例中的名称 "Vector3". ...extra -> 额外的配置参数

其中主要完成的操作包括: 1. type_record 信息填充, 其中包括scope, name, type - c++ 类型(目前使用 typeid() 实现), 以及几个操作函数的指针, init_instance, dealloc, operator_new. type_record 实际就是 pybind11 中用于记录的 C++ 类型相关的元信息的对象了. 2. Extra信息的处理(暂不展开) 3. 利用刚刚填充好的type_record调用generic_type::initialize(), 这个函数执行的操作主要是以下这些: a. make_new_python_type() 为C++类注册新的 Python 类型 b. 生成 C++ 类型对应的detail::type_info, 并存入registered_types_cppregistered_types_py 中, 两者分别对应 keystd::type_index 以及 PyTypeObject 的情况, 我们可以在运行时根据不同类型的 key 查询存储好的 detail::type_info. c. tb_base 的处理 d. 对 local 成员进行额外的处理, 以使外部成员可以正确的访问它. 4. 注册额外的alias type信息(暂不展开)

[!info] C++ 类型的查询是通过 RTTI 来实现的, 通过对具体类型调用 typeid(CxxClassType) 得到一个 std::type_info 类型的对象, 再通过这个对象构造支持哈希和比较的std::type_index, 我们就能将对应类型间接转换出支持查询的 std::type_index 了, 在没有完整实现 c++ 反射的地方, 这是一种很稳妥的对 c++ 类型进行查询处理的方式.

与 Python 虚拟机交互的注册主要发生在3a, 3b, 3c 中, 我们具体来看一下相关的实现:


3.1.1 make_new_python_type() - 3a 部分实现

位于 class.h 中:

代码语言:javascript
复制
auto *metaclass
        = rec.metaclass.ptr() ? (PyTypeObject *) rec.metaclass.ptr() : internals.default_metaclass;

    auto *heap_type = (PyHeapTypeObject *) metaclass->tp_alloc(metaclass, 0);
    heap_type->ht_name = name.release().ptr();
#ifdef PYBIND11_BUILTIN_QUALNAME
    heap_type->ht_qualname = qualname.inc_ref().ptr();
#endif
    auto *type = &heap_type->ht_type;

整体流程比较简单, 利用先前创建好的 metaclass - 一般是预创建的 pybind11-type 类型, 创建对应的 PyHeapTypeObject, 并从中获取真正我们需要用到的PyTypeObject类型, 进行对PyTypeObject对象信息的填充. 最后我们当然还需要将生成的类型向对应的模块注册:

代码语言:javascript
复制
setattr(rec.scope, rec.name, (PyObject *) type);

3.1.2 detail::type_info的生成和注册 - 3b 部分实现

位于 pybind11.h - generic_type::initialize() 中:

代码语言:javascript
复制
auto *tinfo = new detail::type_info();
        tinfo->type = (PyTypeObject *) m_ptr;
        tinfo->cpptype = rec.type;
        //...

        auto &internals = get_internals();
        auto tindex = std::type_index(*rec.type);
        internals.registered_types_cpp[tindex] = tinfo;
        internals.registered_types_py[(PyTypeObject *) m_ptr] = {tinfo};

这部分的实现也比较简单, 创建 detail::type_info 并正确关联 Python 类型后, 分别向对应的 cpp 字典以及 py 字典注册该类型, 这样我们就通过相关的PyTypeObject 或者相关的 C++ 类型实现从 Python 侧或者 C++ 侧的双向查询了.


3.1.3 tb_base 的处理 - 3c 部分的实现

可能有细心的读者发现了, 上面我们创建的 Python 类型, 并没有对对象的构造和析构等做详细的处理, 虽然该部分我们会在后续的 Runtime 讲述部分做具体的展开, 不过我们先要了解信息是如何正确的关联的: 位于 class.h - make_new_python_type() 中:

代码语言:javascript
复制
auto *base = (bases.empty()) ? internals.instance_base : bases[0].ptr();
// ... Some code ignore here
type->tp_base = type_incref((PyTypeObject *) base);

而没有父类声明的情况下, 通过阅读代码可知, internals.instance_base 被填充的信息是:

代码语言:javascript
复制
internals_ptr->instance_base = make_object_base_type(internals_ptr->default_metaclass);

这个地方的类型级联比较复杂, 可以参考下图:

pybind11 使用层次化的结构解决类型之间的依赖关系, 不同的类型一般设置的自定义方法是不一样的. 主要的类型有以下几个: - internals::default_meta_class: pybind11 最基础的类型, 像 tp_call, tp_setattro, tp_getattro 等自定义方法是在此处绑定的. - internals::instance_base: C++ 对象的基础类型, 从上面的 default_meta_class 继承, 并设置了 tp_new, tp_init, tp_dealloc 用于管理 C++ 对象的分配, 构造以及释放 - root_classsub_class: 这两者都是在上面的 pybind11::class_ 构造时处理的, 区别是存在父类的情况, 子类的 tp_base 会被指定到父类创建的PyTypeObject, 否则我们将以 internals::instance_base 作为 tp_base.

通过这种层级式的类型设计, pybind11 就能特定层处理特定事务的方式, 为解决好 C++ 类型在 Python 虚拟机中的表达提供一个基础支持了. 大部分情况我们关注从 internals::instance_base 开始的实现即能比较清楚的了解 pybind11 对 C++ 对象的处理机制了.

[!info] 通过对相关代码的分析, 我们可以看到 pybind11 主要是通过 C++本身的 RTTI 特性支持来完成的 C++ 类型到 pybind11 内部维护的类型的映射. 但我们知道首先 RTTI 实现的功能较薄弱, 其次相关的设施会存在额外的运行时开销, 在 C++17 特性下, 我们一般会选择对应的编译期实现来代替相关的RTTI typeid() 以及 typeindex, 一方面是避免对 C++ 薄弱的 RTTI 的特性的依赖, 另外也是更多的利用编译期特性来提高整体实现的性能.


3.2 Register - C++ 函数注册部分

要完成 C++ 函数到 Python 的注册, 我们需要对 C++ 函数进行类型擦除, pybind11 的实现大致如下图所示:

我们一般通过使用 class_::def() 来注册相关的 C++ 函数, 如上面提到的示例:

代码语言:javascript
复制
pybind11::class_<gbf::math::Vector3>(example, "Vector3")
  .def("Length", &gbf::math::Vector3::Length);

def() 是函数注册的入口, 对于所有注册的函数, 我们调用 def() 会得到一个统一类型的 cpp_function 对象, 而其中的静态成员函数 cpp_function::dispatcher() 则是我们类型擦除的目标, 最终我们将类型已经是 PyCFunctiondispatcher() 注册到 Python 虚拟机中, 完成整个注册过程.

dispatcher() 就是一个标准的 PyCFunction, 其声明如下: 位于 pybind11.h 中:

代码语言:javascript
复制
/// Main dispatch logic for calls to functions bound using pybind11
    static PyObject *dispatcher(PyObject *self, PyObject *args_in, PyObject *kwargs_in);

接下来我们具体展开一下 dispatcher() 向 Python 的注册过程.


3.2.1 dispatcher() 向 Python 的注册

pybind11 默认支持函数的 overload, 所以注册过程也是分为两种情况: - 注册时暂无同名函数注册 -> 全新的函数注册过程 - 注册时已经存在同名函数 -> 添加新的调用到已经存在的函数调用链上 接下来我们分别来看一下这两种情况对应的实现.

全新的函数注册过程 位于 pybind11.h - cpp_function::initialize_generic() 中:

代码语言:javascript
复制
/* No existing overload was found, create a new function object */
            rec->def = new PyMethodDef();
            std::memset(rec->def, 0, sizeof(PyMethodDef));
            rec->def->ml_name = rec->name;
            rec->def->ml_meth
                = reinterpret_cast<PyCFunction>(reinterpret_cast<void (*)()>(dispatcher));
            rec->def->ml_flags = METH_VARARGS | METH_KEYWORDS;

            capsule rec_capsule(unique_rec.release(),
                                [](void *ptr) { destruct((detail::function_record *) ptr); });
            rec_capsule.set_name(detail::get_function_record_capsule_name());
            guarded_strdup.release();

            object scope_module;
            if (rec->scope) {
                if (hasattr(rec->scope, "__module__")) {
                    scope_module = rec->scope.attr("__module__");
                } else if (hasattr(rec->scope, "__name__")) {
                    scope_module = rec->scope.attr("__name__");
                }
            }

            m_ptr = PyCFunction_NewEx(rec->def, rec_capsule.ptr(), scope_module.ptr());
            if (!m_ptr) {
                pybind11_fail("cpp_function::cpp_function(): Could not allocate function object");
            }

我们看到通过 rec->def->ml_meth, 我们将 cpp_function::dispacher() 绑定到了通过 PyCFunction_NexEx 创建的 PyObject 上, 这样 Python 虚拟机就能正确的对对应的C++函数进行访问了. 如果该函数是 C++类的成员函数, 那么我们还需要额外的add_class_method()将创建的 Python 函数对象与我们创建的 Python c++ class 类型关联:

代码语言:javascript
复制
inline void add_class_method(object &cls, const char *name_, const cpp_function &cf) {
    cls.attr(cf.name()) = cf;
    //...
}

这样, 我们就能正确的在 Python 中正确的访问类的成员函数了.

添加调用到函数调用链: 这种情况就不需要再创建 Python 函数对象了, 我们正确的存储相关的C++函数并且正确的生成对应的signature, 在后续 dispatcher() 执行的时候能够的找到对应的 overload 版本, 就能正确的匹配相关的 C++ 函数并执行了.


3.3 Register - C++ ctor()注册部分

pybind11 的 cpp_function 机制比较万能, ctor 本身最后也是被转换为一个 cpp_function 进行存储和使用的. 当然, 如我们所熟知的, ctor最后是被关联到了 __init__ 对应的成员名上. 位于 pybind11.h 中:

代码语言:javascript
复制
// Implementing class for py::init<...>()
template <typename... Args>
struct constructor {
  template <typename Class, typename... Extra, enable_if_t<!Class::has_alias, int> = 0>
  static void execute(Class &cl, const Extra &...extra) {
    cl.def(
        "__init__", [](value_and_holder &v_h, Args... args) { v_h.value_ptr() = construct_or_initialize<Cpp<Class>>(std::forward<Args>(args)...); },
        is_new_style_constructor(), extra...);
  }


template <typename... Args, typename... Extra>
class_ &def(const detail::initimpl::constructor<Args...> &init, const Extra &...extra) {
    PYBIND11_WORKAROUND_INCORRECT_MSVC_C4100(init);
    init.execute(*this, extra...);
    return *this;
}

通过这种实现, 在导出类的时候, 通过以下代码:

代码语言:javascript
复制
pybind11::class_<gbf::math::Vector3>(example, "Vector3")
      .def(pybind11::init<>())
      .def(pybind11::init<double, double, double>());

最终的效果就是对应的.def()声明被转换到了对__init__函数的注册, 而当对象执行__init__的时候, 调用的是construct_or_initialize<Cpp<Class>>(), 这里面其实最终是根据类是否可构造调用的不同版本的 new 实现: 位于 init.h 中:

代码语言:javascript
复制
template <typename Class, typename... Args, detail::enable_if_t<std::is_constructible<Class, Args...>::value, int> = 0>
inline Class *construct_or_initialize(Args &&...args) {
  return new Class(std::forward<Args>(args)...);
}
template <typename Class, typename... Args, detail::enable_if_t<!std::is_constructible<Class, Args...>::value, int> = 0>
inline Class *construct_or_initialize(Args &&...args) {
  return new Class{std::forward<Args>(args)...};
}

虽然代码层面有多次的跳转, 但这些代码在 Release 的情况应该都是能被优化的, 这种利用已有设施扩展新特性的方式, 也不失为一种有效的实现机制. 这种将 ctor() 转义为函数调用的方式, 特定场景下也有比较强的实用性.


3.4 Register - C++ 成员变量注册部分

同ctor(), pybind11 对属性的处理最终也是通过cpp_function来实现的: 位于 pybind11.h 中:

代码语言:javascript
复制
template <typename C, typename D, typename... Extra>
    class_ &def_readwrite(const char *name, D C::*pm, const Extra &...extra) {
        static_assert(std::is_same<C, type>::value || std::is_base_of<C, type>::value,
                      "def_readwrite() requires a class member (or base class member)");
        cpp_function fget([pm](const type &c) -> const D & { return c.*pm; }, is_method(*this)),
            fset([pm](type &c, const D &value) { c.*pm = value; }, is_method(*this));
        def_property(name, fget, fset, return_value_policy::reference_internal, extra...);
        return *this;
    }

具体的Property实现使用了PyPropertyType, 借助该类型本身的机制:

代码语言:javascript
复制
// rec_func must be set for either fget or fset.
    void def_property_static_impl(const char *name,
                                  handle fget,
                                  handle fset,
                                  detail::function_record *rec_func) {
        const auto is_static = (rec_func != nullptr) && !(rec_func->is_method && rec_func->scope);
        const auto has_doc = (rec_func != nullptr) && (rec_func->doc != nullptr)
                             && pybind11::options::show_user_defined_docstrings();
        auto property = handle(
            (PyObject *) (is_static ? get_internals().static_property_type : &PyProperty_Type));
        attr(name) = property(fget.ptr() ? fget : none(),
                              fset.ptr() ? fset : none(),
                              /*deleter*/ none(),
                              pybind11::str(has_doc ? rec_func->doc : ""));
    }

我们可以通过该类型的 __call__ 方法很方便的将 c++版的 get/set 方法与对应的 PyPropertyType 的 get/set 方法绑定.


3.5 Runtime - C++ 类对象的创建和销毁

前面我们介绍了类对象注册相关的部分, 本节我们将展开 Runtime 部分的实现, Runtime 部分也是整体导出实现的最后一环, 最终将 C++ 对象和 Python 对象关联到了一起.


3.5.1 pybind11::detail::instance

在 Python 虚拟机中, 所有的 C++ UDT 对象, 不管是不是由 Python 创建持有的, 都将表达为一个 pybind11::detail::instance 对象, 所有的 C++ UDT对象都会被类型擦除到 instance, 能够想象的, 这个对象在需要的时候能够还原为原始的 C++ 对象并操作, 所以我们在其中需要给它关联足够的 C++ 类型信息. 我们先来看一下 instance 的定义: 位于 common.h 中

代码语言:javascript
复制
/// The 'instance' type which needs to be standard layout (need to be able to use 'offsetof')
struct instance {
    PyObject_HEAD;
    /// Storage for pointers and holder; see simple_layout, below, for a description
    union {
      void *simple_value_holder[1 + instance_simple_holder_in_ptrs()];
      nonsimple_values_and_holders nonsimple;
    };
    /// Weak references
    PyObject *weakrefs;
    /// If true, the pointer is owned which means we're free to manage it with a holder.
    bool owned : 1;
    // ... some members ignore here
};

我们需要注意的是以下几点: - PyObject_HEAD宏: 该类需要被 Python 虚拟机直接使用, 需要包含该宏形成GC对象链表. - union: 一个data holder设计, simple_value 和能够被缓冲区直接装下的对象使用第一个值, 其它情况使用第二个值. - weakrefs: 弱引用字段, 通过构建类型时的tp_weaklistoffset 告知Python 虚拟机对应弱引用的偏移. - owned: 标识对象是否被 Python 虚拟机直接所有, 这种情况下 Python 虚拟机需要负责析构和释放对应对象.

细心的读者可能有疑问了, 我们似乎在上面的代码中并没有看到 C++ 类型信息存储的字段? 这是因为我们的 instance 始终是由 Python 虚拟机负责创建并填充额外信息的, 我们始终可以通过 Py_TYPE(this_instance) 来获取对应的 PyTypeObject, 然后从这个我们为每个 C++ UDT 类型唯一创建的 PyTypeObject 上就可以查询到所有我们需要的信息了.


3.5.2 pybind11::detail::value_and_holder

上面的instance其实是不利于使用的, 首先它关联的 C++ 对象存储的位置可能是 union 中的一项, 另外类型信息需要额外的调用才能准确获取, 所以 pybind11 在使用上包装了一个 value_and_holder 类用来解决便利性的问题, 对应的定义如下: 位于 type_caster_base.h 中:

代码语言:javascript
复制
struct value_and_holder {
    instance *inst = nullptr;
    size_t index = 0u;
    const detail::type_info *type = nullptr;
    void **vh = nullptr;
    //... some thing ignore here.
};

我们可以看到所有类对象的相关信息都已经被存储为 value_and_holder 的成员变量了. 此处我们并不想过多展开 C++ 反射相关的细节, 所以对应的 instance -> value_and_holder的处理代码, 以及处理对象基础类型衍生出的 values_and_holders 我们就不一一展开了.


3.5.3 C++ 类在 Python 中的类型

我们先来回顾一下前面提到过的 pybind11 对 C++ 对象的类型的处理机制:

类对象的创建和销毁都涉及到了上图中的 internals::instance_base 类型, 我们先来看一下创建该类型的 make_object_base_type() 函数的实现: 位于 class.h 中:

代码语言:javascript
复制
/** Create the type which can be used as a common base for all classes.  This is
    needed in order to satisfy Python's requirements for multiple inheritance.
    Return value: New reference. */
inline PyObject *make_object_base_type(PyTypeObject *metaclass) {
    constexpr auto *name = "pybind11_object";
    auto name_obj = reinterpret_steal<object>(PYBIND11_FROM_STRING(name));

    /* Danger zone: from now (and until PyType_Ready), make sure to
       issue no Python C API calls which could potentially invoke the
       garbage collector (the GC will call type_traverse(), which will in
       turn find the newly constructed type in an invalid state) */
    auto *heap_type = (PyHeapTypeObject *) metaclass->tp_alloc(metaclass, 0);
    if (!heap_type) {
        pybind11_fail("make_object_base_type(): error allocating type!");
    }

    heap_type->ht_name = name_obj.inc_ref().ptr();
#ifdef PYBIND11_BUILTIN_QUALNAME
    heap_type->ht_qualname = name_obj.inc_ref().ptr();
#endif

    auto *type = &heap_type->ht_type;
    type->tp_name = name;
    type->tp_base = type_incref(&PyBaseObject_Type);
    type->tp_basicsize = static_cast<ssize_t>(sizeof(instance));
    type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE;

    type->tp_new = pybind11_object_new;
    type->tp_init = pybind11_object_init;
    type->tp_dealloc = pybind11_object_dealloc;

    /* Support weak references (needed for the keep_alive feature) */
    type->tp_weaklistoffset = offsetof(instance, weakrefs);

    if (PyType_Ready(type) < 0) {
        pybind11_fail("PyType_Ready failed in make_object_base_type(): " + error_string());
    }

    setattr((PyObject *) type, "__module__", str("pybind11_builtins"));
    PYBIND11_SET_OLDPY_QUALNAME(type, name_obj);

    assert(!PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));
    return (PyObject *) heap_type;
}

通过 instance_base 上的这些自定义行为方法的映射:

代码语言:javascript
复制
//对象构造相关的 new 和 init 调用
type->tp_new = pybind11_object_new;
type->tp_init = pybind11_object_init;

//对象析构相关的调用
type->tp_dealloc = pybind11_object_dealloc;

诸如 C++ 对象创建, 以及 C++ 对象销毁的操作, 通过这些 instance_base 上关联的自定义方法, 都能很好实现了.


3.5.4 类对象的创建

当我们尝试在Python中构造一个 C++ 对象时, 如上例中的:

代码语言:javascript
复制
math3d.Vector3(3, 4, 5)

与 Lua 类似, Lua 是通过 __call 这个元方法直接完成封装的, Python 此处的实现会稍微复杂一点, 需要结合一部分 Python 源码才方便理解. pybind11中整个 C++ 对象的构建过程如下图所示:

首先, 我们触发的其实是 math3d.Vector3 这个类型的 __call__ 自定义方法, 而这个方法其实在default_meta_class 类型创建的时候被关联到了 pybind11_meta_call 这个函数上: 位于 class.h 中:

代码语言:javascript
复制
/// metaclass `__call__` function that is used to create all pybind11 objects.
extern "C" inline PyObject *pybind11_meta_call(PyObject *type, PyObject *args, PyObject *kwargs) {

    // use the default metaclass call to create/initialize the object
    PyObject *self = PyType_Type.tp_call(type, args, kwargs);
    // ... 
    retrun self;
}

真正负责对象构建的地方发生在 Python 源码部分, PyType_Type.tp_call() 调用最后会调用到 typeobject.c 中的 type_call() 函数: 位于 typeobject.c 中:

代码语言:javascript
复制
static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    //... some code ignore here
    obj = type->tp_new(type, args, kwds);
    obj = _Py_CheckFunctionResult(tstate, (PyObject*)type, obj, NULL);
    if (obj == NULL)
        return NULL;

    /* If the returned object is not an instance of type,
       it won't be initialized. */
    if (!PyType_IsSubtype(Py_TYPE(obj), type))
        return obj;

    type = Py_TYPE(obj);
    if (type->tp_init != NULL) {
        int res = type->tp_init(obj, args, kwds);
        if (res < 0) {
            assert(_PyErr_Occurred(tstate));
            Py_DECREF(obj);
            obj = NULL;
        }
        else {
            assert(!_PyErr_Occurred(tstate));
        }
    }
    return obj;
}

我们会看到其中调用到的 type->tp_new() 以及 type->tp_init(), 最终两者被关联到的是我们上面提到的 instance_base 类型创建时绑定的 pybind11_object_new__init__, 这与 c++ 对象创建的逻辑基本一致, 都是先分配对象空间, 再调用对象的构造函数, 只是中间有部分代码跟 Python源码相关, 理解起来会复杂一些, 相关的自定义方法代码的实现这里也直接给出: pybind11_object_new() - 位于 class.h 中:

代码语言:javascript
复制
extern "C" inline PyObject *pybind11_object_new(PyTypeObject *type, PyObject *, PyObject *) {
    return make_new_instance(type);
}

inline PyObject *make_new_instance(PyTypeObject *type) {
#if defined(PYPY_VERSION)
    // PyPy gets tp_basicsize wrong (issue 2482) under multiple inheritance when the first
    // inherited object is a plain Python type (i.e. not derived from an extension type).  Fix it.
    ssize_t instance_size = static_cast<ssize_t>(sizeof(instance));
    if (type->tp_basicsize < instance_size) {
        type->tp_basicsize = instance_size;
    }
#endif
    PyObject *self = type->tp_alloc(type, 0);
    auto *inst = reinterpret_cast<instance *>(self);
    // Allocate the value/holder internals:
    inst->allocate_layout();

    return self;
}

pybind11_object_new() 的主要作用是负责分配 PyObject 对象, 也就是我们上面介绍的 instance 对象.

__init__ 默认绑定的 pybind11_object_init(), 它的实现如下: pybind11_object_init() - 位于 class.h 中:

代码语言:javascript
复制
/// An `__init__` function constructs the C++ object. Users should provide at least one
/// of these using `py::init` or directly with `.def(__init__, ...)`. Otherwise, the
/// following default function will be used which simply throws an exception.
extern "C" inline int pybind11_object_init(PyObject *self, PyObject *, PyObject *) {
    PyTypeObject *type = Py_TYPE(self);
    std::string msg = get_fully_qualified_tp_name(type) + ": No constructor defined!";
    PyErr_SetString(PyExc_TypeError, msg.c_str());
    return -1;
}

它并不完成真正的 c++ 构造函数的调用, 仅作为一个 fallback, 在 C++ 构造函数匹配失败后被调用. 我们需要如前面构造函数注册提到的那样, 利用类型上注册的名为 __init__ 的函数, 来完成对象的构造.

[!note] instance 对象其实是被间接构造的, 我们告知 Python 的其实是 instance 类型的大小, 然后在 instance 的头部是 PyObject 对象 Head 宏, 所以其实我们只是拿一个内存布局与 Python 内部的 PyObject 对象完全一致的一个C++类 instance 来操作对应的内存块, 这里会比其它语言的相关实现绕一点, 侵入式比较强, 但明白了这一点就基本搞清了pybind11中 C++ 对象在 Python 中存在的形式, 以及为什么对 C++对象在 Python 中的创建是两个单独的函数处理后才完成的.


3.5.5 类对象的销毁

在前面提到的 C++ 对象在 Python 中关联的类型的创建相关的代码中:

代码语言:javascript
复制
//对象构造相关的 new 和 init 调用
type->tp_new = pybind11_object_new;
type->tp_init = pybind11_object_init;

//对象析构相关的调用
type->tp_dealloc = pybind11_object_dealloc;

我们为 tp_dealloc 字段绑定了 pybind11_object_dealloc 方法, 这样相关对象在触发GC回收的时候, 会调用pybind11_object_dealloc(), 而 C++ 对象的析构也是在其中完成的: 位于 class.h 中:

代码语言:javascript
复制
/// Instance destructor function for all pybind11 types. It calls `type_info.dealloc`
/// to destroy the C++ object itself, while the rest is Python bookkeeping.
extern "C" inline void pyra_object_dealloc(PyObject *self) {
    auto *type = Py_TYPE(self);

    // If this is a GC tracked object, untrack it first
    // Note that the track call is implicitly done by the
    // default tp_alloc, which we never override.
    if (PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC) != 0) {
        PyObject_GC_UnTrack(self);
    }

    clear_instance(self);
    type->tp_free(self);
    // ...
}

其中的 clear_instance(self) 调用负责完成 C++ 对象的析构调用, 相关的代码调用链比较长, 此处不再展开了, 我们可以简单了解, 最终是通过前面介绍的 value_and_holder 再调用到 C++ 类注册时 type_record 上绑定的 class_::dealloc() 完成的, 感兴趣的读者可以自行查阅相关代码.


3.6 Runtime - 函数调用

在运行时状态下, 在 Python 中调用对应的 C++ 函数, 入口都是前面注册部分我们提到的 cpp_function::dispatcher() 函数, 我们再通过 pybind11 的实现正确处理从 Python 传入的值, 完成其中对应的原始 C++ 函数的调用, 然后再通过 pybind11 的实现将返回值传递给 Python, 整个 Python 调用 C++函数的过程就完成了, 在下文类型转换相关的章节中我们会具体展开这部分的细节.


3.7 其它

除了上面说到的对 C++ 类的导出支持能力外, 利用 pybind11提供的设施:

代码语言:javascript
复制
class Animal { 
virtual std::string speak() const { return "I am an animal."; } 

class PyAnimal : public Animal { 
public: using Animal::Animal; 
std::string speak() const override { 
  PYBIND11_OVERRIDE_PURE( std::string, // Return type 
  Animal, // Parent class 
  speak // Method name ); } 
};

我们也能很好的实现在 Python 中 override C++ 类的功能. 另外, 通过 pybind11 对 Python 对象的封装, 我们通过直接在 C++ 中与 Python 对象交互, 也能很容易的实现出 C++ 中使用 Python 类的功能, 下面是简单的示例代码:

代码语言:javascript
复制
#pragma once
#include <string>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>

namespace py = pybind11;

class Derived {
public:
    Derived(const std::string &name) : name_(name) {
        py::module base_module = py::module::import("base");
        py::object base_class = base_module.attr("Base");
        py_instance_ = base_class();
    }

    const std::string &get_name() const {
        return name_;
    }

    std::string foo() {
        return py_instance_.attr("foo")().cast<std::string>();
    }

    std::string bar() {
        return py_instance_.attr("bar")().cast<std::string>();
    }

private:
    std::string name_;
    py::object py_instance_;
};

通过 C++ 内部持有一个 py::object 对象, 我们即可在需要的时候对内置的 Python 对象进行访问, 从而实现为一些核心的 Python 对象提供 C++ Wrapper 类的目的.


4. pybind11 中的类型转换系统

前面我们分别介绍了 pybind11 对 Python对象的封装, 以及对 C++对象的导出支持, 很多时候我们需要跨语言边界对各种不同的类型进行处理, 做 C++ 类型 <=> Python 类型相关的转换支持, 这部分功能是由 pybind11 中的类型转换系统来完成的, 下面我们来具体了解它的设计与实现.


4.1 type_caster

pybind11 是通过特化 pybind11::detail::type_caster<type> 来完成的对不同类型的支持. pybind11 对常规c++类型(UDT)的支持比较特殊, 不同于大部分 Traits 使用的默认实现对应的是空类型, 在 pybind11 中, 未特化处理到的类型, 即是 UDT类型, 也就是我们最开始看到的:

代码语言:javascript
复制
template <typename type, typename SFINAE = void>
class type_caster : public type_caster_base<type> {};

来完成对 UDT 类型的支持, 而其他类型使用特化版本的 type_caster<> 来支持. 我们先来看一下 type_caster<> 这个Traits的公共成员:

如上图所示, 一个特化的 type_caster<> 主要包括三部分实现: 1. py_type -> type_caster<>转换到的 C++ 类型 2. bool load(pybind11::handle src, bool convert) -> 从PyObject加载对应pt_type的 c++ 值. 3. pybind11::handle cast(U src, return_value_policy) -> 从 C++ 类型值转换到目标 PyObject 对象. 很多时候我们会有多个版本的cast() 重载以适应不同类型的C++值的情况, 比如对于数值类型, 可能存在double, int64_t, ...等一系列子类型, 需要我们进行特化处理.


4.2 builtin 类型的处理简述

相关的实现比较多, 我们仅列举其中一部分实现:

4.2.1 数值类型

位于 cast.h 中:

代码语言:javascript
复制
template <typename T>
struct type_caster<T, enable_if_t<std::is_arithmetic<T>::value && !is_std_char_type<T>::value>> {
};

具体的py_type实现, 以及对应的load(), cast()实现不详细展开了, 已知原始类型和目标类型的情况下, 相关的实现都比较好理解.

4.2.2 void, void_type, nullptr_t

位于 cast.h 中:

代码语言:javascript
复制
template <typename T>
struct void_caster {
public:
    bool load(handle src, bool) {
        if (src && src.is_none()) {
            return true;
        }
        return false;
    }
    static handle cast(T, return_value_policy /* policy */, handle /* parent */) {
        return none().release();
    }
    PYBIND11_TYPE_CASTER(T, const_name("None"));
};

template <>
class type_caster<void_type> : public void_caster<void_type> {};

template <>
class type_caster<std::nullptr_t> : public void_caster<std::nullptr_t> {};

其中比较特殊的是void, 因为 void* 本身是有意义的类型, 所以void的特化有处理void* <-> PyObject 之间的相互转换.

4.2.3 bool

位于 cast.h 中:

代码语言:javascript
复制
template <>
class type_caster<bool> {

4.2.4 字符串

位于 cast.h 中:

代码语言:javascript
复制
// Helper class for UTF-{8,16,32} C++ stl strings:
template <typename StringType, bool IsView = false>
struct string_caster {
//...
};

template <typename CharT, class Traits, class Allocator>
struct type_caster<std::basic_string<CharT, Traits, Allocator>,
                   enable_if_t<is_std_char_type<CharT>::value>>
    : string_caster<std::basic_string<CharT, Traits, Allocator>> {};

template <typename CharT, class Traits>
struct type_caster<std::basic_string_view<CharT, Traits>,
                   enable_if_t<is_std_char_type<CharT>::value>>
    : string_caster<std::basic_string_view<CharT, Traits>, true> {};

std::string 以及 std::string_view 都是继承string_caster来实现的, c-style string的实现也是借助一个内嵌的type_caster<:string>来完成的, 内置定义了一个StringCaster类型:

代码语言:javascript
复制
using StringType = std::basic_string<CharT>;
using StringCaster = make_caster<StringType>;

4.3 pyobject 类型的处理

前面我们介绍了, pybind11 定义了一些辅助类型用于直接关联 Python 的各种 PyObject 类型, 在一些特定的地方, 我们可能会直接对使用到相关类型的函数或者变量进行导出, 这就会存在这种 wrapper 类型本身跟 Python 进行交互的转换的需要, 而一般这种 wrapper 类型的转换肯定是特殊的, 所以这个地方, pybind11 也需要对handle, object等类型实现特化版本的type_caster: 位于 cast.h 中:

代码语言:javascript
复制
template <typename type>
struct pyobject_caster {
    template <typename T = type, enable_if_t<std::is_same<T, handle>::value, int> = 0>
    pyobject_caster() : value() {}

    // `type` may not be default constructible (e.g. frozenset, anyset).  Initializing `value`
    // to a nil handle is safe since it will only be accessed if `load` succeeds.
    template <typename T = type, enable_if_t<std::is_base_of<object, T>::value, int> = 0>
    pyobject_caster() : value(reinterpret_steal<type>(handle())) {}

    template <typename T = type, enable_if_t<std::is_same<T, handle>::value, int> = 0>
    bool load(handle src, bool /* convert */) {
        value = src;
        return static_cast<bool>(value);
    }

    template <typename T = type, enable_if_t<std::is_base_of<object, T>::value, int> = 0>
    bool load(handle src, bool /* convert */) {
        if (!isinstance<type>(src)) {
            return false;
        }
        value = reinterpret_borrow<type>(src);
        return true;
    }

    static handle cast(const handle &src, return_value_policy /* policy */, handle /* parent */) {
        return src.inc_ref();
    }
    PYBIND11_TYPE_CASTER(type, handle_type_name<type>::name);
};

template <typename T>
class type_caster<T, enable_if_t<is_pyobject<T>::value>> : public pyobject_caster<T> {};

通过 objects 相关的 type_caster<> 特化实现, 所有的 pybind11 定义的 pyobject 都能在 C++ <-> Python 间简单的进行转换了, 甚至在 C++ 函数中直接使用 pyobject 类型, pybind11 也能自动处理相关参数在 C++ <-> Python 间的传递, 这种抽象对于易用性的提高是非常有好处的.


4.4 常规 C++ 类的处理 - UDT 类型支持

前面我们也提到了, type_caster 的默认实现就是 常规 C++ 类对应的特化版本实现, 这个地方 pybind11 利用了 SFINAE 的正交特性, 也就是任何基于 SFINAE 支持的特化类型实现, 不能出现交集, 不能出现存在类型 A 和 B, 两者同时满足某个特化版本, 所以对于 pybind11 的 type_caster 实现来说, 如果我对其它类型都实现了特化模板, 剩下的默认匹配的模板自然就是唯一没有处理的 UDT 了: 位于 cast.h 中:

代码语言:javascript
复制
template <typename type, typename SFINAE = void>
class type_caster : public type_caster_base<type> {};

/// Generic type caster for objects stored on the heap
template <typename type>
class type_caster_base : public type_caster_generic{
  //...
};

从继承关系上, 我们也能看出, UDT 涉及的类, 有 type_caster_basetype_caster_generic, type_caster_base 的作用跟下面要介绍的intrinsic_type<> 模板类是直接相关的, 内部会记录擦除修饰符的原始类型itype, 在type_caster_base这一层处理带修饰符相关的类型逻辑, 而到type_caster_generic这部分, 就只需要处理不带任何修饰符的 UDT 相关的逻辑了. 我们直接聚焦 type_caster_generic 对 UDT 的处理.


4.4.1 load()实现

位于 type_cast_base.h - type_caster_generic 类中:

代码语言:javascript
复制
PYBIND11_NOINLINE bool load_impl(handle src, bool convert) {
    if (!src) {
        return false;
    }
    if (!typeinfo) {
        return try_load_foreign_module_local(src);
    }

    auto &this_ = static_cast<ThisT &>(*this);
    this_.check_holder_compat();

    PyTypeObject *srctype = Py_TYPE(src.ptr());

    // Case 1: If src is an exact type match for the target type then we can reinterpret_cast
    // the instance's value pointer to the target type:
    if (srctype == typeinfo->type) {
        this_.load_value(reinterpret_cast<instance *>(src.ptr())->get_value_and_holder());
        return true;
    }
    //...
}

这里为了表述的简便, 我们只给出了 需要load()到 C++ 中的 Python 对象本身的类型与使用场景预期的类型完全匹配的情况. 这种情况最后调用到了type_caster_generic::load_value(), 向 value_and_holder 对象进行填充, 具体的实现细节我们可以忽略, 此处我们只要知道 value_and_holder 是 pybind11 对所有 C++ UDT 对象做类型擦除的对象, 再去理解相关的实现, 就更容易理解了. 通过load()调用, 最后 Python 中保存的 C++ 对象, 对应的指针被存储在了 type_caster_genericvalue 成员上, 在C++函数调用等场合, 我们就能向相关的C++函数正确的传递从 Python 中传入的对应值了. 此处 value 是直接使用万能类型 void* 表达的, 因为相关的调用路径是严格限制的, 使用void*也不会导致什么问题, 只是相关代码的调试跟踪会变复杂. 其他一些像父子类的匹配等情形, pybind11 需要自己对相关的类型关系进行维护并在此处使用, 相关的代码此处不详细展开了.


4.4.2 cast()实现

位于 type_cast_base.h - type_caster_generic 类中:

代码语言:javascript
复制
PYBIND11_NOINLINE static handle cast(const void *_src,
                                         return_value_policy policy,
                                         handle parent,
                                         const detail::type_info *tinfo,
                                         void *(*copy_constructor)(const void *),
                                         void *(*move_constructor)(const void *),
                                         const void *existing_holder = nullptr) {
        if (!tinfo) { // no type info: error will be set already
            return handle();
        }

        void *src = const_cast<void *>(_src);
        if (src == nullptr) {
            return none().release();
        }

        if (handle registered_inst = find_registered_python_instance(src, tinfo)) {
            return registered_inst;
        }

        auto inst = reinterpret_steal<object>(make_new_instance(tinfo->type));
        auto *wrapper = reinterpret_cast<instance *>(inst.ptr());
        wrapper->owned = false;
        void *&valueptr = values_and_holders(wrapper).begin()->value_ptr();

        switch (policy) {
            case return_value_policy::copy:
                if (copy_constructor) {
                    valueptr = copy_constructor(src);
                } else {
#if defined(PYBIND11_DETAILED_ERROR_MESSAGES)
                    std::string type_name(tinfo->cpptype->name());
                    detail::clean_type_id(type_name);
                    throw cast_error("return_value_policy = copy, but type " + type_name
                                     + " is non-copyable!");
#else
                    throw cast_error("return_value_policy = copy, but type is "
                                     "non-copyable! (#define PYBIND11_DETAILED_ERROR_MESSAGES or "
                                     "compile in debug mode for details)");
#endif
                }
                wrapper->owned = true;
                break;
            //... some cases ignore here
            default:
                throw cast_error("unhandled return_value_policy: should not happen!");
        }

        tinfo->init_instance(wrapper, existing_holder);

        return inst.release();
    }

此处 pybind11 实现了多种 policy 用来控制不同的对象从 c++ 到 Python的转移行为, 此处我们以 copy plicy 为例来说明, 主要步骤如下: 1. auto inst = reinterpret_steal<object>(make_new_instance(tinfo->type)); 2. 获取inst关联的value_ptr 3. copy模式下对利用src对value_ptr执行拷贝构造. 4. 调用tinfo->init_instance()执行对应 Python 类型的 __init__操作 5. 释放引用计数并向 Python 返回新创建的对象(PyCapsule对象)

整体比Lua的相关实现复杂很多, 很多程度的原因是因为Python对C++对象的支持, 不是跟Lua一样使用的帮你分配指定size的内存块, 再关联meta table的做法, 从上面的代码我们可以看到, pybind11 的实现中, Python对象的创建, 和对应C++对象的构建, 是完全分开的, 并不是我们向Python虚拟机请求一块内存做 replacement new 的操作流程.

[!info] 需要注意的是, cast()的地方始终在创建新的Python对象(注意make_new_instance()任何policy的情况都会调用到), 这可能对一些频繁与 Python 交互的 C++ 对象并不友好, 迭代中我们需要对这部分重新进行考虑.


4.4.3 UDT 实现小结

从 C++17/20 的角度来看, 这种依赖 SFINAE 正交分解 side effect 特性来实现特定功能的方式不是特别可取, 一般我们会选择实现 is_udt<> 类似的宏来准确判断一个类型是否是我们预期的UDT, 但 pybind11 作为一个从 c++11 特性开始迭代的库, 使用这种设计, 也无可厚非, 只是对于现在的C++来说, 这种设计肯定就不推荐了.


4.5 其他类型的处理

还有剩下的对std::tuple, std::shared_ptr 等的特化, 可以借助这些对 Python C API操作各种数据类型进行快速的了解, 相关代码针对性比较强, 不同的特化用来处理不同的数据类型, 按需阅读使用即可.


4.6 函数的输入输出参数处理

pybind11 对导出功能的封装上, 对 ctor(), function call(), property 统一使用了cpp_function 的封装方式, 这点可能也是跟其他Bridge有差异的地方, 先抛开可能存在的问题, 我们先来看优点, 这样我们了解cpp_function的机制, 就了解了 pybind11 的基础运行机制, 本节中我们重点来了解一下 cpp_function 对输入输出参数的处理, 这也是对前面讲到的 C++ <-> Python 间类型转换的一个应用.

4.6.1 make_caster - 对&, *, &&, const等修饰符的支持

实际C++代码中, 我们很多时候使用的类型都是带修饰符的, 比如引用, 指针类型等, 在 pybind11 中, 我们通过make_caster<> 辅助模板来统一不同修饰符类型对应的caster: 位于 cast.h 中:

代码语言:javascript
复制
template <typename type>
using make_caster = type_caster<intrinsic_t<type>>;

template <typename T>
struct intrinsic_type {
    using type = T;
};
template <typename T>
struct intrinsic_type<const T> {
    using type = typename intrinsic_type<T>::type;
};
template <typename T>
struct intrinsic_type<T *> {
    using type = typename intrinsic_type<T>::type;
};
template <typename T>
struct intrinsic_type<T &> {
    using type = typename intrinsic_type<T>::type;
};
//...

实现机制也比较简单, 通过递归实现的intrinsic_type<>模板类, 我们可以成功的提取类型T对应的原始数据类型. 这样通过使用的这个辅助模板类的 make_caster<> , 我们始终可以拿到原始数据类型对应的 type_caster<> 了, 也就是我们上面介绍的那些针对不同类型的特化版本的实现.

4.6.2 cpp_function 的 输入输出参数处理

cpp_function 对输入输出的处理是发生在initialize()模板函数上的, 同时该函数也完成了对 C++ 函数的类型擦除: 位于 pybind11.h cpp_function::initialize()中:

代码语言:javascript
复制
/// Special internal constructor for functors, lambda functions, etc.
    template <typename Func, typename Return, typename... Args, typename... Extra>
    void initialize(Func &&f, Return (*)(Args...), const Extra &...extra) {
    //...

    /* Type casters for the function arguments and return value */
    using cast_in = argument_loader<Args...>;
    using cast_out
        = make_caster<conditional_t<std::is_void<Return>::value, void_type, Return>>;

    //...
}

我们本节重点关注输入输出处理的这部分, 输入参数的类型在initialize()中被定义为 cast_in, 输出的类型则是cast_out, 两者最终都是通过前面介绍的make_caster<>来关联Python<->C++类型的. 这两个类型最终通过下面的lambda来使用, 同时它也是最终所有 C++ 函数能够统一到 cpp_function 类型的原因: 位于 pybind11.h cpp_function::initialize()中:

代码语言:javascript
复制
/* Dispatch code which converts function arguments and performs the actual function call */
rec->impl = [](function_call &call) -> handle {
    cast_in args_converter;

    /* Try to cast the function arguments into the C++ domain */
    if (!args_converter.load_args(call)) {
        return PYBIND11_TRY_NEXT_OVERLOAD;
    }

    /* Invoke call policy pre-call hook */
    process_attributes<Extra...>::precall(call);

    /* Get a pointer to the capture object */
    const auto *data = (sizeof(capture) <= sizeof(call.func.data) ? &call.func.data
                                                                  : call.func.data[0]);
    auto *cap = const_cast<capture *>(reinterpret_cast<const capture *>(data));

    /* Override policy for rvalues -- usually to enforce rvp::move on an rvalue */
    return_value_policy policy
        = return_value_policy_override<Return>::policy(call.func.policy);

    /* Function scope guard -- defaults to the compile-to-nothing `void_type` */
    using Guard = extract_guard_t<Extra...>;

    /* Perform the function call */
    handle result
        = cast_out::cast(std::move(args_converter).template call<Return, Guard>(cap->f),
                         policy,
                         call.parent);

    /* Invoke call policy post-call hook */
    process_attributes<Extra...>::postcall(call, result);

    return result;
};

通过 Python虚拟机 <-> 与C++数据的交换和传递, 我们最终完成了在Python中调用一个C++函数的目的, 此处我们仅关注这其中发生的类型转换, 具体的实现先不展开.

4.6.3 pybind11 C++ 函数参数类型处理机制


5 异常处理

Pybind11 使得在 C++ 和 Python 之间传递异常变得简单。当 C++ 代码抛出一个异常时,Pybind11 会捕获该异常并将其转换为相应的 Python 异常。同样,当 Python 代码抛出异常时,Pybind11 也可以将其转换为 C++ 异常。我们通过具体的代码简单了解一下这两种情况.


5.1 Python 中处理 C++ 异常

这种情况下我们需要先在 C++ 中对对应的异常进行注册, 然后再在 python中使用它: C++ 代码 (exception_example.cpp):

代码语言:javascript
复制
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <stdexcept>

namespace py = pybind11;

int divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Division by zero!");
    }
    return a / b;
}

PYBIND11_MODULE(exception_example, m) {
    m.doc() = "Exception handling example";

    m.def("divide", &divide, "Divide a by b");

    // Register C++ exception for translation to Python exception
    py::register_exception<std::runtime_error>(m, "CppRuntimeError");
}

Python 代码 (test_exception.py):

代码语言:javascript
复制
import exception_example

try:
    result = exception_example.divide(10, 2)
    print(result)  # 输出 5
except exception_example.CppRuntimeError as e:
    print(f"Caught C++ exception: {e}")

try:
    result = exception_example.divide(10, 0)
    print(result)
except exception_example.CppRuntimeError as e:
    print(f"Caught C++ exception: {e}")  # 输出 "Caught C++ exception: Division by zero!"

如上面代码中所示, 通过在 C++ 中调用 pybind11 的 register_exception<>() 模板函数注册一个异常后, 我们随后可以直接在 Python 中使用 except 直接捕获这个可能抛出的 C++ 异常.


5.2 C++ 中处理 Python 异常

这个其实就是我们一般需要在引擎中支持的脚本错误处理回调, 回调中一般会输出错误日志等信息, 通过 pybind11, 这个功能也能很好的完成:

Python 代码 (python_function.py):

代码语言:javascript
复制
def add(a, b):
    if not isinstance(a, int) or not isinstance(b, int):
        raise ValueError("Both arguments must be integers")
    return a + b

C++ 代码 (call_python_function.cpp):

代码语言:javascript
复制
#include <iostream>
#include <pybind11/pybind11.h>
#include <pybind11/embed.h>

namespace py = pybind11;

int main() {
    py::scoped_interpreter guard{}; // Start Python interpreter

    py::module module = py::module::import("python_function");
    py::function add = module.attr("add");

    try {
        int a = 2;
        std::string b = "3";
        int result = add(a, b).cast<int>();
        std::cout << a << " + " << b << " = " << result << std::endl;
    } catch (const py::error_already_set &e) {
        std::cerr << "Caught Python exception: " << e.what() << std::endl;
        e.restore(); // 重新抛出异常,这样 Python 可以提取更多信息
        PyErr_Print(); // 打印完整的 Python 错误信息
    }

    return 0;
}

上面的代码演示了如何在调用 Python 函数的时候正确的处理 Python 抛出的异常并打印相关的错误.


6. 总结

我们从 pybind11 的示例出发, 再深入到它对 Python对象的处理, 以及C++对象的处理, 再到整个 pybind11的类型系统, 讲述了 pybind11 核心功能的实现, 目的也比较简单, 深入了解它, 才能让它跟目前的项目更好的结合, 同时也给有需要的读者一个参考.

当然, 还有一些比较重要的内容, 比如: - 有 C++ 反射的情况下如何更好的整合 pybind11 - 主流的 Python 脚本调试方法 - 在 C++ 调试状态下如何便利的查看 PyObject 等 Python 虚拟机对象 - 如何支持 Python 脚本的性能分析 - ... 我们尽量在下一篇 <<记 CrossEngine 的 Python 脚本接入>> 篇中填坑.

7. 参考

  1. pybind11 GitHub
  2. OpenAI Gpt4 Web版
  3. 来自 @人丑就要多读书 的友情指导
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-09-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0. 导语
    • 1. 为什么 pybind11 这类中间件是必要的
      • 1.1 pybind11 的简单使用
        • 1.2 本节小结
          • 2. pybind11 对 Python 对象的支持
            • 2.1 对象体系概述
              • 2.1.1 pyobject_tag
                • 2.1.2 detail::object_api<Derived>
                  • 2.1.3 handle
                    • 2.1.4 object
                      • 2.1.5 detail::generic_type 和 class_
                        • 2.1.6 capsule
                          • 2.2 生命周期控制的辅助设施
                            • 2.2.1 reinterpret_borrow<>
                              • 2.2.2 reinterpret_steal<>
                                • 2.2.3 Python对象支持小结
                                  • 3. pybind11 对 C++ 类的支持
                                    • 3.1 Register - C++ 类注册部分
                                      • 3.1.1 make_new_python_type() - 3a 部分实现
                                        • 3.1.2 detail::type_info的生成和注册 - 3b 部分实现
                                          • 3.1.3 tb_base 的处理 - 3c 部分的实现
                                            • 3.2 Register - C++ 函数注册部分
                                              • 3.2.1 dispatcher() 向 Python 的注册
                                                • 3.3 Register - C++ ctor()注册部分
                                                  • 3.4 Register - C++ 成员变量注册部分
                                                    • 3.5 Runtime - C++ 类对象的创建和销毁
                                                      • 3.5.1 pybind11::detail::instance
                                                        • 3.5.2 pybind11::detail::value_and_holder
                                                          • 3.5.3 C++ 类在 Python 中的类型
                                                            • 3.5.4 类对象的创建
                                                              • 3.5.5 类对象的销毁
                                                                • 3.6 Runtime - 函数调用
                                                                  • 3.7 其它
                                                                    • 4. pybind11 中的类型转换系统
                                                                      • 4.1 type_caster
                                                                        • 4.2 builtin 类型的处理简述
                                                                          • 4.2.1 数值类型
                                                                            • 4.2.2 void, void_type, nullptr_t
                                                                              • 4.2.3 bool
                                                                                • 4.2.4 字符串
                                                                                  • 4.3 pyobject 类型的处理
                                                                                    • 4.4 常规 C++ 类的处理 - UDT 类型支持
                                                                                      • 4.4.1 load()实现
                                                                                        • 4.4.2 cast()实现
                                                                                          • 4.4.3 UDT 实现小结
                                                                                            • 4.5 其他类型的处理
                                                                                              • 4.6 函数的输入输出参数处理
                                                                                                • 4.6.1 make_caster - 对&, *, &&, const等修饰符的支持
                                                                                                  • 4.6.2 cpp_function 的 输入输出参数处理
                                                                                                    • 4.6.3 pybind11 C++ 函数参数类型处理机制
                                                                                                      • 5 异常处理
                                                                                                        • 5.1 Python 中处理 C++ 异常
                                                                                                          • 5.2 C++ 中处理 Python 异常
                                                                                                            • 6. 总结
                                                                                                              • 7. 参考
                                                                                                              相关产品与服务
                                                                                                              消息队列 TDMQ
                                                                                                              消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
                                                                                                              领券
                                                                                                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档