PyBind11:基本用法和底层实现

 

PyBind11底层其实就是CPython的各个API调用,但使用C++做了良好的封装。

PyBind11中大量应用了现代C++技巧,如变长参数模板(variadic template)、lambda表达式,同时也使用了一些传统的C++技巧和设计模式,如奇异递归模板模式(CRTP,Curiously Recurring Template Pattern)。

下面的所有说明遵循官方的惯例,使用如下头文件和命名空间别名:

#include <pybind11/pybind11.h>
namespace py = pybind11;

模块入口函数

导入模块:

PYBIND11_MODULE(test, m) {
    m.doc() = "PyBind11 Test";
    m.def("add", [](int i, int j) { return i + j; });
}

该宏PYBIND11_MODULE定义了函数PYBIND11_PLUGIN_IMPL(name)

#define PYBIND11_MODULE(name, variable)                                    \
    static void PYBIND11_CONCAT(pybind11_init_, name)(pybind11::module&);  \
    PYBIND11_PLUGIN_IMPL(name) {                                           \
        PYBIND11_CHECK_PYTHON_VERSION                                      \
        auto m = pybind11::module(pybind11_init_, name)(m);                \
        try {                                                              \
            PYBIND11_CONCAT(pybind11_init_, name)(m);                      \
            return m.ptr();                                                \
        } PYBIND11_CATCH_INIT_EXCEPTIONS                                   \
    }                                                                      \
    void PYBIND11_CONCAT(pybind11_init_, name)(pybind11::module& variable)

这个函数名会宏展开为extern "C" PyObject* PyInit_##name()

#define PYBIND11_PLUGIN_IMPL(name)                        \
    extern "C" PYBIND11_EXPORT PyObject* PyInit_##name(); \
    extern "C" PYBIND11_EXPORT PyObject* PyInit_##name()

而CPython需要导入的模块实现的即这个函数。

C++函数的封装

C++函数被封装在类cpp_function中,它允许接受的C++“函数”有:

  • 普通函数指针(形如R (*)(Args ...)
  • 函数对象(亦称仿函数)
    • lambda表达式实际上就是仿函数的语法糖
    • 具有R operator()(Args ...)的括号运算符重载)
  • 类的成员函数指针(形如R (C::*)(Args ...)
  • 类的常成员函数指针(形如R (C::*)(Args ...) const)。

函数类cpp_function

C++侧

概括来说,cpp_function使用了和std::function类似的类型擦除(type erasure)方法。在模板化的构造函数中趁着还有目标函数(被绑定的C++函数)的类型信息,完成如下变换:

  • 把类的成员函数通过lambda转化为仿函数:

    Result (Class::*f)(Args ...) /* const */;
    [f](/* const */ Class* c, Args... args) -> Result {
        return c->f(args...);
    };
    

    此后都可以按照普通函数来调用。

  • 实现一个普通函数implstd::vector中存储的参数转发给被绑定的C++函数
  • 最终将上述函数impl转化为PyObject备用

CPython侧

PyBind11使用CPython的API函数PyCFunction_NewEx创建Python函数PyCFunctionObject;使用PyInstanceMethod_New(Python 3)或者PyMethod_New(Python 2)来创建一个PyObject

辅助函数def

module的成员函数def就接受一个函数名,一个C++“函数”,以及数个额外属性:

template <typename Func, typename... Extra>
module& module::def(const char* name_, Func&& f, const Extra& ... extra);

其中函数f会被完美转发给cpp_function的构造函数。

返回值为module&用于支持连续的def调用。

额外属性可以是类型为arg的对象,用于表示Python的具名参数。PyBind11定义了用户自定义字面量constexpr arg operator"" _a(const char* name, size_t length);这允许我们直接将调用py::arg("i")写为字面量"i"_a。此外,arg重载了赋值运算符,可以通过"i"_a = 1表示一个具有默认形参1的具名参数i

最终,函数f通过CPython的PyModule_AddObject函数注册到Python侧,但并不是作为Python函数,而是作为一个可调用对象(实现了__call__方法的对象)。

Python属性

函数attr接受一个const char*(空字符结尾的C字符串)或者一个句柄handle(持有另一个Python对象py::object),这个参数作为Python中访问它所使用的键(key)。attr返回一个可赋值的obj_attr_accessorstr_attr_accessor对象,对其赋值则会指定该属性名对应的Python对象。

// 直接赋值
m.attr("the_answer") = 42;
// 或者,使用 py::cast 转换
m.attr("what") = py::cast("World");

这将最终在PyBind11内部转化为对CPython的API函数PyObject_SetAttrPyObject_SetAttrString的调用。

C++类的封装

首先创建一个py::class_类型的变量,然后使用defattr向类中添加属性和方法。py::class_最终继承自py::object,因此attr函数的描述可以参考上面的说明。

成员函数def有新的定义,并且没有显式using父类的def,因此父类的def被隐藏。

这里只讨论最常用的def版本。它的函数签名仍然和上面描述的一致,接受一个函数名,一个C++”函数“,以及一组额外属性。不同的是,这里的处理方法是将这个cpp_function作为该类的一个属性(对于模块是直接添加为模块的对象),不过相同的是在Python一侧看来它都是一个可调用对象。