C++ 20 个带有 PyBind11 的协程

发布于 2025-01-12 23:29:02 字数 1931 浏览 1 评论 0原文

我正在尝试使用 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);
}

但是我收到错误:

进程已完成,退出代码为 139(被信号 11:SIGSEGV 中断)

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

I'm trying to get a simple C++ 20 based generator pattern work with PyBind11. This is the code:

#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);
}

However I'm getting an error:

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

Could c++ coroutines, coroutine_handles, co_yield etc. be a low-level thing that is not supported by PyBind11 yet?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

救星 2025-01-19 23:29:02

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

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

这一行:

myCoroutineResult = myCoroutineFunction();

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

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

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

本质上,您违反了规则 5。您有一个自定义析构函数,但没有复制/移动构造函数或赋值运算符。由于无法复制构造协程,因此可以忽略复制构造函数,但需要提供移动构造函数/分配运算符:

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()} {}

即使在这个答案中也可以看到推理。第一个调用赋值运算符,它执行一系列额外的工作,而第二个调用移动构造函数,它非常精简。

Even though PyBind11 does not support coroutines directly, your problem does not mix coroutine and pybind code since you are hiding the coroutine behind Gen anyway.

The problem is that your Generator2 type uses the compiler provided copy and move constructors.

This line:

myCoroutineResult = myCoroutineFunction();

Creates a coroutine handle when you call myCoroutineFunction, and puts it in the temporary Generator2 in the right hand side. Then, you initialize myCoroutineResult from the right hand side generator. All is well, but then the temporary gets destroyed. Your destructor checks whether the handle is valid or not:

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

But in your implementation, the coro member of the member generator gets copied from the temporary without resetting the temporary's coro member. So the coroutine itself gets destroyed once you initialize myCoroutineResult, and you are holding onto a dangling coroutine handle. Remember that std::coroutine_handles behave like a raw pointer.

Essentially, you have a violation of the rule of 5. You have a custom destructor, but no copy/move constructors or assignment operators. Since you cannot copy construct a coroutine, you can ignore the copy constructors but you need to provide 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;
}

Also, use member initialization list to initialize members instead of assigning them within the constructor body. So instead of this:

Gen(){
    myCoroutineResult = myCoroutineFunction();
}

Use this:

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

The reasoning can be seen even in this answer. The first one calls the assignment operator, which performs a bunch of additional work, whereas the second one calls the move constructor, which is as lean as it gets.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文