如何在Python中实现一个C++类,以便被C++调用?

38
我有一个用C++编写的类接口。 我有一些实现此接口的类也是用C++编写的。 这些在较大的C++程序的上下文中调用,该程序基本上实现了“main”。 我想能够在Python中编写此接口的实现,并允许它们在较大的C++程序的上下文中使用,就像它们只是用C++编写的一样。
有很多关于Python和C++接口的文章,但我无法完全弄清楚如何做到我想要的。 最接近的是这里:http://www.cs.brown.edu/~jwicks/boost/libs/python/doc/tutorial/doc/html/python/exposing.html#python.class_virtual_functions,但这还不太对。
更具体地说,假设我有一个现有的C++接口定义,类似于:
// myif.h
class myif {
   public:
     virtual float myfunc(float a);
};

我希望您能够翻译以下内容:

我希望能够做到类似以下的操作:

// mycl.py
... some magic python stuff ...
class MyCl(myif):
  def myfunc(a):
    return a*2

然后,在我的C++代码中,我希望能够这样说:

// mymain.cc
void main(...) {
  ... some magic c++ stuff ...
  myif c = MyCl();  // get the python class
  cout << c.myfunc(5) << endl;  // should print 10
}

I hope this is sufficiently clear ;)


4
我希望这足够清晰了。 C++用Python封装,然后再次用C++封装? - AJG85
2
你建造这个基础设施究竟希望实现什么目标? - Karl Knechtel
3
@AJG85并没有要求"wrapped in",他要求能够在Python中继承一个C++类,并在C++中使用它。(哎呀!) - Johan Lundberg
1
这听起来像是 SIP (http://www.riverbankcomputing.co.uk/software/sip/intro) 应该做的事情 - 很多与 PyQT 的工作都涉及到对 Qt 的 C++ 类进行子类化。话虽如此,我不知道它作为一个独立工具有多可用。 - millimoose
假设您有一个需要回调对象来通知客户端的C++库。此回调是使用C++抽象类实现的,应该被实现、创建并传递给库以便稍后调用。如果这个库将在Python中使用,您应该能够先在Python中实现接口,然后将其传递给C++库。这可以是一个用例。 - TonySalimi
6个回答

40

这个答案分为两部分。首先,您需要以一种方式在Python中公开您的接口,使得Python实现可以随意重写其中的某些部分。然后,您需要展示如何让C++程序(在main中)调用Python。


将现有接口暴露给Python:

第一部分可以使用SWIG很容易地完成。我稍微修改了您的示例场景以修复一些问题,并添加了一个额外的测试函数:

// myif.h
class myif {
   public:
     virtual float myfunc(float a) = 0;
};

inline void runCode(myif *inst) {
  std::cout << inst->myfunc(5) << std::endl;
}

目前我将研究在不将Python嵌入您的应用程序中的问题,即您在Python中启动异常,而不是在C ++的int main()中启动。不过稍后添加这一点相当简单。

首先要做的是让跨语言多态正常工作:

%module(directors="1") module

// We need to include myif.h in the SWIG generated C++ file
%{
#include <iostream>
#include "myif.h"
%}

// Enable cross-language polymorphism in the SWIG wrapper. 
// It's pretty slow so not enable by default
%feature("director") myif;

// Tell swig to wrap everything in myif.h
%include "myif.h"
为了实现这个功能,我们已经全局启用了 SWIG 的 director 特性,并针对我们的接口进行了特定设置。不过其余部分都是标准的 SWIG 内容。
我编写了一个测试 Python 实现:
import module

class MyCl(module.myif):
  def __init__(self):
    module.myif.__init__(self)
  def myfunc(self,a):
    return a*2.0

cl = MyCl()

print cl.myfunc(100.0)

module.runCode(cl)

有了这个,我能够编译并运行以下内容:

swig -python  -c++ -Wall myif.i 
g++ -Wall -Wextra -shared -o _module.so myif_wrap.cxx -I/usr/include/python2.7 -lpython2.7

python mycl.py 
200.0
10

这正是你希望从测试中看到的。


将Python嵌入应用程序:

接下来,我们需要实现一个真正的版本的mymain.cc。我已经草拟了一个大致的样子:

#include <iostream>
#include "myif.h"
#include <Python.h>

int main()
{
  Py_Initialize();

  const double input = 5.0;

  PyObject *main = PyImport_AddModule("__main__");
  PyObject *dict = PyModule_GetDict(main);
  PySys_SetPath(".");
  PyObject *module = PyImport_Import(PyString_FromString("mycl"));
  PyModule_AddObject(main, "mycl", module);

  PyObject *instance = PyRun_String("mycl.MyCl()", Py_eval_input, dict, dict);
  PyObject *result = PyObject_CallMethod(instance, "myfunc", (char *)"(O)" ,PyFloat_FromDouble(input));

  PyObject *error = PyErr_Occurred();
  if (error) {
    std::cerr << "Error occured in PyRun_String" << std::endl;
    PyErr_Print();
  }

  double ret = PyFloat_AsDouble(result);
  std::cout << ret << std::endl;

  Py_Finalize();
  return 0;
}

基本上它只是使用标准的Python嵌入其他应用程序。它可以正常工作,并提供了您所希望看到的结果:

g++ -Wall -Wextra -I/usr/include/python2.7 main.cc -o main -lpython2.7
./main
200.0
10
10

拼图的最后一块就是将从Python创建实例得到的PyObject*转换为myif *。 SWIG再次使这相当简单。

首先,我们需要要求SWIG向我们公开其运行时在头文件中的内容。我们可以通过额外调用SWIG来完成此操作:

swig -Wall -c++ -python -external-runtime runtime.h

接下来,我们需要重新编译SWIG模块,显式地给出SWIG知道的类型表的名称,以便我们可以在main.cc内部查找它。 我们使用以下命令重新编译.so文件:

g++ -DSWIG_TYPE_TABLE=myif -Wall -Wextra -shared -o _module.so myif_wrap.cxx -I/usr/include/python2.7 -lpython2.7

然后,在我们的main.cc中添加一个帮助函数,用于将PyObject*转换为myif*

#include "runtime.h"
// runtime.h was generated by SWIG for us with the second call we made

myif *python2interface(PyObject *obj) {
  void *argp1 = 0;
  swig_type_info * pTypeInfo = SWIG_TypeQuery("myif *");

  const int res = SWIG_ConvertPtr(obj, &argp1,pTypeInfo, 0);
  if (!SWIG_IsOK(res)) {
    abort();
  }
  return reinterpret_cast<myif*>(argp1);
}

现在我们已经完成这个步骤,可以从main()中使用它了:

int main()
{
  Py_Initialize();

  const double input = 5.5;

  PySys_SetPath(".");
  PyObject *module = PyImport_ImportModule("mycl");

  PyObject *cls = PyObject_GetAttrString(module, "MyCl");
  PyObject *instance = PyObject_CallFunctionObjArgs(cls, NULL);

  myif *inst = python2interface(instance);
  std::cout << inst->myfunc(input) << std::endl;

  Py_XDECREF(instance);
  Py_XDECREF(cls);

  Py_Finalize();
  return 0;
}

最后,我们需要使用-DSWIG_TYPE_TABLE=myif编译main.cc,得到如下结果:

./main
11

1
@JonoRR - 已修复。我使用的是模块句柄而不是其字典作为PyRun的参数。 - Flexo
1
我回答中包含了所有所需的源代码和命令,但我也将它们全部放在了网上:http://static.lislan.org.uk/~ajw/pyvirt.tar.gz - Flexo
您能否评论一下如何正确销毁创建的对象?快速测试似乎表明需要运行Py_DECREF(instance),而仅仅运行delete inst并不会释放Python对象。我理解的对吗? - mtvec
假设使用Python编程语言,“拥有”底层本地对象的Py_DECREF是必要且足够删除它的。如果您删除了接口并且Python拥有它,如果您不撤销所有权,则最终会出现双重删除。 - Flexo
谢谢解释!对于像我这样希望C++拥有对象所有权并能够以C++方式删除对象的人,这是你需要做的:1)通过运行mycl.MyCl().__disown__()创建实例,2)通过调用python2interface将实例转换为您的接口,3)在PyObject实例上运行Py_DECREF,4)当您完成对象时,使用delete销毁它,这也应该销毁Python对象(假设在Python中没有更多对它的引用,如果您使用Flexo的方法创建它,则不应该有)。 - mtvec
非常棒的解决方案。在Windows上使用VStudio,我必须创建两个单独的项目,一个用于生成_module.pyd(这是从_module.dll重命名的,但Python搜索*.pyd),另一个是实际的Main,即*.exe入口点。谢谢。 - Mihai Galos

12

以下是一个最简示例;需要注意的是由于Base不是纯虚函数而变得复杂。这是代码:

  1. baz.cpp:

    #include<string>
    #include<boost/python.hpp>
    using std::string;
    namespace py=boost::python;
    
    struct Base{
      virtual string foo() const { return "Base.foo"; }
      // fooBase is non-virtual, calling it from anywhere (c++ or python)
      // will go through c++ dispatch
      string fooBase() const { return foo(); }
    };
    struct BaseWrapper: Base, py::wrapper<Base>{
      string foo() const{
        // if Base were abstract (non-instantiable in python), then
        // there would be only this->get_override("foo")() here
        //
        // if called on a class which overrides foo in python
        if(this->get_override("foo")) return this->get_override("foo")();
        // no override in python; happens if Base(Wrapper) is instantiated directly
        else return Base::foo();
      }
    };
    
    BOOST_PYTHON_MODULE(baz){
      py::class_<BaseWrapper,boost::noncopyable>("Base")
        .def("foo",&Base::foo)
        .def("fooBase",&Base::fooBase)
      ;
    }
    
  2. bar.py

    import sys
    sys.path.append('.')
    import baz
    
    class PyDerived(baz.Base):
      def foo(self): return 'PyDerived.foo'
    
    base=baz.Base()
    der=PyDerived()
    print base.foo(), base.fooBase()
    print der.foo(), der.fooBase()
    
  3. Makefile

    default:
           g++ -shared -fPIC -o baz.so baz.cpp -lboost_python `pkg-config python --cflags`
    

结果如下:

Base.foo Base.foo
PyDerived.foo PyDerived.foo

您可以看到如何调用虚函数foo(),该函数解析为重写版本,无论在c++还是Python中,这是通过非虚拟c ++函数fooBase()实现的。您可以从c ++中派生一个类,并且它可以正常工作。

编辑(提取c++对象):

PyObject* obj; // given
py::object pyObj(obj); // wrap as boost::python object (cheap)
py::extract<Base> ex(pyObj); 
if(ex.check()){ // types are compatible
  Base& b=ex(); // get the wrapped object
  // ...
} else {
  // error
}

// shorter, thrwos when conversion not possible
Base &b=py::extract<Base>(py::object(obj))();

构建py::object并使用py::extractPyObject*中提取信息,查询Python对象是否与您要提取的内容匹配:PyObject * obj; py :: extract <Base> extractor (py :: object(obj)); 如果(!extractor.check()) /* 错误 */; Base&b=extractor();


这看起来非常不错 - 你能否将等效的Boost.Python嵌入解释器到我的示例中,该示例使用myif *python2interface(PyObject *obj);将PyObject转换回C ++接口的实现? (有了它,它将完全符合我对赏金的想法) - Flexo
我已经授予您此赏金,谢谢!我还将您的解决方案扩展到同时嵌入和扩展,这是我发布的第二个答案。 - Flexo

10

引自http://wiki.python.org/moin/boost.python/Inheritance

"Boost.Python还允许我们表示C++继承关系,以便将包装的派生类作为值、指针或引用传递给希望接受基类的参数。"

有虚函数的示例解决了第一部分(其中包括class MyCl(myif))。

要进行这样的具体示例,请参见http://wiki.python.org/moin/boost.python/OverridableVirtualFunctions

对于myif c = MyCl()这行代码,您需要将Python(模块)暴露给C ++。 这里有一些示例:http://wiki.python.org/moin/boost.python/EmbeddingPython


你能将其扩展为完整的示例吗?如果这能激发你,我已经为此设置了赏金 :) - Flexo
我不是那个给你点踩的人,但我怀疑你回答缺乏细节并且有些推测性质是导致这种情况发生的原因。这也是我为什么要对它进行悬赏的原因。 - Flexo

8

根据Eudoxos的回答(非常有帮助),我使用他的代码并扩展了它,现在有一个内嵌解释器,并带有一个内置模块。

这个回答是Boost.Python版本的我的基于SWIG的回答

头文件myif.h:

class myif {
public:
  virtual float myfunc(float a) const { return 0; }
  virtual ~myif() {}
};

这基本上就是问题中的要求,但默认实现为myfunc并具有虚析构函数。

对于Python实现,MyCl.py与问题中基本相同:

import myif

class MyCl(myif.myif):
  def myfunc(self,a): 
    return a*2.0

这样就只剩下了mymain.cc,其大部分基于Eudoxos的答案:
#include <boost/python.hpp>
#include <iostream>
#include "myif.h"

using namespace boost::python;

// This is basically Eudoxos's answer:
struct MyIfWrapper: myif, wrapper<myif>{
  float myfunc(float a) const {
    if(this->get_override("myfunc")) 
      return this->get_override("myfunc")(a);
    else 
      return myif::myfunc(a);
  }
};

BOOST_PYTHON_MODULE(myif){
  class_<MyIfWrapper,boost::noncopyable>("myif")
    .def("myfunc",&myif::myfunc)
  ;
}
// End answer by Eudoxos

int main( int argc, char ** argv ) {
  try {
    // Tell python that "myif" is a built-in module
    PyImport_AppendInittab("myif", initmyif);
    // Set up embedded Python interpreter:
    Py_Initialize();

    object main_module = import("__main__");
    object main_namespace = main_module.attr("__dict__");

    PySys_SetPath(".");
    main_namespace["mycl"] = import("mycl");

    // Create the Python object with an eval()
    object obj = eval("mycl.MyCl()", main_namespace);

    // Find the base C++ type for the Python object (from Eudoxos)
    const myif &b=extract<myif>(obj)();
    std::cout << b.myfunc(5) << std::endl;

  } catch( error_already_set ) {
    PyErr_Print();
  }
}

我在这里添加的关键部分,超越了“如何使用Boost.Python嵌入Python?”和“如何使用Boost.python扩展Python?”(由Eudoxos回答),是回答“如何在同一个程序中同时执行这两个操作?”的答案。解决方案在于调用PyImport_AppendInittab函数,该函数接受通常在加载模块时调用的初始化函数,并将其注册为内置模块。因此,当mycl.py说import myif时,它最终导入了内置的Boost.Python模块。

1

1
它的强大程度仅限于Python C API,但绝对是一个很棒的库。使用Boost似乎无法做到的一件事情是在C++中定义Python元类;但是使用C API可以实现。我正在寻找的一件事 - Paul Manta

-2

没有直接将C++代码与Python进行接口的真正方法。

SWIG可以处理这个问题,但它会构建自己的包装器。

我更喜欢的一种替代方法是ctypes,但要使用它,您需要创建一个C包装器。

例如:

// myif.h
class myif {
   public:
     virtual float myfunc(float a);
};

建立一个C语言的包装器,如下所示:

extern "C" __declspec(dllexport) float myif_myfunc(myif* m, float a) {
    return m->myfunc(a);
}

由于您正在使用C++进行构建,extern "C"允许使用C链接,因此您可以轻松地从dll中调用它,而__declspec(dllexport)允许从dll中调用该函数。

在Python中:

from ctypes import *
from os.path import dirname

dlldir = dirname(__file__)                      # this strips it to the directory only
dlldir.replace( '\\', '\\\\' )                  # Replaces \ with \\ in dlldir
lib = cdll.LoadLibrary(dlldir+'\\myif.dll')     # Loads from the full path to your module.

# Just an alias for the void pointer for your class
c_myif = c_void_p

# This tells Python how to interpret the return type and arguments
lib.myif_myfunc.argtypes = [ c_myif, c_float ]
lib.myif_myfunc.restype  = c_float

class MyCl(myif):
    def __init__:
        # Assume you wrapped a constructor for myif in C
        self.obj = lib.myif_newmyif(None)

    def myfunc(a):
        return lib.myif_myfunc(self.obj, a)

虽然SWIG可以为您完成所有这些工作,但是如果您想要自由地进行修改,而不必在重新生成SWIG包装器时感到沮丧,那么您的空间就很小。

ctypes的一个问题是它不能处理STL结构,因为它是为C语言设计的。SWIG可以为您处理此问题,但是您也可以自己在C语言中进行包装。这取决于您。

这是ctypes的Python文档:

http://docs.python.org/library/ctypes.html

此外,构建的dll应该与您的Python接口位于同一文件夹中(为什么不呢?)。

不过我很好奇,为什么您想从C++内部调用Python而不是直接调用C++实现呢?


1
“如果你想随心所欲地修改SWIG包装器,而不会因为需要重新生成SWIG包装器而感到沮丧,那么你的空间很小。你永远不应该手动编辑SWIG生成的文件。总有一种方法可以让SWIG生成你想要的代码。” - Flexo
就赏金而言,我看不出这个答案有多大的帮助 - 这并没有解决嵌入和“如何将从基接口派生的Python类型的实例传递回C++函数问题”的问题。我可以编写一个代理来处理这个问题,但我正在寻找一种涉及编写更少或比我提出的SWIG更整洁的粘合剂的解决方案。这似乎增加了更多手动编写的粘合剂,并没有解决在C++中嵌入Python的问题。 - Flexo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接