danvratil/qcoro

Bind coroutine to a QObject lifetime

Opened this issue · 6 comments

Based on this discussion in a plasma-workspace PR, when porting existing code to QCoro it might be more and more common that people run into crashes due to being used to the fact that when a QObject is deleted, it never happens in the middle of a function, and all ongoing asynchronous operations won't cause use-after-free, because signals-lots connections get disconnected when the signal or slot owner is the deleted object.

Consider the (simplified) case from the PR:

QCoro::Task<> MyQObject::foo()
{
    mResult = co_await mDbusInterface->asyncCall(...);
}

This will crash if the MyQObject instance is deleted while the method is suspended in co_await, waiting the completion of the DBus call. Once resumed it will try to access this->mResult, but this will be a dangling pointer by that time.

The purpose of this task is to come up with some helper macro/function that would allow to effectively cancel the current coroutine if the object gets deleted.

This also relies on the yet unsolved problem of what should happen with coroutine that is co_awaiting a canceled coroutine. A canceled coroutine cannot provide a result, thus

const auto result = co_await coroutineThatCanBeCanceled();

will be problematic, as result we need to return something to store in result.

The proposal in the past was for a special QCoro::CancellableTask<T> that always returns a sort-of std::expected<T> instead of T and leaves it up to the caller to deal with the cancelation.

There's an interesting concept introduced in this article from Raymond Chen - the idea of using await_transform() to allow the coroutine to obtain or set a custom behavior policies (it could be the solution for #97!).

We could use abuse this feature to also make the Promise be aware of the object it belongs to, in other words, you would put

co_await qCoroGuardThis(this);

at the beginning of each method where you want to guard.

This would end up storing this inside the current TaskPromise and in all the other await_transform() overloads it would set up a guard that just destroys the current coroutine if this has been deleted in the meanwhile instead of resuming it.

This very issue was what prompted me at researching how to have QCoro-like capabilities in Qt itself. A variation of this problem (or of the solution...) was by having a task-like return type that owns the coroutine activation. Qt would use the return type to know that it's dealing with a coroutine, and do things slightly differently when the method is invoked through QMetaObject (signal/slot, invokemethod, ...). In such a case the return object isn't dropped on the floor (which would cancel the coroutine), but Qt can associate it with the "receiver" (= this) by storing it somewhere into it. If the receiver is destroyed, its suspended coroutines get all cancelled automatically.

I think co_yield QCoro::Guard (QOjbect) is more user-friendly

Right now it looks like this:

co_await QCoro::thisCoro().guard(this)

with the idea that thisCoro() returns an object that allows customizing more features of the coroutine than just a guard.

I'm not opposed to having a shorter alternative, like

co_await qCoro::guard(this);

First, I need to make the other version to not crash, though 😃

i think the feature only init once at the head of corotine。yield_value mean easy to understand that return a struct to init. the detail thad how to init corotine is hide at core,contain init more than once ,this can not be controlled by user.single direction set the feature to corotine is better
co_wait look like some thing has run,not good for user.

qCoro::Feature feature;
feature.guard(QObject*);
yeild_value feature;// init once

or simple code

yeild_value qCoro::Feature::guard(QObject*);