在 Python 中调用 C++ 代码

  Python 与 C++ 的互操作性为开发者提供了多种灵活的选择,在本篇文章中将会总结一些在 Python 中调用 C++ 代码的方法,包括使用 ctypes 库直接调用 C++ 动态链接库函数,借助 SWIG 或 Boost.Python 自动生成或手动编写接口,以及利用 Cython 和 Pybind11 等现代工具创建 Python 与 C++ 之间的绑定。这些方法简化了在 Python 中调用 C++ 代码的过程,使得开发者能够轻松利用 C++ 的性能优势。本文将着重介绍 Pybind11 这一工具。


方案筛选

  1. Ctypes: Ctypes 是 Python 内置的一个标准库,可以用来调用动态链接库(DLL)中的 C/C++ 函数。通过一套类型映射的方式将 Python 与二进制动态链接库相连接。
  2. SWIG(Simplified Wrapper and Interface Generator):SWIG 是一个能够自动生成 C/C++ 程序和其他高级语言(如 Python)之间的包装器的工具。它可以将 C/C++ 代码包装成可以被 Python 直接调用的模块。但由于支持的语言众多,因此在 Python 端性能表现不是太好。
  3. Boost.Python: Boost.Python 是 C++ Boost 库中的一个子模块,它提供了一组 C++ 类和函数,用于将 C++ 代码包装成 Python 可以直接调用的模块。但最大的缺点是需要依赖庞大的 Boost 库,编译和依赖关系包袱重。
  4. Cython: Cython 是一个用于将 Python 代码转换为 C/C++ 代码的编译器,可以通过将 C/C++ 代码嵌入到 Python 中。
  5. Pybind11:Pybind11 是一个轻量级的开源库,可以将 C++ 代码封装成可以被 Python 直接调用的模块。它提供了简洁而直观的语法,使得将 C++ 代码封装成 Python 接口变得更加容易。

对比

  • 底层实现:Ctypes 是使用 Python 自带的标准库,通过 调用动态链接库 (DLL)中的 C/C++ 函数来实现。SWIG、Boost.Python、Cython 和 Pybind11 则是通过 生成封装代码 来实现,将 C/C++ 代码封装成可以被 Python 直接调用的模块。
  • 使用难度:Ctypes 的使用相对较简单,只需要导入函数原型并调用即可。SWIG 在配置和使用上较为复杂,需要编写接口文件和配置文件。Boost.Python 和 Pybind11 的使用相对较简单。

开源库的选择参考

  • HiGHS:选择了 Pybind11;
  • Tensorflow:已于 2019 年将 SIWG 切换为 pybind11;
  • 目前市面上大部分 AI 计算框架,如 TensorFlow、Pytorch、阿里 X-Deep Learning、百度 PaddlePaddle 等,均使用 pybind11 来提供 C++ 到 Python 端接口封装。





pybind11 使用总结

参考:Pybind11 文档

模块引入

  pybind11 是一个 header-only 的库,只需要 C++ 项目里直接 include pybind11 的头文件就能使用。可以 git submodule 添加子模块:

1
2
3
4
5
6
7
8
9
git submodule add https://github.com/pybind/pybind11.git pybind11
cd pybind11/
git checkout tags/v2.10.0

mkdir build
cd build
cmake ..
cmake --build . --config Release --target check
make check -j 4

在 CMakeLists.txt 里 add_subdirectory pybind11 的路径,再用其提供的 pybind11_add_module 就能创建 pybind11 的模块了。

1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.25)
project(pybind_test)

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

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

如果想在已有 C++ 动态库上扩展 pybind11 绑定,那么 target_link_libraries 链接该动态库就可以了。
(示例代码:https://github.com/ikuokuo/start-pybind11

使用 pybind11 封装 C++

C++ 文件

1
2
3
4
5
6
7
8
#include "vdot.h"
double dot(std::vector<double> &a, std::vector<double> &b) {
double res = 0;
for (int i = 0; i < (int)a.size(); ++i) {
res += a[i] * b[i];
}
return res;
}

pybind11 文件

1
2
3
4
5
6
7
8
9
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include "cpp/vdot_cpp/vdot.h"

namespace py = pybind11;
PYBIND11_MODULE(np, m) { // (Python 包名为 np, 实例对象)
m.doc() = "";
m.def("vdot", &dot); // m.def("Python 函数名 ", &C 函数名);
}

编写 CMake

编译 C++ 的库

使用 start-pybind11 提供的宏进行编译 C++ 动态库(静态库也可以)。

1
2
3
4
5
# add_my_library(LIB_NAME [SRCS srcs] [LIBS libs] [SHARED] [THREAD])
add_my_library(vdotlib # C++ 编译后的库名
SRCS vdot.cpp # C++ 源文件
SHARED # 动态库
THREAD)

在父层文件夹添加该子目录

1
add_subdirectory(${MY_CURR}/vdot_cpp)
编译 pybind11 的 .so 库

使用 start-pybind11 提供的宏进行编译 C++ 动态库(静态库也可以)。

1
2
3
4
5
add_pb_library(np # 库的名字( Python 的包名 )
SRCS vdot_py.cpp # binding 文件
LIBS vdotlib # C++ 编译后的库名,静态动态均可
SHARED
THREAD)

在父层文件夹添加该子目录

1
add_subdirectory(${MY_CURR}/vdot)

让 Python 的.so 库可以找到 C++ 库

把 C++ 编译后的库文件导入动态连接库的搜索路径

1
2
3
p_c="/Users/sxj/CLionProjects/start-pybind11-new/_output/lib/vdot_cpp"
export DYLD_LIBRARY_PATH=$p_c${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}}
echo $DYLD_LIBRARY_PATH

或者直接移动 .dylib 库(或 .a 库)到 .so 库相同目录。

把 .so 库 加入 Python 的搜索路径

1
2
3
p_so="/Users/sxj/CLionProjects/MDecomper0922/_output/lib/pybind"
export PYTHONPATH=$p_so${PYTHONPATH:+:${PYTHONPATH}}
echo $PYTHONPATH

然后就可以使用 import 加 .so 的名字来使用了。

支持的数据类型

参考:https://daobook.github.io/pybind11/advanced/cast/index.html
  float, doubleboolcharconst char *std::stringstd::pair<T1, T2>std::tuple<...>std::complex<T>std::array<T, Size>std::vector<T>std::set<T>std::function<...>Eigen::Matrix<...>Eigen::SparseMatrix<...> ……

STL 容器
pybind11 支持 STL 容器自动转换,当需要处理 STL 容器时,只要额外包括头文件 <pybind11/stl.h> 即可。

bytes、string 类型传递
由于在 Python3 中 string 类型默认为 UTF-8 编码,如果从 C++ 端传输 string 类型的 protobuf 数据到 Python,则会出现 “UnicodeDecodeError” 的报错,所以需要使用 py::bytes

1
2
3
4
5
6
m.def("return_bytes",
[]() {
std::string s("\xba\xd0\xba\xd0"); // Not valid UTF-8
return py::bytes(s); // Return the data without transcoding
}
);

智能指针
智能指针 - pybind11 中文文档

函数

声明函数参数名称和默认值

1
2
m.def("add", &add, "A function which adds two numbers",
py::arg("i") = 1, py::arg("j") = 2);

返回指针

1
2
Data *get_data() { return _data; }
m.def("get_data", &get_data, py::return_value_policy::reference);

运算符重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Vector2 {
public:
Vector2(float x, float y) : x(x), y(y) { }

Vector2 operator+(const Vector2 &v) const { return Vector2(x + v.x, y + v.y); }
Vector2 operator*(float value) const { return Vector2(x * value, y * value); }
Vector2& operator+=(const Vector2 &v) { x += v.x; y += v.y; return *this; }
Vector2& operator*=(float v) { x *= v; y *= v; return *this; }

friend Vector2 operator*(float f, const Vector2 &v) {
return Vector2(f * v.x, f * v.y);
}

std::string toString() const {
return "[" + std::to_string(x) + ", " + std::to_string(y) + "]";
}
private:
float x, y;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pybind11/operators.h>

PYBIND11_MODULE(example, m) {
py::class_<Vector2>(m, "Vector2")
.def(py::init<float, float>())
.def(py::self + py::self)
.def(py::self += py::self)
.def(py::self *= float())
.def(float() * py::self)
.def(py::self * float())
.def(-py::self)
.def("__repr__", &Vector2::toString);
}

面向对象

公有变量

1
.def_readwrite("name", &Pet::name)

私有变量

1
.def_property("name", &Pet::getName, &Pet::setName)

继承

1
2
3
4
5
6
7
8
9
struct Pet {
Pet(const std::string &name) : name(name) { }
std::string name;
};

struct Dog : Pet {
Dog(const std::string &name) : Pet(name) { }
std::string bark() const { return "woof!"; }
};
1
2
3
4
5
6
py::class_<Pet>(m, "Pet")
.def(py::init<const std::string &>())
.def_readwrite("name", &Pet::name);
py::class_<Dog, Pet>(m, "Dog")
.def(py::init<const std::string &>())
.def("bark", &Dog::bark);

重载

1
2
3
4
5
6
7
struct Pet {
void set(int age_) { age = age_; }
void set(const std::string &name_) { name = name_; }
};
py::class_<Pet>(m, "Pet")
.def("set", py::overload_cast<int>(&Pet::set), "Set the pet's age")
.def("set", py::overload_cast<const std::string &>(&Pet::set), "Set the pet's name");
1
2
3
4
5
6
7
8
struct Widget {
int foo(int x, float y);
int foo(int x, float y) const;
};

py::class_<Widget>(m, "Widget")
.def("foo", py::overload_cast<int, float>(&Widget::foo))
.def("foo", py::overload_cast<int, float>(&Widget::foo, py::const_));

内部类和内部枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Pet {
struct Attributes {
float age = 0;
};
enum Kind {
Dog = 0,
Cat
};
};
py::class_<Pet> pet(m, "Pet");
py::class_<Pet::Attributes> attributes(pet, "Attributes")
.def(py::init<>())
.def_readwrite("age", &Pet::Attributes::age);
py::enum_<Pet::Kind>(pet, "Kind")
.value("Dog", Pet::Kind::Dog)
.value("Cat", Pet::Kind::Cat)
.export_values();

手动编译

1
c++ -O3 -Wall -shared -std=c++11 -fPIC $(python3-config --includes) -Iextern/pybind11/include example.cpp -o example$(python3-config --extension-suffix)

py::cast

用于在 C++ 代码中进行 Python 对象类型的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <pybind11/pybind11.h>

namespace py = pybind11;

int main() {
py::initialize_interpreter(); // 初始化 Python 解释器

// 将 Python 整数对象转换为 C++ 整数
py::object py_int = py::int_(42);
int cpp_int = py::cast<int>(py_int);
std::cout << "C++ int: " << cpp_int << std::endl;

// 将 C++ 整数转换为 Python 整数对象
int cpp_int2 = 123;
py::object py_int2 = py::cast<py::object>(cpp_int2);
std::cout << "Python int: " << py::str(py_int2) << std::endl;

py::finalize_interpreter(); // 清理 Python 解释器

return 0;
}





开源示例

示例 start-pybind11 运行命令记录

GitHub - ikuokuo/start-pybind11: Start pybind11
【注意】 切换 Python 的环境为 3.9,首先需要在 CLion 中设置 Python Interpreter 为指定版本的 conda 环境(本地测试成功的为 py39)。完全退出 CLion,在命令行conda activate py39 切换环境后再次打开open CLion.app 后即可更改运行的 Python 环境。

编译

1
-DBUILD_PYTHON_BINDINGS=True
1
2
cd start-pybind11/
make install

运行

加入 Python 的环境变量

1
source setup.bash first_steps
1
2
import first_steps_pb as pb
pb.add(1, 2)

HiGHS 运行命令记录

编译

1
-DBUILD_PYTHON=True -DBUILD_DEPS=ON
1
2
3
4
mkdir build
cd build
cmake ..
cmake --build .

安装

1
sudo cmake --install .

Python 包安装(cd 到项目根目录,借助 setup.py 进行安装)

1
pip install -e ./

同时需要安装依赖

1
2
pip install pybind11
pip install pyomo

测试 Python 接口

1
pytest -v ./highspy/tests/



参考网站





打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2019-2024 SongXJ
  • 访问人数: | 浏览次数:

      请我喝杯咖啡吧~

      支付宝
      微信