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...); };
此后都可以按照普通函数来调用。
- 实现一个普通函数
impl
将std::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_accessor
或str_attr_accessor
对象,对其赋值则会指定该属性名对应的Python对象。
// 直接赋值
m.attr("the_answer") = 42;
// 或者,使用 py::cast 转换
m.attr("what") = py::cast("World");
这将最终在PyBind11内部转化为对CPython的API函数PyObject_SetAttr
和PyObject_SetAttrString
的调用。
C++类的封装
首先创建一个py::class_
类型的变量,然后使用def
和attr
向类中添加属性和方法。py::class_
最终继承自py::object
,因此attr
函数的描述可以参考上面的说明。
成员函数def
有新的定义,并且没有显式using
父类的def
,因此父类的def
被隐藏。
这里只讨论最常用的def
版本。它的函数签名仍然和上面描述的一致,接受一个函数名,一个C++”函数“,以及一组额外属性。不同的是,这里的处理方法是将这个cpp_function
作为该类的一个属性(对于模块是直接添加为模块的对象),不过相同的是在Python一侧看来它都是一个可调用对象。