📜 ⬆️ ⬇️

Coroutines everywhere

In his report on C ++ Russia 2016, Gor Nishanov mentioned that korutin can work in any environments, even where there is no C ++ runtime. I wanted to try Korutin in such environments. See how most from scratch to implement the support of corutin in the standard library. Check how the cortinas live without exception, and whether they work outside the operating system (on bare hardware).


Compiler support


I used the Microsoft compiler - cl.exe. Starting with Visual Studio 2015, the compiler has experimental support for coroutines. In order to start using coroutines (coroutines, corutines), you must pass the / await key to the compiler.

How implemented corticles in cl.exe


Korutin supported at the compiler level, while the compiler provides a set of intrinsic functions that allow you to control the process of creating, suspending and resuming coruntine:
extern "C" size_t _coro_resume(void *); extern "C" void _coro_destroy(void *); extern "C" size_t _coro_done(void *); #pragma intrinsic(_coro_resume) #pragma intrinsic(_coro_destroy) #pragma intrinsic(_coro_done) 

')
In principle, prototypes of these functions are enough for a programmer to implement his own corutins.

Standard Library Implementation Requirements


When the compiler encounters a co_await EXPR construct, it requires the type of EXPR expression to implement the following methods:

 bool await_ready(); void await_suspend(std::experimental::coroutine_handle<promise_type> coroutineHandle); value_type await_resume(); 


In other words, for the type to satisfy the concept of Awaitable (for more details, see here )

C ++ runtime requirements


Here is the most interesting, the compiler requires the following statements:
 void operator delete(void* location, size_t); void * operator new(size_t size); 


Or the presence of operators
 void operator delete(void* location, size_t); void * operator new(size_t size, std::nothrow_t const &); 


More compiler does not need anything to fully support corutin!

From the above signatures of the operators new and delete, it is clear that the corutines can work with both the throwing option new and the nothrow option new, which is just suitable for various environments in which there are no exceptions!

What is coroutine_handle


We have seen that one of the methods (await_suspend) requires the type coroutine_handle, what type is it?
In fact, coroutine_handle is a C ++ wrapper over void *, moreover, this type has semantics close to the pointer semantics, it can be assigned, copied, and most importantly, the responsibility for the destruction of this object lies with the programmer (there is some analogy with memory removal indicated by the pointer).
An object of this type is responsible for executing and completing a coruntine.
Very few methods are required from coroutine_handle:

 static coroutine_handle from_address(void *_Addr) noexcept; void *address() const noexcept; void operator()() const noexcept { resume(); } void resume() const; void destroy(); bool done() const; 


We should also mention the from_address method - this method is designed to get an object from void *, why is it necessary? Usually, asynchronous operations allow you to set the context when the callback is executed, and so, the coroutine_handle object (more precisely, the address returned by coroutine_handle :: address) is passed as this context.

As I already said, the responsibility for the destruction of Korutina lies with the programmer, just for this there is a destroy method. In principle, it can be called at any time working with cororne, but the main thing to remember is, in what context we are performing, about this a little further.

Execution context


So, coroutine_handle gives us the opportunity to pause (freeze) the execution of the current function in one context, and continue execution in another context. Execution can continue anywhere: it can be another thread, asynchronous event handling in some Wait * function. Purely theoretically, this may even be a different process or interruption. In this case, all local variables and parameters of our function are generally NOT on the stack, but on the heap (remember the new and delete operators), although the current proposal allows the compiler to optimize to avoid allocating memory from the heap.

In the previous paragraph, I said that we need to understand the context in which we are performing. The destroy method cancels the execution of coroutine, but it is important to understand how a call to the destroy method will affect the stability of the program — another thread or an asynchronous operation could already begin execution, it is important to remember this. In addition, in system programming, often some asynchronous operations prohibit accessing functions that control resources (for example, in EFI on high TPLs, memory cannot be allocated or distributed) - this is also an important point and must be remembered.

Proof of concept


I have a proof-of-concept of working corutin in the following environments:


This post can be considered a primer, since I will speak at C ++ Siberia 2016 , and there I will analyze in more detail all the nuances of using quorutine.

Wishes, comments, corrections are welcome in every way, see you at the conference!

ps I recommend to see the above report Mountain, the report is really cool!

Source: https://habr.com/ru/post/306940/


All Articles