[pybind11]为c++项目写python API接口

发布时间 2023-06-18 12:25:13作者: wildkid1024

C++项目的pybind方法有哪些?有什么区别?

以下是主要的python绑定cpp的方法:

方法 年份 代表用户
适用于 CPython 的 C/C++ 扩展模块 1991 标准库
PyBind11(推荐用于 C++) 2015
Cython(推荐用于 C) 2007 gevent、kivy
HPy 2019
mypyc 2017
ctype 2003 oscrypto
cffi 2013 cryptography、pypy
SWIG 1996 crfsuite
Boost.Python 2002
cppyy 2017

其中

  1. ctypes: C 与 Python 绑定, Python 内建模块,通过调用.so动态链接库来使用方法,不需要在c++中有特殊的写法,如果使用c++则需要使用extern c的方式再包装一层
  2. Boost.Python: C++ 与 Python 绑定, Boost 模块
  3. pybind11: C++11 与 Python 绑定, 减去了旧 C++ 支持,更轻量化,需要在编写cpp时处理响应的绑定函数。
  4. CPython python标准库中的方式,需要手写绑定方法

一般使用pybind11多一些,因为更为轻量化,能够轻易地加入c++特性。

怎么使用pybind11为python添加c++扩展?

安装pybind11

注意事项

首先在安装时,应确保c++环境和python环境保持一致,具体来讲,就是输入python后

wildkid1024@debian:~$ python3
Python 3.9.2 (default, Feb 28 2021, 17:03:44) 
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 

会显示python使用的gcc版本号,这里需要和c++项目对应的编译器对应:

wildkid1024@debian:~$ cc --version
cc (Debian 10.2.1-6) 10.2.1 20210110
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

因为笔者使用anaconda的环境,在这里就栽了不少跟头,稍微吐槽以下发现anaconda越来越臃肿和无用。

有两种方式可以安装pybind11,第一种是通过c++的项目引入,一种是通过python库的方式安装

cpp 安装

如果是熟悉cpp的编程方式,则是通过submodule的方式进行导入的,具体而言:

git submodule add https://github.com/pybind/pybind11.git third_party/pybind11
cd third_party/pybind11/
git checkout tags/my-version

相应地需要在cmake编译时将pybind11库添加进来,这里需要注意的是pybind11_add_module里的名称需要和编写cpp扩展中的名称保持一直,否则将保持找不到对应的库。

cmake_minimum_required(VERSION 3.1)
project(start-pybind11 VERSION 0.1.0 LANGUAGES C CXX)

set(MY_PYBIND ${MY_CURR}/third_party/pybind11)

add_subdirectory(${MY_PYBIND})
pybind11_add_module(example_pb example_pb.cpp)

如果想在已有 C++ 动态库上扩展 pybind11 绑定,那么 target_link_libraries 链接该动态库就可以了。

target_link_libraries(example_pb PUBLIC example)

python库安装

使用python库的方式安装比较简单,更适合熟悉python的同学。

  1. 使用 pip 安装 PyBind11:pip install pybind11 或 py -m pip install pybind11。
  2. 在cpp项目目录下,运行 python -m pybind11 --includes 或 py -m pybind11 --includes导入pybind11的库

通过pybind11编写绑定扩展

通过以上安装步骤,已经在cpp项目中添加了pybind11的库,接下来就是使用pybind11编写cpp到pybind的扩展:

首先导入pybind11的头文件:

#include <pybind11/pybind11.h>

然后在要导入的module底部,添加以下方法绑定:

namespace py = pybind11;

PYBIND11_MODULE(superfastcode2, m) {
    m.def("fast_tanh2", &tanh_impl, R"pbdoc(
        Compute a hyperbolic tangent of a single argument expressed in radians.
    )pbdoc");

#ifdef VERSION_INFO
    m.attr("__version__") = VERSION_INFO;
#else
    m.attr("__version__") = "dev";
#endif
}

tanh_impl为对应的cpp函数实现的地址,fast_tanh为导入的方法名称,具体来讲,在python中将通过以下方式进行调用:

import superfastcode2
superfastcode2.fast_tanh2(args)

C++类的绑定

PYBIND11_MODULE(tick_pb, m) {
  m.doc() = "tick_pb bindings";

  py::class_<Tick, std::shared_ptr<Tick>>(m, "Tick")
    .def(py::init<std::int64_t, std::int64_t>())
    .def(py::init<TickEvent, std::int64_t, std::int64_t,
                  TickRunCallback, TickRunCallback>())
    .def_property_readonly("is_running", &Tick::IsRunning)
    .def("start", &Tick::Start)
    .def("stop", &Tick::Stop, "wait_life_over"_a = false)
    .def("get_time_start", &Tick::GetTimeStart)
    .def("set_tick_event", [](Tick &self, const TickEvent &tick_event) {
      self.SetTickEvent(tick_event);
    })
    .def("set_run_beg_callback", [](Tick &self,
        const TickRunCallback &run_beg) {
      self.SetRunBegCallback(run_beg);
    })
    .def("set_run_end_callback", [](Tick &self,
        const TickRunCallback &run_end) {
      self.SetRunEndCallback(run_end);
    })
    .def("wait_life_over", &Tick::WaitLifeOver,
        py::call_guard<py::gil_scoped_release>());
}

py::class_定义了从cpp类到python对象的映射,py::init表示了几种实例化方法,def_property_readonly定义了只读属性对应私有成员变量,def_readwrite定义可读写的公有成员变量,其他按照def,取相应的方法地址即可。

库的导出和使用

cpp项目build之后,会在build目录下产生name.cpython-python_version-arch-linux-gnu.so的文件,需要将该文件移到python的运行导入库下面,或通过sys.path手动添加。

还可以通过编写setup的方式将cpp项目打包为python库:

from setuptools import setup, Extension
import pybind11

cpp_args = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']

sfc_module = Extension(
    'superfastcode2',
    sources=['module.cpp'],
    include_dirs=[pybind11.get_include()],
    language='c++',
    extra_compile_args=cpp_args,
    )

setup(
    name='superfastcode2',
    version='1.0',
    description='Python package with superfastcode2 C++ extension (PyBind11)',
    ext_modules=[sfc_module],
)

问题排查

1. dynamic module does not define module export function

  1. 检查包名是否一致,即cmake中bind的名称是否和cpp绑定代码中的名称一致。
  2. 检查cc版本是否python编译版本一致
  3. 检查python项目是否包含了编译好的lib路径

相似问题:

ImportError: dynamic module does not define init function
Symbol not found: __Py_ZeroStruct / _PyInstanceMethod_Type
SystemError: dynamic module not initialized properly
The Python interpreter immediately crashes when importing my module

2. Some automatic conversions are optional and require extra headers to be included when compiling your pybind11 module.

  1. 检查是否导入#include <pybind11/functional.h>

文献引用

  1. https://learn.microsoft.com/zh-cn/visualstudio/python/working-with-c-cpp-python-in-visual-studio?view=vs-2022
  2. https://pybind11.readthedocs.io/en/stable/index.html