简体   繁体   English

创建与 cppcoro 生成器的 Python 绑定

[英]Creating Python binding to cppcoro generator

I am trying to create a class in C++ with a generator method to run in Python, and I need the method to return lists of values.我正在尝试使用生成器方法在 C++ 中创建一个类以在 Python 中运行,并且我需要该方法来返回值列表。 For coroutines I am using a maintained fork of cppcoro .对于协程,我使用的是cppcoro的维护分支。

Here's an example:这是一个例子:

#include <vector>
#include <experimental/random>
#include <cppcoro/generator.hpp>

class RandomVectorGenerator{
    int Range;
    int Limit;
public:
    RandomVectorGenerator(int range, int limit): Range(range), Limit(limit){}

    cppcoro::generator<std::vector<int>> get_random_vector(){
        for(int i = 0; i < Limit; i++) {

            int random_lenght = std::experimental::randint(0, Range);
            std::vector<int> random_vector;

            for (int i = 0; i < random_lenght; i++) {
                uint32_t random_value = std::experimental::randint(0, Range);
                random_vector.push_back(random_value);
            }
            co_yield random_vector;
        }
        co_return;
    }

};

Given Range and Limit , this class can generate up to Limit integer vectors, with 0 to Range values from 0 to Range .给定RangeLimit ,此类最多可以生成Limit整数向量,其中 0 到Range值从 0 到Range

Using it in C++ as follows:在 C++ 中使用它如下:

int main() {
    RandomVectorGenerator generator = RandomVectorGenerator(5, 5);
    auto gen = generator.get_random_vector();
    auto iter = gen.begin();
    while (true) {
        std::vector<int> solution = *iter;
        for (int j = 0; j < solution.size(); j++) {
            std::cout << solution[j] << " ";
        }
        std::cout << std::endl;
        ++iter;
        if (iter == gen.end()) break;
    }
    return 0;
}

As expected I might get an output as such正如预期的那样,我可能会得到这样的输出

2 2 4 1 
0 5 2 

0 
2 4 

If I bind the class and its' methods to python as follows:如果我将类及其方法绑定到 python,如下所示:

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

PYBIND11_MODULE(random_vectors, m) {
    py::class_<RandomVectorGenerator>(m, "random_vectors")
    .def(py::init<int, int>())
    .def("__iter__", [](RandomVectorGenerator &generator) { 
        auto gen = generator.get_random_vector(); 
        return py::make_iterator(gen.begin(), gen.end()); 
        },
        py::keep_alive<0, 1>());
};

This binding compiles and creates an importable module.此绑定编译并创建一个可导入模块。 However, when I proceed to use the iterator,但是,当我继续使用迭代器时,

from random_vectors import random_vectors

generator = random_vectors(5, 5)
iterator = iter(generator)

print(next(iterator))

Running the code above in a fresh kernel causes next(iterator) to raise StopIteration .在新内核中运行上面的代码会导致next(iterator)引发StopIteration

Runnning it after the first time gives output.在第一次之后运行它会给出输出。 The output lenght is of the expected range, but the values are all over the place, for example [1661572905, 5, 1514791955, -1577772014]输出长度在预期范围内,但值到处都是,例如[1661572905, 5, 1514791955, -1577772014]

further more if I call next(iterator) again, the kernel silently crashes.更进一步,如果我再次调用next(iterator) ,内核会默默地崩溃。

I can reproduce the behaviour on C++ side by modifying int main() as such:我可以通过修改int main()来重现 C++ 端的行为:

int main() {
    RandomVectorGenerator generator = RandomVectorGenerator(5, 5);
    auto iter = generator.get_random_vector().begin();             //Here's a change
    while (true) {
        std::vector<int> solution = *iter;
        for (int j = 0; j < solution.size(); j++) {
            std::cout << solution[j] << " ";
        }
        std::cout << std::endl;
        ++iter;
        if (iter == generator.get_random_vector().end()) break;    //Also here
    }
    return 0;
}

This gives the same output as in python, but does not crash silently, it happens right at ++iter , and the message is Segmentation fault (core dumped)这给出了与 python 中相同的输出,但不会静默崩溃,它发生在++iter ,并且消息是Segmentation fault (core dumped)

My guess is that the issue with the binding is that the gen object in the binding is created temporarily and does not remain after creation of the iterator.我的猜测是绑定的问题是绑定中的gen对象是临时创建的,并且在创建迭代器后不会保留。 I tried changing the py::keep_alive arguments, but to no avail.我尝试更改py::keep_alive参数,但无济于事。

I am convinced that for this to work, the begin() and end() methdods have to be part of the whole class, just like it is in the pybind11 examples on iterators, but I can not define them just like in the examples, because the generator method has to first be initialized.我确信要使其工作, begin()end()方法必须是整个类的一部分,就像它在迭代器上的 pybind11 示例中一样,但我不能像在示例中那样定义它们,因为必须首先初始化生成器方法。

Thus my conclusion is that RandomVectorGenerator has to be derived from the cppcoro::generator, if that is right, how would I go about this?因此我的结论是RandomVectorGenerator必须从 cppcoro::generator 派生,如果这是正确的,我将如何处理?

First up, the solution I got earlier ended up being broken.首先,我之前得到的解决方案最终被打破了。 I am still going to describe it at the end of this answer for reference.我仍然会在这个答案的末尾描述它以供参考。 It behaved in an interesting manner, very rarely, under seemingly normal conditions, it worked.它以一种有趣的方式表现,很少见,在看似正常的条件下,它起作用了。

I had to resort to a workaround, the resulting python class does not have an __iter__ method, but it's good enough because I can then create a children class on python side after compilation.我不得不求助于一种解决方法,生成的 python 类没有__iter__方法,但这已经足够好了,因为我可以在编译后在 python 端创建一个子类。

Solution is as follows:解决方法如下:

class RandomVectorGenerator{
    int Range;
    int Limit;
    cppcoro::generator<std::vector<int>> generator;
    cppcoro::detail::generator_iterator<std::vector<int> > iter;
    cppcoro::detail::generator_sentinel end;
public:
    RandomVectorGenerator(int range, int limit): Range(range), Limit(limit){}

    void make_generator(){
        generator = this->get_random_vector();
        iter = generator.begin();
        end = generator.end();
    }

    std::vector<int> get_next(){
        if (iter == end) throw py::stop_iteration("end reached");
        std::vector<int> vect = *iter;
        ++iter;
        return vect;
    }

    cppcoro::generator<std::vector<int>> get_random_vector(){
        for(int i = 0; i < Limit; i++) {
            int random_lenght = std::experimental::randint(0, Range);
            std::vector<int> random_vector;
            for (int i = 0; i < random_lenght; i++) {
                uint32_t random_value = std::experimental::randint(0, Range);
                random_vector.push_back(random_value);
            }
            co_yield random_vector;
        }
        co_return;
    }
};

What I've done is created a method which creates an iterator internally from the cppcoro generator ( make_generator ), which is then wrapped in an equivalent of __next__ ( get_next ).我所做的是创建了一个方法,该方法在内部从cppcoro生成器( make_generator )创建迭代器,然后将其包装在等效的__next__get_next )中。

This class is then bound to python as such:然后这个类被绑定到python,如下所示:

PYBIND11_MODULE(random_vectors, m) {
    py::class_<RandomVectorGenerator>(m, "random_vectors")
    .def(py::init<int, int>())
    .def("make_generator", &RandomVectorGenerator::make_generator)
    .def("__next__", &RandomVectorGenerator::get_next);
};

Given that I am going to create a child class in python afterwards, binding get_next to __next__ isn't strictly necessary, however, it felt cute .鉴于之后我将在 python 中创建一个子类,将__next__绑定到get_next并不是绝对必要的,但是,它感觉很可爱 Note that nowhere do I have to release the GIL.请注意,我无需在任何地方发布 GIL。

On Python side, I then utilize this bound class like so:在 Python 方面,我然后像这样使用这个绑定类:

from random_vectors import random_vectors

class generator(random_vectors):
    def __init__(self, Range, Limit):
        super().__init__(Range, Limit)
    
    def __iter__(self):
        self.make_generator()
        return self

for i in generator(5,5):
    print(i)

It works as expected, the solution just ends up somewhat hacky.它按预期工作,解决方案最终有点hacky。 However, the result is that I can now utilize C++ coroutines in python.然而,结果是我现在可以在 python 中使用 C++ 协程。 Plus I was able to make this work for my more complex use case (Dancing Links), however it crashes for larger problems, so the quest is not complete, but it's something.另外,我能够为我更复杂的用例(Dancing Links)完成这项工作,但是它会因更大的问题而崩溃,所以任务并不完整,但它是一些东西。


As for the broken solution.至于破碎的解决方案。 It was much more elegant that what I have above.它比我上面的要优雅得多。

This was the solution:这是解决方案:

class RandomVectorGenerator{
    int Range;
    int Limit;
public:

    RandomVectorGenerator(int range, int limit): Range(range), Limit(limit){
        generator = this->get_random_vector();}

    cppcoro::generator<std::vector<int>> generator;

    cppcoro::generator<std::vector<int>> get_random_vector(){
        for(int i = 0; i < Limit; i++) {
            int random_lenght = std::experimental::randint(0, Range);
            std::vector<int> random_vector;
            for (int i = 0; i < random_lenght; i++) {
                uint32_t random_value = std::experimental::randint(0, Range);
                random_vector.push_back(random_value);
            }
            co_yield random_vector;
        }
        co_return;
    }
};

It is quite similar to the one above, but rather than creating a __next__ I tried to create an __iter__ with pybind11 .它与上面的非常相似,但我没有创建一个__next__ ,而是尝试使用pybind11创建一个__iter__

Binding was done like so:绑定是这样完成的:

PYBIND11_MODULE(random_vectors, m) {
    py::class_<RandomVectorGenerator>(m, "random_vectors")
    .def(py::init<int, int>())
    .def("__iter__", [](RandomVectorGenerator &generator) {py::gil_scoped_release release;
        return py::make_iterator(generator.generator.begin(), generator.generator.end());
        },
         py::keep_alive<0, 1>());
};

Code above compiled and was used in python like so:上面的代码编译并在 python 中使用,如下所示:

from random_vectors import random_vectors

rv = random_vectors(15,10)

iterator = iter(rv)

while True:
    try:
        print(next(iterator))
    except StopIteration:
        break

Running it from terminal with -X dev resulted in segmentation error, line 7 being iterator = iter(rv) :使用-X dev从终端运行它会导致分段错误,第 7 行是iterator = iter(rv)

Fatal Python error: Segmentation fault

Current thread 0x00007f8c889f1740 (most recent call first):
  File "path-to-scrip/test.py", line 7 in <module>

Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

This happened every time when it was executed from terminal, however, very rarely running it in Spyder 5.1.5 on Python 3.9.12 it worked and provided the expected output.每次从终端执行时都会发生这种情况,但是,很少在 Python 3.9.12 上的 Spyder 5.1.5 中运行它并提供预期的输出。 The way this happened was very inconsistent, but my observation was this:这种情况发生的方式非常不一致,但我的观察是这样的:

  1. Try running it once in a fresh console尝试在新的控制台中运行一次
  2. It fails and restarts kernel它失败并重新启动内核
  3. Restart kernel manually手动重启内核
  4. Very rarely it started working in said console它很少开始在所述控制台中工作

Another observation was that the process of trial and error caused temporary freezing and unresponsiveness.另一个观察结果是,反复试验的过程会导致暂时的冻结和反应迟钝。 I also would like to note that it never worked in my actual application, which is much more complex.我还想指出,它从未在我的实际应用程序中工作过,这要复杂得多。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM