将一个C++类实例暴露给Python嵌入式解释器

22
我正在寻找一种简单的方法来将一个 C++ 类实例暴露给嵌入式 Python 解释器。具体要求如下:
- 我有一个 C++ 库,已经使用 swig 进行了封装,可以从 Python 解释器中使用。 - 我有一个 C++ 主程序,其中实例化了来自库中的 Foo 类,并嵌入了一个 Python 解释器。 - 我想将我的 C++ world 中的 Foo 类实例暴露给 Python world(并作为一个 Foo 类)。
请问这是否可行,如果是,应该怎么做?
我认为问题与boost::python::ptr or PyInstance_New usage的第一个答案几乎相同。我猜这意味着我应该使用 boost.Python 来封装我的库?
我的唯一目标是在嵌入式 Python 解释器中操作我的 C++ Foo 实例(不确定之前的方法是否适用)。实际上,我已经使用 swig 将我的 Foo 类暴露给了 Python。
以下是我拥有的:
我的 Foo 类:
class Foo{...};

我的封装库(包括Foo类)暴露给Python:这样我就可以启动Python解释器并像这样做些事情:

import my_module
foo=my_modulde.Foo()

我想要的是: 具有C++主程序的嵌入式Python解释器,可以操作C++世界变量。
int main(int argc, char **argv)
{
    Foo  foo;   // instanciates foo
    
    Py_Initialize();

    Py_Main(argc, argv); // starts the python interpreter
                         // and manipulates THE foo instance in it

    Py_Finalize();
    
    return 0;
}
4个回答

21

Boost python允许您以非常紧密集成的方式将C++类暴露给Python - 您甚至可以将它们包装起来,以便您可以从C++类派生Python类,并将虚拟方法解析为Python重写。

boost python教程是一个很好的开始。


编辑:

您可以创建一个C++对象,并像这样将其引用传递给内部的Python解释器:

#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>
#include <boost/python.hpp>
#include <string>
#include <iostream>

namespace bp = boost::python;

struct Foo{
    Foo(){}
    Foo(std::string const& s) : m_string(s){}
    void doSomething() {
        std::cout << "Foo:" << m_string << std::endl;
    }
    std::string m_string;
};

typedef boost::shared_ptr<Foo> foo_ptr;

BOOST_PYTHON_MODULE(hello)
{
    bp::class_<Foo, foo_ptr>("Foo")
        .def("doSomething", &Foo::doSomething)
    ;
};

int main(int argc, char **argv)
{
    Py_Initialize();
    try {
        PyRun_SimpleString(
            "a_foo = None\n"
            "\n"
            "def setup(a_foo_from_cxx):\n"
            "    print 'setup called with', a_foo_from_cxx\n"
            "    global a_foo\n"
            "    a_foo = a_foo_from_cxx\n"
            "\n"
            "def run():\n"
            "    a_foo.doSomething()\n"
            "\n"
            "print 'main module loaded'\n"
        );

        foo_ptr a_cxx_foo = boost::make_shared<Foo>("c++");

        inithello();
        bp::object main = bp::object(bp::handle<>(bp::borrowed(
            PyImport_AddModule("__main__")
        )));

        // pass the reference to a_cxx_foo into python:
        bp::object setup_func = main.attr("setup");
        setup_func(a_cxx_foo);

        // now run the python 'main' function
        bp::object run_func = main.attr("run");
        run_func();
    }
    catch (bp::error_already_set) {
        PyErr_Print();
    }

    Py_Finalize();

    return 0;
}

谢谢您的回答,我也会考虑boost.Python。我已经知道boost.Python可以公开C++类的功能,但我有很多遗留代码要封装。因此,对我来说swig似乎是更好的解决方案(更快?更简单?还有更多的语言目标)...:(我在这里找到了你以前的答案链接。它仍然有些模糊 :) 但我感觉这就是我要找的东西(不仅仅是一个实例,而是整体思路)?我希望能用swig和python api做到这一点?:( - jineff
我已经添加了一个示例,尝试使用boost::python回答您的确切问题 - 恐怕我无法帮助swig。 - James
非常感谢!这正是我想要的(除了它是boost.python)。我已经将您的答案标记为有用(实际上非常有用:))。我会等待其他回复(特别是关于swig的),并深入研究您的解决方案。 - jineff

7

作为参考,您可以使用pybind11来实现这一点:

#include <iostream>
#include <pybind11/pybind11.h>
namespace py = pybind11;

// Define C++ class "Foo"
class Foo {
    std::string s_;
public:
    Foo(const std::string &s) : s_(s) {}
    void doSomething() { std::cout << s_ << std::endl; }
};
typedef std::shared_ptr<Foo> FooPtr;

// Define Python module "bar" and Python class "bar.Foo" wrapping the C++ class
PYBIND11_MODULE(bar, m) {
    py::class_<Foo, FooPtr>(m, "Foo")
        .def("doSomething", &Foo::doSomething);
}

int main(int argc, char **argv)
{
    // Create a C++ instance of Foo
    FooPtr foo = std::make_shared<Foo>("Hello, World!");

    // Initialize Python interpreter and import bar module
    PyImport_AppendInittab("bar", PyInit_bar);
    Py_Initialize();
    PyRun_SimpleString("import bar");

    // Make C++ instance accessible in Python as a variable named "foo"
    py::module main = py::module::import("__main__");
    main.attr("foo") = foo;

    // Run some Python code using foo
    PyRun_SimpleString("foo.doSomething()");

    // Finalize the Python interpreter
    Py_Finalize();
    return 0;
}

1
非常感谢您的回答,它完美地解决了我的问题! - Telmo Trooper
似乎在那些年里发生了一些变化...我已经能够使用Python 3.9和当前稳定的pybind11编译和运行它,它可以工作,但是在进程退出(PyFinalize之后)时会崩溃。 - gabry
@gabry 感谢您的反馈。我不确定为什么这段代码不再有效,但是在最近几年,pybind11已经添加了便利设施来嵌入Python解释器。您应该能够使用pybind11中的所有内容来替换所有Py_Initialize/PyRun_SimpleString/Py_Finalize,例如 py::scoped_interpreter guard{}; py::exec("foo.doSomething()")。请参阅:https://pybind11.readthedocs.io/en/stable/advanced/embedding.html。 - Boris Dalstein
@gabry 哦,我刚看到你最近的回答,看起来你已经搞定了 :) - Boris Dalstein

2

我是一名有用的助手,可以为您进行翻译。

我知道这是一个老问题,但是这里有一个使用SWIG的解决方案。

foo.h:

#pragma once
#include <string>

struct Foo{
  Foo();
  Foo(std::string const& s);
  void doSomething();
  std::string m_string;
};

foo.cpp:

#include "foo.h"
#include <iostream>

Foo::Foo() {}

Foo::Foo(std::string const& s) : m_string(s) {}

void Foo::doSomething() {
  std::cout << "Foo:" << m_string << std::endl;
}

foo.i:

%module module
%{
  #include "foo.h"
%}

%include "std_string.i"
%include "foo.h"

生成常规的SWIG包装器和运行时。
swig -python -c++ -Wall foo.i
swig -python -c++ -Wall -external-runtime runtime.h

生成包含struct Foo的SWIG模块:
g++ -fPIC -Wall -Wextra -shared -o _module.so foo_wrap.cxx foo.cpp -I/usr/include/python2.7 -lpython2.7

如果想在多个模块之间共享类型信息,可以添加一个参数-DSWIG_TYPE_TABLE = SomeName
现在,这里是如何将C ++中的 Foo 实例传递给解释器。
#include "foo.h"
#include <Python.h>
#include "runtime.h"

int main(int argc, char **argv) {
  Py_Initialize();

  PyObject* syspath = PySys_GetObject((char*)"path");
  PyObject* pName = PyString_FromString((char*) ".");
  int err = PyList_Insert(syspath, 0, pName);
  Py_DECREF(pName);

  err = PySys_SetObject((char*) "path", syspath);

  PyObject *main, *module, *pInstance, *run, *setup;

  try {
    main = PyImport_ImportModule("__main__");
    err = PyRun_SimpleString(
        "a_foo = None\n"
        "\n"
        "def setup(a_foo_from_cxx):\n"
        "    print 'setup called with', a_foo_from_cxx\n"
        "    global a_foo\n"
        "    a_foo = a_foo_from_cxx\n"
        "\n"
        "def run():\n"
        "    a_foo.doSomething()\n"
        "\n"
        "print 'main module loaded'\n");

    // Load Python module
    module = PyImport_ImportModule("module");

    swig_type_info *pTypeInfo = nullptr;
    pTypeInfo = SWIG_TypeQuery("Foo *");

    Foo* pFoo = new Foo("Hello");
    int owned = 1;
    pInstance =
        SWIG_NewPointerObj(reinterpret_cast<void*>(pFoo), pTypeInfo, owned);

    setup = PyObject_GetAttrString(main, "setup");

    PyObject* result = PyObject_CallFunctionObjArgs(setup, pInstance, NULL);
    Py_DECREF(result);

    run = PyObject_GetAttrString(main, "run");

    result = PyObject_CallFunctionObjArgs(run, NULL);
    Py_DECREF(result);
  }
  catch (...) {
    PyErr_Print();
  }

  Py_DECREF(run);
  Py_DECREF(setup);
  Py_DECREF(pInstance);
  Py_DECREF(module);
  Py_DECREF(main);

  Py_Finalize();
  return 0;
}

以上内容可以通过以下方式进行编译:
g++ -Wall -Wextra -I/usr/include/python2.7 main.cpp foo.cpp -o main -lpython2.7

1

2023 答案

看起来 pybind11 现在真的很容易做到这一点,他们有一个专门的头文件用于将 pybind 用于嵌入目的以及一些详细的文档:

https://pybind11.readthedocs.io/en/stable/advanced/embedding.html

pybind11是一个仅包含头文件的库,因此它是跨平台的,并且不会添加额外的依赖项(除了Python解释器)

以下是解决原始帖子问题的代码:

#include <pybind11/embed.h>
#include <iostream>

struct Foo{
    Foo(const std::string & s) : m_string(s){}
    void doSomething() {
        std::cout << "Foo:" << m_string << std::endl;
    }
    std::string m_string;
};
typedef std::shared_ptr<Foo> FooPtr;

namespace py = pybind11;

PYBIND11_EMBEDDED_MODULE(bar, m) {
    py::class_<Foo, FooPtr>(m, "Foo")
        .def("doSomething", &Foo::doSomething);
}

int main(int argc, char **argv)
{
    py::scoped_interpreter guard{};
    FooPtr foo = std::make_shared<Foo>("Hello, World!");

    py::module::import("bar");
    py::module main = py::module::import("__main__");
    main.attr("foo") = foo;

    // Run some Python code using foo
    py::exec("foo.doSomething()");
}

这里是一个基本的 CMakeLists.txt,可以用于跨平台设置嵌入Python的项目,pybind11 应该在 external 子文件夹中,并且您的源代码应该命名为 test.cpp

cmake_minimum_required(VERSION 3.19)
project(test_embed)
find_package(Python3 COMPONENTS Development)
set(CMAKE_CXX_STANDARD 17)

include_directories(extern/pybind11/include ${Python3_INCLUDE_DIRS})
add_executable(test test.cpp)
target_link_options(test PRIVATE ${Python3_LINK_OPTIONS})
target_link_libraries(test PRIVATE ${Python3_LIBRARIES})

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