从 Python 调用 C/C++?[关闭]
- 2024-11-28 08:37:00
- admin 原创
- 254
问题描述:
构建 Python 到 C 或 C++ 库的绑定的最快方法是什么?
(如果这很重要的话,我会使用 Windows。)
解决方案 1:
ctypes模块是标准库的一部分,因此比swig更稳定、更广泛可用,而 swig 总是给我带来问题。
使用 ctypes,您需要满足对 python 的任何编译时依赖,并且您的绑定将在任何具有 ctypes 的 python 上工作,而不仅仅是针对它进行编译的 python。
假设您在名为 foo.cpp 的文件中想要与之对话的简单 C++ 示例类:
#include <iostream>
class Foo{
public:
void bar(){
std::cout << "Hello" << std::endl;
}
};
由于 ctypes 只能与 C 函数对话,因此您需要提供将其声明为 extern "C" 的函数
extern "C" {
Foo* Foo_new(){ return new Foo(); }
void Foo_bar(Foo* foo){ foo->bar(); }
}
接下来你必须将其编译为共享库
g++ -c -fPIC foo.cpp -o foo.o
g++ -shared -Wl,-soname,libfoo.so -o libfoo.so foo.o
最后,你必须编写你的 Python 包装器(例如在 fooWrapper.py 中)
from ctypes import cdll
lib = cdll.LoadLibrary('./libfoo.so')
class Foo(object):
def __init__(self):
self.obj = lib.Foo_new()
def bar(self):
lib.Foo_bar(self.obj)
一旦你有了这个你可以像这样调用它
f = Foo()
f.bar() #and you will see "Hello" on the screen
解决方案 2:
你应该看看Boost.Python。以下是摘自其网站的简短介绍:
Boost Python 库是一个用于连接 Python 和 C++ 的框架。它允许您快速无缝地将 C++ 类函数和对象公开给 Python,反之亦然,无需使用任何特殊工具——只需使用您的 C++ 编译器。它旨在以非侵入方式包装 C++ 接口,因此您无需更改 C++ 代码即可包装它,这使得 Boost.Python 成为将第三方库公开给 Python 的理想选择。该库使用高级元编程技术简化了用户的语法,因此包装代码看起来像是一种声明性接口定义语言 (IDL)。
解决方案 3:
还有pybind11
,它就像是Boost.Python的轻量级版本并且与所有现代 C++ 编译器兼容:
https://pybind11.readthedocs.io/en/latest/
解决方案 4:
最快的方法是使用SWIG。
SWIG教程中的示例:
/* File : example.c */
int fact(int n) {
if (n <= 1) return 1;
else return n*fact(n-1);
}
接口文件:
/* example.i */
%module example
%{
/* Put header files here or function declarations like below */
extern int fact(int n);
%}
extern int fact(int n);
在 Unix 上构建 Python 模块:
swig -python example.i
gcc -fPIC -c example.c example_wrap.c -I/usr/local/include/python2.7
gcc -shared example.o example_wrap.o -o _example.so
用法:
>>> import example
>>> example.fact(5)
120
请注意,您必须拥有 python-dev。此外,在某些系统中,python 头文件将位于 /usr/include/python2.7 中,具体取决于您的安装方式。
来自本教程:
SWIG 是一个相当完整的 C++ 编译器,支持几乎所有语言功能。这包括预处理、指针、类、继承,甚至 C++ 模板。SWIG 还可用于将结构和类打包到目标语言的代理类中 — 以非常自然的方式公开底层功能。
解决方案 5:
我从这个页面开始了我的 Python <-> C++ 绑定之旅,目的是链接高级数据类型(多维 STL 向量与 Python 列表):-)
在尝试了基于ctypes和boost.python 的解决方案后(并且不是一名软件工程师),我发现当需要高级数据类型绑定时它们很复杂,而我发现SWIG对于这种情况要简单得多。
因此,此示例使用 SWIG,并且它已经在 Linux 中测试过(但 SWIG 在 Windows 中也可用并且广泛使用)。
目标是使 Python 可以使用一个 C++ 函数,该函数以 2D STL 向量形式的矩阵为输入,并返回每行的平均值(作为 1D STL 向量)。
C++代码(“code.cpp”)如下:
#include <vector>
#include "code.h"
using namespace std;
vector<double> average (vector< vector<double> > i_matrix) {
// Compute average of each row..
vector <double> averages;
for (int r = 0; r < i_matrix.size(); r++){
double rsum = 0.0;
double ncols= i_matrix[r].size();
for (int c = 0; c< i_matrix[r].size(); c++){
rsum += i_matrix[r][c];
}
averages.push_back(rsum/ncols);
}
return averages;
}
等效头文件(“code.h”)为:
#ifndef _code
#define _code
#include <vector>
std::vector<double> average (std::vector< std::vector<double> > i_matrix);
#endif
我们首先编译 C++ 代码以创建一个目标文件:
g++ -c -fPIC code.cpp
然后,我们为我们的 C++ 函数定义一个SWIG 接口定义文件(“code.i”)。
%module code
%{
#include "code.h"
%}
%include "std_vector.i"
namespace std {
/* On a side note, the names VecDouble and VecVecdouble can be changed, but the order of first the inner vector matters! */
%template(VecDouble) vector<double>;
%template(VecVecdouble) vector< vector<double> >;
}
%include "code.h"
使用 SWIG,我们从 SWIG 接口定义文件生成 C++ 接口源代码。
swig -c++ -python code.i
最后,我们编译生成的 C++ 接口源文件并将所有内容链接在一起,生成一个可由 Python 直接导入的共享库(“_”很重要):
g++ -c -fPIC code_wrap.cxx -I/usr/include/python2.7 -I/usr/lib/python2.7
g++ -shared -Wl,-soname,_code.so -o _code.so code.o code_wrap.o
我们现在可以在 Python 脚本中使用该函数:
#!/usr/bin/env python
import code
a= [[3,5,7],[8,10,12]]
print a
b = code.average(a)
print "Assignment done"
print a
print b
解决方案 6:
对于现代 C++,请使用 cppyy:
http://cppyy.readthedocs.io/en/latest/
它基于 Cling,即 Clang/LLVM 的 C++ 解释器。绑定是在运行时进行的,不需要额外的中间语言。得益于 Clang,它支持 C++17。
使用 pip 安装:
$ pip install cppyy
对于小型项目,只需加载您感兴趣的相关库和标头。例如,从 ctypes 示例中获取代码是此线程,但分为标头和代码部分:
$ cat foo.h
class Foo {
public:
void bar();
};
$ cat foo.cpp
#include "foo.h"
#include <iostream>
void Foo::bar() { std::cout << "Hello" << std::endl; }
编译它:
$ g++ -c -fPIC foo.cpp -o foo.o
$ g++ -shared -Wl,-soname,libfoo.so -o libfoo.so foo.o
并使用它:
$ python
>>> import cppyy
>>> cppyy.include("foo.h")
>>> cppyy.load_library("foo")
>>> from cppyy.gbl import Foo
>>> f = Foo()
>>> f.bar()
Hello
>>>
大型项目支持自动加载准备好的反射信息和创建它们的 cmake 片段,以便已安装软件包的用户只需运行:
$ python
>>> import cppyy
>>> f = cppyy.gbl.Foo()
>>> f.bar()
Hello
>>>
得益于 LLVM,高级功能成为可能,例如自动模板实例化。继续示例:
>>> v = cppyy.gbl.std.vector[cppyy.gbl.Foo]()
>>> v.push_back(f)
>>> len(v)
1
>>> v[0].bar()
Hello
>>>
注:我是cppyy的作者。
解决方案 7:
pybind11 最小可运行示例
pybind11 之前在https://stackoverflow.com/a/38542539/895245中提到过,但我想在这里提供一个具体的使用示例和一些关于实现的进一步讨论。
总而言之,我强烈推荐 pybind11,因为它真的很容易使用:你只需包含一个标题,然后 pybind11 使用模板魔法来检查你想要向 Python 公开的 C++ 类,并以透明的方式完成。
这种模板魔法的缺点是,它会立即减慢编译速度,使使用 pybind11 的任何文件增加几秒钟的时间,例如,请参见对此问题所做的调查。PyTorch对此表示同意。已在以下网址提出了补救此问题的建议:https ://github.com/pybind/pybind11/pull/2445
这是一个最小的可运行示例,可以让您感受到 pybind11 有多么棒:
类_测试.cpp
#include <string>
#include <pybind11/pybind11.h>
struct ClassTest {
ClassTest(const std::string &name, int i) : name(name), i(i) { }
void setName(const std::string &name_) { name = name_; }
const std::string getName() const { return name + "z"; }
void setI(const int i) { this->i = i; }
const int getI() const { return i + 1; }
std::string name;
int i;
};
namespace py = pybind11;
PYBIND11_PLUGIN(class_test) {
py::module m("my_module", "pybind11 example plugin");
py::class_<ClassTest>(m, "ClassTest")
.def(py::init<const std::string &, int>())
.def("setName", &ClassTest::setName)
.def("getName", &ClassTest::getName)
.def_readwrite("name", &ClassTest::name)
.def("setI", &ClassTest::setI)
.def("getI", &ClassTest::getI)
.def_readwrite("i", &ClassTest::i);
return m.ptr();
}
类测试主类
#!/usr/bin/env python3
import class_test
my_class_test = class_test.ClassTest("abc", 1);
print(my_class_test.getName())
print(my_class_test.getI())
my_class_test.setName("012")
my_class_test.setI(2)
print(my_class_test.getName())
print(my_class_test.getI())
assert(my_class_test.getName() == "012z")
assert(my_class_test.getI() == 3)
编译并运行:
#!/usr/bin/env bash
set -eux
sudo apt install pybind11-dev
g++ `python3-config --cflags` -shared -std=c++11 -fPIC class_test.cpp \n -o class_test`python3-config --extension-suffix` `python3-config --libs`
./class_test_main.py
标准输出:
abcz
2
012z
3
如果我们尝试使用错误的类型,例如:
my_class_test.setI("abc")
它正如预期的那样爆炸了:
Traceback (most recent call last):
File "/home/ciro/test/./class_test_main.py", line 9, in <module>
my_class_test.setI("abc")
TypeError: setI(): incompatible function arguments. The following argument types are supported:
1. (self: my_module.ClassTest, arg0: int) -> None
Invoked with: <my_module.ClassTest object at 0x7f2980254fb0>, 'abc'
此示例展示了 pybind11 如何允许您毫不费力地将ClassTest
C++ 类暴露给 Python!
值得注意的是,Pybind11 自动从 C++ 代码中理解为name
,std::string
因此应该映射到 Pythonstr
对象。
编译会生成一个名为的文件class_test.cpython-36m-x86_64-linux-gnu.so
,该文件class_test_main.py
会自动作为class_test
本机定义模块的定义点。
也许只有当你尝试使用原生 Python API 手动执行相同的操作时,你才会意识到这有多么棒,例如,请参见这个示例,它的代码量大约是 10 倍:https://github.com/cirosantilli/python-cheat/blob/4f676f62e87810582ad53b2fb426b74eae52aad5/py_from_c/pure.c在这个例子中,你可以看到 C 代码如何痛苦地、明确地一点一点地定义 Python 类及其包含的所有信息(成员、方法、更多元数据……)。另请参阅:
python-C++扩展可以获取C++对象并调用其成员函数吗?
将 C++ 类实例暴露给 Python 嵌入式解释器
具有 Python C 扩展的类(而非方法)的完整且最小的示例?
在 C++ 中嵌入 Python 并使用 Boost.Python 从 C++ 代码调用方法
Python C++ 扩展中的继承
pybind11 声称与https://stackoverflow.com/a/145436/895245Boost.Python
中提到的类似,但更简约,因为它摆脱了 Boost 项目内部的臃肿:
pybind11 是一个轻量级的头文件库,可在 Python 中公开 C++ 类型,反之亦然,主要用于创建现有 C++ 代码的 Python 绑定。它的目标和语法类似于 David Abrahams 的优秀 Boost.Python 库:通过使用编译时自省推断类型信息来最大限度地减少传统扩展模块中的样板代码。
Boost.Python 的主要问题(也是创建类似项目的原因)是 Boost。Boost 是一套庞大而复杂的实用程序库,可与几乎所有现有的 C++ 编译器配合使用。这种兼容性有其代价:需要使用晦涩难懂的模板技巧和解决方法来支持最古老、错误最多的编译器样本。现在,兼容 C++11 的编译器已经广泛可用,这种笨重的机器已成为一种过大且不必要的依赖项。
可以将这个库视为 Boost.Python 的一个小型独立版本,其中删除了与绑定生成无关的所有内容。没有注释,核心头文件只需要约 4K 行代码,并且依赖于 Python(2.7 或 3.x,或 PyPy2.7 >= 5.7)和 C++ 标准库。这种紧凑的实现得益于一些新的 C++11 语言功能(具体来说:元组、lambda 函数和可变参数模板)。自创建以来,这个库在许多方面都超越了 Boost.Python,在许多常见情况下显著简化了绑定代码。
pybind11 也是当前 Microsoft Python C 绑定文档中重点介绍的唯一非原生替代方案:https://learn.microsoft.com/en-us/visualstudio/python/working-with-c-cpp-python-in-visual-studio ?view=vs-2019 (存档)。
在 Ubuntu 18.04、pybind11 2.0.1、Python 3.6.8、GCC 7.4.0 上测试。
解决方案 8:
我认为 python 的 cffi 可以是一个选择。
目标是从 Python 调用 C 代码。您应该无需学习第三种语言即可做到这一点:每种替代方案都要求您学习自己的语言(Cython、SWIG)或 API(ctypes)。因此,我们尝试假设您了解 Python 和 C,并尽量减少您需要学习的额外 API。
http://cffi.readthedocs.org/en/release-0.7/
解决方案 9:
我喜欢 cppyy,它可以很容易地用 C++ 代码扩展 Python,在需要时显著提高性能。
它功能强大,而且使用起来非常简单,
这是一个关于如何创建一个 numpy 数组并将其传递给 C++ 中的类成员函数的示例。
cppyy_测试.py
import cppyy
import numpy as np
cppyy.include('Buffer.h')
s = cppyy.gbl.Buffer()
numpy_array = np.empty(32000, np.float64)
s.get_numpy_array(numpy_array.data, numpy_array.size)
print(numpy_array[:20])
缓冲区.h
struct Buffer {
void get_numpy_array(double *ad, int size) {
for( long i=0; i < size; i++)
ad[i]=i;
}
};
您还可以非常轻松地创建 Python 模块(使用 CMake),这样您就可以避免一直重新编译 C++ 代码。
解决方案 10:
如果我理解正确的话,问题是如何从 Python 调用 C 函数。那么最好的选择是 Ctypes(顺便说一下,它可以移植到所有 Python 变体上)。
>>> from ctypes import *
>>> libc = cdll.msvcrt
>>> print libc.time(None)
1438069008
>>> printf = libc.printf
>>> printf("Hello, %s
", "World!")
Hello, World!
14
>>> printf("%d bottles of beer
", 42)
42 bottles of beer
19
如需详细指南,您可能需要参考我的博客文章。
解决方案 11:
Cython 绝对是最佳选择,除非您打算编写 Java 包装器,在这种情况下 SWIG 可能更可取。
我建议使用runcython
命令行实用程序,它使使用 Cython 的过程变得非常简单。如果您需要将结构化数据传递给 C++,请查看 Google 的 protobuf 库,它非常方便。
下面是我使用这两种工具制作的最小示例:
https://github.com/nicodjimenez/python2cpp
希望它可以成为一个有用的起点。
解决方案 12:
首先,你应该决定你的具体目的是什么。上面提到了有关扩展和嵌入 Python 解释器的官方 Python 文档,我可以添加一个关于二进制扩展的良好概述。用例可分为 3 类:
加速器模块:比在 CPython 中运行的等效纯 Python 代码运行速度更快。
包装模块:将现有的 C 接口暴露给 Python 代码。
低级系统访问:访问 CPython 运行时、操作系统或底层硬件的较低级功能。
为了给其他感兴趣的人提供更广阔的视角,并且由于您的初始问题有点模糊(“针对 C 或 C++ 库”),我认为这些信息可能会让您感兴趣。在上面的链接中,您可以阅读使用二进制扩展及其替代方案的缺点。
除了建议的其他答案之外,如果您想要加速器模块,可以尝试Numba。它的工作原理是“在导入时、运行时或静态地(使用包含的 pycc 工具)使用 LLVM 编译器基础结构生成优化的机器代码”。
扫码咨询,免费领取项目管理大礼包!