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

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


当前回答

对于现代c++,使用cppyy: http://cppyy.readthedocs.io/en/latest/

它基于Clang/LLVM的c++解释器kling。绑定是在运行时进行的,不需要额外的中间语言。得益于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的作者。

其他回答

问题是如何从Python中调用C函数,如果我理解正确的话。那么最好的选择是c类型(顺便说一句,可移植到所有Python变体)。

>>> from ctypes import *
>>> libc = cdll.msvcrt
>>> print libc.time(None)
1438069008
>>> printf = libc.printf
>>> printf("Hello, %s\n", "World!")
Hello, World!
14
>>> printf("%d bottles of beer\n", 42)
42 bottles of beer
19

关于详细的指南,你可以参考我的博客文章。

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

还有pybind11,它类似于Boost的轻量级版本。Python和兼容所有现代c++编译器:

https://pybind11.readthedocs.io/en/latest/

我从本页开始了我的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++代码。