C++ 20 协程与 PyBind11

C++ 20 coroutines with PyBind11

我正在尝试使用 PyBind11 获得一个简单的基于 C++ 20 的生成器模式。这是代码:

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

struct Generator2 {
    Generator2(){}
    struct Promise;
    using promise_type=Promise;
    std::coroutine_handle<Promise> coro;
    Generator2(std::coroutine_handle<Promise> h): coro(h) {}
    ~Generator2() {
        if(coro)
            coro.destroy();
    }
    int value() {
        return coro.promise().val;
    }
    bool next() {
        std::cout<<"calling coro.resume()";
        coro.resume();
        std::cout<<"coro.resume() called";
        return !coro.done();
    }
    struct Promise {
        void unhandled_exception() {std::rethrow_exception(std::move(std::current_exception()));}
        int val;
        Generator2 get_return_object() {
            return Generator2{std::coroutine_handle<Promise>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() {
            return {};
        }
        std::suspend_always yield_value(int x) {
            val=x;
            return {};
        }
        std::suspend_never return_void() {
            return {};
        }
        std::suspend_always final_suspend() noexcept {
            return {};
        }
    };
};


Generator2 myCoroutineFunction() {
    for(int i = 0; i < 100; ++i) {
        co_yield i;
    }
}

class Gen{
private:
    Generator2 myCoroutineResult;
public:
    
    Gen(){
        myCoroutineResult = myCoroutineFunction();
    }

    int next(){
        return (myCoroutineResult.next());
    }
};


PYBIND11_MODULE(cmake_example, m) {
    pybind11::class_<Gen>(m, "Gen")
            .def(pybind11::init())
            .def("next", &Gen::next);
}

但是我收到一个错误:

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

C++ 协程、coroutine_handles、co_yield 等是否是 PyBind11 尚不支持的低级事物?

即使 PyBind11 不直接支持协程,您的问题也不会混合协程和 pybind 代码,因为无论如何您都将协程隐藏在 Gen 后面。

问题是您的 Generator2 类型使用编译器提供的复制和移动构造函数。

这一行:

myCoroutineResult = myCoroutineFunction();

当你调用myCoroutineFunction时创建一个协程句柄,并把它放在右边的临时Generator2中。然后,从右侧生成器初始化 myCoroutineResult。一切都很好,但是临时的被破坏了。您的析构函数检查句柄是否有效:

~Generator2() {
    if(coro)
        coro.destroy();
}

但是在您的实现中,成员生成器的 coro 成员是从临时文件中复制的,而不重置临时文件的 coro 成员。因此,一旦您初始化 myCoroutineResult,协程本身就会被销毁,并且您持有一个悬空的协程句柄。请记住,std::coroutine_handles 的行为类似于原始指针。

本质上,您违反了规则 5。您有自定义析构函数,但没有 copy/move 构造函数或赋值运算符。由于你不能复制构造协程,你可以忽略复制构造函数但你需要提供 move constructors/assigment operators:

Generator2(Generator2&& rhs) : coro{std::exchange(rhs.coro, nullptr)} {
    // rhs will not delete our coroutine, 
    // since we put nullptr to its coro member
}

Generator2& operator=(Generator2&& rhs) {
    if (&rhs == this) {
        return *this;
    }
    if (coro) {
        coro.destroy();
    }
    coro = std::exchange(rhs.coro, nullptr);
    return *this;
}

此外,使用成员初始化列表来初始化成员,而不是在构造函数体内分配它们。所以不是这个:

Gen(){
    myCoroutineResult = myCoroutineFunction();
}

使用这个:

Gen() : myCoroutineResult{myCoroutineFunction()} {}

即使在这个答案中也能看出推理。第一个调用赋值运算符,它执行大量额外的工作,而第二个调用移动构造函数,它尽可能精简。