构造Python绑定到C或c++库的最快方法是什么?

(如果这很重要的话,我使用的是Windows。)


当前回答

Ctypes模块是标准库的一部分,因此比swig更稳定和更广泛可用,而swig总是给我带来问题。

使用ctypes,您需要满足python的任何编译时依赖,并且您的绑定将适用于任何具有ctypes的python,而不仅仅是它编译时针对的python。

假设你有一个简单的c++示例类,你想在一个名为foo.cpp的文件中进行对话:

#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

其他回答

最快的方法是使用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还可以用于用目标语言将结构和类打包为代理类——以非常自然的方式公开底层功能。

Ctypes模块是标准库的一部分,因此比swig更稳定和更广泛可用,而swig总是给我带来问题。

使用ctypes,您需要满足python的任何编译时依赖,并且您的绑定将适用于任何具有ctypes的python,而不仅仅是它编译时针对的python。

假设你有一个简单的c++示例类,你想在一个名为foo.cpp的文件中进行对话:

#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

Pybind11最小可运行示例

pybind11之前在https://stackoverflow.com/a/38542539/895245上提到过,但我想在这里给出一个具体的使用示例,并进一步讨论实现。

总而言之,我强烈推荐pybind11,因为它真的很容易使用:你只需要包含一个头文件,然后pybind11使用模板魔法来检查你想要公开给Python的c++类,并且是透明的。

这个模板魔法的缺点是,它会立即降低编译速度,为任何使用pybind11的文件增加几秒钟的时间,参见关于这个问题的调查。PyTorch表示同意。解决这一问题的建议已在https://github.com/pybind/pybind11/pull/2445上提出

下面是一个最小的可运行示例,让你感受一下pybind11有多棒:

class_test.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();
}

class_test_main.py

#!/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 \
  -o class_test`python3-config --extension-suffix` `python3-config --libs`
./class_test_main.py

Stdout输出:

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,因此应该映射到Python str对象。

编译生成一个名为class_test.cpython-36m-x86_64-linux-gnu的文件。因此,class_test_main.py自动选择哪个class_test_main.py作为class_test原生定义模块的定义点。

Perhaps the realization of how awesome this is only sinks in if you try to do the same thing by hand with the native Python API, see for example this example of doing that, which has about 10x more code: https://github.com/cirosantilli/python-cheat/blob/4f676f62e87810582ad53b2fb426b74eae52aad5/py_from_c/pure.c On that example you can see how the C code has to painfully and explicitly define the Python class bit by bit with all the information it contains (members, methods, further metadata...). See also:

python- c++扩展可以获得一个c++对象并调用其成员函数吗? 将c++类实例暴露给python嵌入式解释器 一个完整的和最小的例子类(不是方法)与Python C扩展? 在c++中嵌入Python,并使用Boost从c++代码中调用方法。Python Python c++扩展中的继承

pybind11声称类似于Boost。在https://stackoverflow.com/a/145436/895245上提到了Python,但更简单,因为它从Boost项目内部的膨胀中解放出来:

pybind11 is a lightweight header-only library that exposes C++ types in Python and vice versa, mainly to create Python bindings of existing C++ code. Its goals and syntax are similar to the excellent Boost.Python library by David Abrahams: to minimize boilerplate code in traditional extension modules by inferring type information using compile-time introspection. The main issue with Boost.Python—and the reason for creating such a similar project—is Boost. Boost is an enormously large and complex suite of utility libraries that works with almost every C++ compiler in existence. This compatibility has its cost: arcane template tricks and workarounds are necessary to support the oldest and buggiest of compiler specimens. Now that C++11-compatible compilers are widely available, this heavy machinery has become an excessively large and unnecessary dependency. Think of this library as a tiny self-contained version of Boost.Python with everything stripped away that isn't relevant for binding generation. Without comments, the core header files only require ~4K lines of code and depend on Python (2.7 or 3.x, or PyPy2.7 >= 5.7) and the C++ standard library. This compact implementation was possible thanks to some of the new C++11 language features (specifically: tuples, lambda functions and variadic templates). Since its creation, this library has grown beyond Boost.Python in many ways, leading to dramatically simpler binding code in many common situations.

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上测试。

我从本页开始了我的Python <-> c++绑定之旅,目标是链接高级数据类型(多维STL向量与Python列表):-)

已经尝试了基于ctypes和boost的解决方案。当需要高级数据类型绑定时,我发现它们很复杂,而在这种情况下,我发现SWIG要简单得多。

因此,本例使用SWIG,并且已经在Linux中进行了测试(但SWIG是可用的,并且在Windows中也广泛使用)。

目标是为Python提供一个c++函数,该函数接受二维STL向量形式的矩阵,并返回每一行的平均值(作为一维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

我喜欢cppyy,它使得用c++代码扩展Python变得非常容易,在需要时极大地提高了性能。

它功能强大,坦白说使用起来非常简单,

这里是一个如何创建numpy数组并将其传递给c++中的类成员函数的示例。

cppyy_test.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])

Buffer.h

struct Buffer {
  void get_numpy_array(double *ad, int size) {
    for( long i=0; i < size; i++)
        ad[i]=i;
  }
};

你也可以很容易地创建一个Python模块(使用CMake),这样你就可以避免一直重新编译c++代码。