I’ve recently learnt about coroutines, and got excited as I maintain several async libraries, for which to support coroutines for at least the base library -the task scheduler-.
The task scheduler serves bare functions: No parameters , no return value, and I want to keep it as it is while supporting coroutine to co_await
for periods, primarily.
Therefore I went to have the promise_type::get_return_object()
to return void, as no intention to enforce application/user to switch such tasks to different function signature, and to avoid double managing the tasks.
I’ve initially implemented that, with a compile error preventing me to proceed: error: unable to find the promise type for this coroutine
.
Following is the main implementation of the coroutine, and here’s the execution link: https://godbolt.org/z/4hWce9n6P
Am I getting coroutines wrong? What is suggested to do?
Thanks.
class H4Delay {
uint32_t duration;
task* owner;
task* resumer=nullptr;
public:
class promise_type {
task* owner=nullptr;
friend class H4Delay;
public:
void get_return_object() noexcept {}
std::suspend_never initial_suspend() noexcept { return {}; }
void return_void() noexcept {}
void unhandled_exception() noexcept { std::terminate(); }
struct final_awaiter {
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<promise_type> h) noexcept {
auto owner = h.promise().owner;
if (owner) owner->_destruct();
task::suspendedTasks.erase(owner);
// [ ] IF NOT IMMEDIATEREQUEUE: MANAGE REQUEUE AND CHAIN CALLS.
}
void await_resume() noexcept {}
};
final_awaiter final_suspend() noexcept { return {}; }
};
std::coroutine_handle<promise_type> _coro;
H4Delay(uint32_t duration, task* caller=H4::context) : duration(duration), owner(caller) {}
~H4Delay() {
if (_coro) _coro.destroy();
}
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<promise_type> h) noexcept {
// Schedule the resumer.
resumer = h4.once(duration, [h]{ h.resume(); });
_coro = h;
_coro.promise().owner = owner;
task::suspendedTasks[owner] = this;
}
void await_resume() noexcept { resumer = nullptr; }
void cancel() { ... }
};
10
I’ve skimmed your code and as far as I can tell, you have a misunderstanding of coroutines. There are 2 concepts at play here: a coroutine and an awaitable.
A coroutine is a function that runs as a coroutine, meaning it can suspend and resume its execution at suspension points. For example:
Foo foo() {
co_await std::suspend_never{};
}
This is necessarily a coroutine, because otherwise you can’t use co_await
. Therefore, Foo
must be capable of being used as a returnvalue from a coroutine, meaning it must define a suitable promise_type
.
What you want is an awaitable. An awaitable is simply a type that can be co_await
ed. For this it needs the await_ready
, await_suspend
and await_resume
methods (or an operator co_await
whose return type has these methods). No need to define a promise type for H4Delay, if all you want to do with it is co_await H4Delay{...};
.
Now you’ll run into the issue, that you can’t just co_await
in a regular function. You can only do that in a coroutine. That’s why, in your godbolt, the compiler complains:
In function 'void someF()':
<source>:737:5: error: unable to find the promise type for this coroutine
737 | co_await H4Delay(400);
Because you use co_await
, someF
becomes a coroutine. Meaning void
needs to define a suitable promise_type
, which it does not.
You can either define your own rudimentary coroutine type (Coro<Result>
or whatever), or you can search for libraries that implement this for you. Then you define someF
as Coro<void> someF()
, and you’ll be able to co_await
in it.
Word of warning: implementing a coroutine type yourself isn’t that hard to do initially, but the devil is very much in the details.
-
coroutine state: Language provided thingy that represents the state of a coroutine. Things like a copy of your stack frame get associated with it.
-
coroutine promise object: user or library written object that handles coroutine logic. The language will regularly interact with it in response to coroutine type operations.
-
coroutine handle: A relatively opaque handle from which you can manipulate/interact with the coroutine state, and get your coroutine promise object.
-
C++ coroutine: a function whose code is converted into something that can be suspended and resumed. You can think of it as having local variables in a struct and a bunch of GOTO labels at every possible suspension/resumption location. Created by using any coroutine operation in a function.
-
Awaitable. A type that supports being co_await’d “in the raw” within a coroutine. I’d include types with an
operator co_await()
(member or free) that return an Awaitable here. Awaitables need to support support:bool await_ready();
? await_suspend(handle); // (can return void, bool, or a handle)
? await_resume(); // (return type of co_await operation).
(Coroutines can modify what co_await means to convert expressions into awaitables)
As a rule, you cannot await something outside of a coroutine. The await API basically lets the computation “reach back” into the containing coroutine and suspend or resume it.
From the outside — from its signature — it isn’t possible to determine if a function is a coroutine or not. To the external caller, you invoke the function and it returns something.
The difference is that a coroutine can set up the suspend/resume machinery. It can even expose access to this machinery within the returned object or other parameters passed to the function. It can also choose not to expose access to this machinery.
But, as the language supports user-defined types of coroutines, some means to map from the function to the coroutine defined coroutine needs to exist.
Typically this is done through the return type. The default coroutine_traits
just returns R::promise_type
. So if your return type has a promise_type
typedef, and you have coroutine operations, it uses that type.
In your case, you are using a coroutine operation (co_await
in a function returning void. The compiler is complaining that it doesn’t know how to turn
void someF() {
printf("on 500, awaiting 400 ms:n");
co_await H4Delay(400);
printf("400ms awaited!n");
return;
}
into a coroutine.
co_await
tries to hook into the coroutine machinery of someF
and fails.
Specifically, in:
void await_suspend(std::coroutine_handle<promise_type> h)
that h
is the coroutine handle of someF
, which doesn’t exist.
To fix this we need to turn someF
into a coroutine. When you co_await
, someF
will return immediately. Later on, it will be resumed by the H4Delay
class and the tail end of the code will run.
Does the caller of someF
deserve need to know when it finished? Then you can return a std::task<void>
. If not, you could do this:
struct secret_coroutine {};
template<class R>
struct secret_coroutine_impl {};
template<>
struct secret_coroutine_impl<secret_coroutine> {
secret_coroutine get_return_object() noexcept { return {}; }
std::suspend_never initial_suspend() noexcept { return {}; }
void return_void() noexcept {}
void unhandled_exception() noexcept { std::terminate(); }
struct final_awaiter {
bool await_ready() noexcept { return false; }
template<class T>
void await_suspend(std::coroutine_handle<T> h) noexcept {}
void await_resume() noexcept {}
};
final_awaiter final_suspend() noexcept { return {}; }
};
template<>
struct secret_coroutine_impl<void> {
void get_return_object() noexcept {}
std::suspend_never initial_suspend() noexcept { return {}; }
void return_void() noexcept {}
void unhandled_exception() noexcept { std::terminate(); }
struct final_awaiter {
bool await_ready() noexcept { return false; }
template<class T>
void await_suspend(std::coroutine_handle<T> h) noexcept {}
void await_resume() noexcept {}
};
final_awaiter final_suspend() noexcept { return {}; }
};
note that this doesn’t do the proper resource management and lifetime; I just made it compile.
When used in your code:
secret_coroutine someF() {
printf("on 500, awaiting 400 ms:n");
co_await H4Delay(400);
printf("400ms awaited!n");
co_return;
}
it fails because your co_await
code for H4Delay
assumes that the coroutine it is suspended in is H4Delay
s specific coroutine, both by setting its type and by assuming it has a .owner
field and similar.
So you’d have to augment the someF
coroutine to have that data.
Once you are done, your H4Delay no longer has to have the coroutine support (just the awaitable support) I think, unless you want to chain co_await
s.