构造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

其他回答

我喜欢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++代码。

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

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

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

你应该看看Boost.Python。以下是他们网站上的简短介绍:

The Boost Python Library is a framework for interfacing Python and C++. It allows you to quickly and seamlessly expose C++ classes functions and objects to Python, and vice-versa, using no special tools -- just your C++ compiler. It is designed to wrap C++ interfaces non-intrusively, so that you should not have to change the C++ code at all in order to wrap it, making Boost.Python ideal for exposing 3rd-party libraries to Python. The library's use of advanced metaprogramming techniques simplifies its syntax for users, so that wrapping code takes on the look of a kind of declarative interface definition language (IDL).