I've developed a coroutine library for C++ that is contained within a single header file. It is compatible with both Windows and Linux platforms. This library is not multi-threaded; instead, it is specifically designed to allow C++ developers to write code in an event-driven manner.
While I would appreciate feedback on the entire library, I understand that reviewing all 5,000 lines of code might be challenging. Therefore, I am particularly interested in receiving comments on the library's interface.
The library is hosted in its own GitHub repository at https://github.com/Pangi790927/co-lib, where you can also find the library's tests.
#ifndef COLIB_H
#define COLIB_H
/* DOCUMENTATION
====================================================================================================
====================================================================================================
====================================================================================================
1. Introduction
===============
In C++20 a new feature was added, namely coroutines. This feature allows the creation of a kind of
special function named a coroutine that can have its execution suspended and resumed later on.
Classical functions also do this to a degree, but they only suspend for the duration of the call of
functions that run inside them. Classical functions form a stack of data with their frames, where
coroutines have heap-allocated frames without a clear stack-like order. A classical function is
suspended when it calls another function and resumed when the called function returns. Its frame,
or state if you will, is created when the function is called and destroyed when the function returns.
A coroutine has more suspension points, normal functions can be called but there are two more
suspension points added: co_await and co_yield, in both, the state is left in the coroutine frame
and the execution is continued outside the coroutine. In the case of this library co_await suspends
the current coroutine until the 'called' awaiter is complete and the co_yield suspends the current
coroutine, letting the 'calling' coroutine to continue. The main point of coroutines is that while
a coroutine waits to be resumed another one can be executed, obtaining an asynchronous execution
similar to threads but on a single thread (Those can also be usually run on multiple threads,
but that is not supported by this library), this almost eliminates the need to use synchronization
mechanisms.
Let's consider an example of a coroutine calling another coroutine:
14: colib::task<int32_t> get_messages() {
15: int value;
16:
17: while (true) {
18: value = co_await get_message();
19: if (value == 0)
20: break;
21: co_yield value;
22: }
22: co_return 0;
23: }
At line 11, the coroutine is declared. As you can see, coroutines need to declare their return value
of the type of their handler object, namely colib::task<Type>. That is because the coroutine holds the
return value inside the state of the coroutine, and the user only gets the handler to the coroutine.
At line 15, another awaiter, in this case another coroutine, is awaited with the use of co_await.
This will suspend the get_messages coroutine at that point, letting other coroutines on the system
run if there are any that need to do work, or block until a coroutine gets work to do. Finally,
this coroutine continues to line 16 when a message becomes available. Note that this continuation
will happen if a) there are no things to do or b) if another coroutine awaits something and this
one is the next that waits for execution.
Assuming value is not 0, the coroutine yields at line 18, returning the value but keeping its state.
This state contains the variable value and some other internals of coroutines.
When the message 0 is received, the coroutine returns 0, freeing its internal state. You shouldn't
call the coroutine anymore after this point.
24: colib::task<int32_t> co_main() {
25: colib::task<int32_t> coro = get_messages();
26: for (int32_t value = co_await coro; value; value = co_await coro) {
27: printf("main: %d\n", value);
28: if (!value)
29: break;
30: }
31: co_return 0;
32: }
The coroutine that calls the above coroutine is co_main. You can observe the creation of the
coroutine at line 25; what looks like a call of the coroutine in fact allocates the coroutine state
and returns the handle that can be further awaited, as you can see in the for loop at line 26.
The coroutine will be called until value is 0, in which case we know that the coroutine has ended
(from its code) and we break from the for loop.
We observe that at line 31 we co_return 0; that is because the co_return is mandatory at the end of
coroutines (as mandated by the language).
0: int cnt = 3;
1: colib::task<int32_t> get_message() {
2: co_await colib::sleep_s(1);
3: co_return cnt--;
4: }
5:
6: colib::task<int32_t> co_timer() {
7: int x = 50;
8: while (x > 0) {
9: printf("timer: %d\n", x--);
10: co_await colib::sleep_ms(100);
11: }
12: co_return 0;
13: }
Now we can look at an example for get_message at line 1. Of course, in a real case, we would await a
message from a socket, for some device, etc., but here we simply wait for a timer of 1 second to
finish.
As for an example of something that can happen between awaits, we can look at co_timer at line 6.
This is another coroutine that prints x and waits 100 ms, 50 times. If you copy and run the message
yourself, you will see that the prints from the co_timer are more frequent and in-between the ones
from co_main.
33: int main() {
34: colib::pool_p pool = colib::create_pool();
35: pool->sched(co_main());
36: pool->sched(co_timer());
37: pool->run();
38: }
Finally, we can look at main. As you can see, we create the pool at line 34, schedule the main
coroutine and the timer one, and we wait on the pool. The run function won't exit unless there are
no more coroutines to run or, as we will see later on, if a force_awake is called, or if an
error occurs.
2. Library Layout
=================
The library is split in four main sections:
1. The documentation
2. Macros and structs/types
3. Function definitions
4. Implementation
3. Task
=======
As explained, each coroutine has an internal state. This state remembers the return value of the
function, its local variables, and some other coroutine-specific information. All of these are
remembered inside the coroutine promise. Each coroutine has, inside its promise, a state_t state
that remembers important information for its scheduling within this library. You can obtain this
state by (await get_state()) inside the respective coroutine for which you want to obtain the state
pointer. This state will live for as long as the coroutine lives, but you would usually ignore
its existence. The single instance for which you would use the state is if you are using
modifications (see below).
To each such promise (state, return value, local variables, etc.), the language assigns a handle in
the form of std::coroutine_handle<PromiseType>. These handles are further managed by tasks inside
this library. So, for a coroutine, you will get a task as a handle. The task further specifies the
type of the promise and implicitly the return value of the coroutine, but you don't need to bother
with those details.
A task is also an awaitable. As such, when you await it, it calls or resumes the awaited coroutine,
depending on the state it was left in. The await operation will resume the caller either on a
co_yield (the C++ yield; colib::yield does something else) or on a co_return of the callee. In the
latter case, the awaited coroutine is also destroyed, and further awaits on its task are undefined
behavior.
The task type, as in colib::task<Type>, is the type of the return value of the coroutine.
4. Pool
=======
For a task, the pool is its running space. A task runs on a pool along with other tasks. This pool
can be run only on one thread, i.e., there are no thread synchronization primitives used, except in
the case of COLIB_ENABLE_MULTITHREAD_SCHED.
The pool remembers the coroutines that are ready and resumes them when the currently running
coroutine yields to wait for some event (as in colib::yield). The pool also holds the allocator
used internally and the io_pool and timers, which are explained below and are responsible for
managing the asynchronous waits in the library.
A task remembers the pool it was scheduled on while either (co_await colib::sched(task)) or
pool_t::sched(task) are used on the respective task.
There are many instances where there are two variants of a function: one where the function has
the pool as an argument and another where that argument is omitted, but the function is in fact a
coroutine that needs to be awaited. Inside a coroutine, using await on a function, the pool is
deduced automatically from the running coroutine.
From inside a running coroutine, you can use (co_await colib::get_pool()) to get the pool of the
running coroutine.
5. Semaphores
=============
Semaphores are created by using the function/coroutine create_sem and are handled by using
sem_p smart pointers. They have a counter that can be increased by signaling them or decreased by
one if the counter is bigger than 0 by using wait. In case the counter is 0 or less than 0, the wait
function blocks until the semaphore is signaled. In this library, semaphores are a bit unusual, as
they can be initialized to a value that is less than 0 so that multiple awaiters can wait for a
task to finish.
6. IO Pool
==========
Inside the pool, there is an Input/Output event pool that implements the operating system-specific
asynchronous mechanism within this library. It offers a way to notify a single function for
multiple requested events to either be ready or completed in conjunction with a state_t *.
In other words, we add pairs of the form (io_desc_t, state_t *) and wait on a function for any of
the operations described by io_desc_t to be completed. We do this in a single place to wait for all
events at once.
On Linux, the epoll_* functions are used, and on Windows, the IO Completion Port mechanism is used.
Of course, all these operations are done internally.
7. Allocator
============
Another internal component of the pool is the allocator. Because many of the internals of coroutines
have the same small memory footprint and are allocated and deallocated many times, an allocator was
implemented that keeps the allocated objects together and also ignores some costs associated with
new or malloc. This allocator can be configured (COLIB_ALLOCATOR_SCALE) to hold more or less memory,
as needed, or ignored completely (COLIB_DISABLE_ALLOCATOR), with malloc being used as an alternative.
If the memory given to the allocator is used up, malloc is used for further allocations.
8. Timers
=========
Another internal component of the pool is the timer_pool_t. This component is responsible
for implementing and managing OS-dependent timers that can run with the IO pool. There are a limited
number of these timers allocated, which limits the maximum number of concurrent sleeps. This number
can be increased by changing COLIB_MAX_TIMER_POOL_SIZE.
9. Modifs
=========
Modifications are callbacks attached to coroutines that are called in specific cases:
on suspend/resume, on call/sched (after a valid state_t is created), on IO wait (on both wait and
finish wait), and on semaphore wait and unwait.
These callbacks can be used to monitor the coroutines, to acquire resources before re-entering a
coroutine, etc. (Internally, these are used for some functions; be aware while replacing existing
ones not to break the library's modifications).
Modifications can be inherited by coroutines in two cases: on call and on sched. More precisely,
each modification can be inherited by a coroutine scheduled from this one or called from this one.
You can modify the modifications for each coroutine using its task to get/add/remove modifications
or awaiters from inside the current coroutine.
10. Debugging
============
Sometimes unwanted behavior can occur. If that happens, it may be debugged using the internal
helpers, those are:
dbg_enum - get the description of a library enum code
dbg_name - when COLIB_ENABLE_DEBUG_NAMES is true, it can be used to get the name
associated with a task, a coroutine handle or a coroutine promise address,
those can be registered with COLIB_REGNAME or dbg_register_name
dbg_create_tracer - creates a modif_pack_t that can be attached to a coroutine to debug all
the coroutine that it calls or schedules
log_str - the function that is used to print a logging string (user can change it)
dbg - the function used to log a formatted string
dbg_format - the function used to format a string
All those are enabled by COLIB_ENABLE_LOGGING true, else those are disabled.
11. Config Macros
=================
|--------------------------------|------|---------------|------------------------------------------|
| Macro Name | Type | Default Value | Description |
|================================|======|===============|==========================================|
| COLIB_OS_LINUX | BOOL | true | If true, the library provided Linux |
| | | | implementation will be used to implement |
| | | | the IO pool and timers. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_OS_WINDOWS | BOOL | false | If true, the library provided Windows |
| | | | implementation will be used to implement |
| | | | the IO pool and timers. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_OS_UNKNOWN | BOOL | false | If true, the user provided implementation|
| | | | will be used to implement the IO pool and|
| | | | timers. In this case |
| | | | COLIB_OS_UNKNOWN_IO_DESC and |
| | | | COLIB_OS_UNKNOWN_IMPLEMENTATION must be |
| | | | defined. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_OS_UNKNOWN_IO_DESC | CODE | undefined | This define must be filled with the code |
| | | | necessary for the struct io_desc_t, use |
| | | | the Linux/Windows implementations as |
| | | | examples. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_OS_UNKNOWN_IMPLEMENTATION| CODE | undefined | This define must be filled with the code |
| | | | necessary for the structs timer_pool_t |
| | | | and io_pool_t, use the Linux/Windows |
| | | | implementations as examples. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_MAX_TIMER_POOL_SIZE | INT | 64 | The maximum number of concurrent sleeps. |
| | | | (Only for Linux) |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_MAX_FAST_FD_CACHE | INT | 1024 | The maximum file descriptor number to |
| | | | hold in a fast access path, the rest will|
| | | | be held in a map. Only for Linux, on |
| | | | Windows all are held in a map. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_ENABLE_MULTITHREAD_SCHED | BOOL | false | If true, pool_t::thread_sched can be used|
| | | | from another thread to schedule a |
| | | | coroutine in the same way pool_t::sched |
| | | | is used, except, modifications can't be |
| | | | added from that schedule point. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_ENABLE_LOGGING | BOOL | true | If true, coroutines will use log_str to |
| | | | print/log error strings. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_ENABLE_DEBUG_TRACE_ALL | BOOL | false | TODO: If true, all coroutines will have a|
| | | | debug tracer modification that would |
| | | | print on the given modif points |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_DISABLE_ALLOCATOR | BOOL | false | If true, the allocator will be disabled |
| | | | and malloc will be used instead. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_ALLOCATOR_SCALE | INT | 16 | Scales all memory buckets inside the |
| | | | allocator. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_ALLOCATOR_REPLACE | BOOL | false | If true, COLIB_ALLOCATOR_REPLACE_IMPL_1 |
| | | | and COLIB_ALLOCATOR_REPLACE_IMPL_2 must |
| | | | be defined. As a result, the allocator |
| | | | will be replaced with the provided |
| | | | implementation. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_ALLOCATOR_REPLACE_IMPL_1 | CODE | undefined | This define must be filled with the code |
| | | | necessary for the struct |
| | | | allocator_memory_t and alloc, |
| | | | dealloc_create functions, use the |
| | | | provided implementations as examples. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_ALLOCATOR_REPLACE_IMPL_2 | CODE | undefined | This define must be filled with the code |
| | | | necessary for the allocate/deallocate |
| | | | functions, use the provided |
| | | | implementations as examples. |
|--------------------------------|------|---------------|------------------------------------------|
| COLIB_WIN_ENABLE_SLEEP_AWAKE | BOOL | false | Sets the last parameter of the function |
| | | | SetWaitableTimer to true or false, |
| | | | depending on the value. This function is |
| | | | used for timers on Windows. |
|--------------------------------|------|---------------|------------------------------------------|
12. API
=======
12.1 Object types
-----------------
pool_p
Smart pointer handle to the pool object. When destroyed, the pool is also destroyed. You must
keep the pool alive while corutines are running and while semaphore exist
sem_p
Smart pointer handle to the semaphore object. When destroyed, the semaphore is destroyed. It
is undefined behaviour to destroy the semaphore while a coroutine is waiting on it.
modif_p
Smart pointer handle to a single modification. Ownership is transfered to the corutine when
attached.
modif_pack_t
A vector consisting of modif_p-s. Functions receive those packs togheter to ease use.
task<T>
The task handle of a coroutine, T is the return type of the respective coroutine.
12.2 Enums and structs
----------------------
struct io_desc_t
This is the structure that describes an I/O operation, OS-dependent, used internally to
handle I/O operations.
On Linux it consists of:
1. a file descriptor (fd)
2. epoll event type (events)
On Windows it consists of:
1. a file handle (h)
2. a smart pointer to an internal `io_data_t` structure
The `io_data_t` structure holds the state of the I/O operation:
1. the OVERLAPPED structure (check IOCP documentation)
2. flags - a mostly internal field that would normally be `IO_FLAG_NONE` that holds the
state type of the I/O operation
3. recvlen - the byte transfer count
4. io_request - the actual action to be performed
5. user pointer to be passed to the `io_request`
6. a copy of the file handle
The smart pointer must be null for the function `stop_handle` to work.
struct state_t
This structure, as explained above, is the common type for all coroutines from this library. It
also holds a user pointer user_ptr that can be used. This pointer can be useful when working
with modifications.
enum error_e
Most of the functions from this library return this error type. Warnings or non-errors are
positive, while errors are negative.
enum run_e
This is the return value of the pool_t::run.
enum modif_e
This is the modification type of the modification and it describes the place that this
modification should be called from.
enum modif_flags_e
This selects the inheritance mode of the modification; values can be OR-ed together.
12.3 Member functions and vars
------------------------------
pool_t::sched(task<T> task, const modif_pack_t& v) -> void
Schedules the task with the modifications specified in v to be executed on the pool. That is, it
adds the task to the ready_queue.
pool_t::run()
Runs the first coroutine in the ready queue. When this coroutine awaits something, the next
one will be scheduled. Will keep running until there are no more I/O events to wait for, no more
timers to sleep on, no more coroutines to run, or force_stop is used. Returns RUN_OK if no error
occurred during the run. This will block the thread that executed the run.
pool_t::clear()
Destroys all the coroutines that are attached to this pool, meaning those in the ready queue,
those waiting for I/O operations, and those waiting for semaphores.
pool_t::stop_io(const io_desc_t&) -> error_e
Takes as an argument a valid io_desc_t and stops the operation described by the descriptor
on the respective handle.
pool_t::thread_sched
Similar to pool_t::sched, can't add modifications with it. Must have
COLIB_ENABLE_MULTITHREAD_SCHED set to true and can be used from other threads.
pool_t::user_ptr
This is a pointer that you can use however you want.
sem_t::wait() ~> sem_t::unlocker_t
Returns an awaiter that can be awaited to decrement the internal counter of the semaphore.
This awaiter object returns an unlocker that has the `lock` member function doing nothing
and `unlock` function calling `signal` on the semaphore, meaning it can be used inside a
`std::lock_guard` object to protect a piece of code using the RAII principle.
sem_t::signal()
Increments the counter of the semaphore. Pushes waiting tasks, if any, onto the ready queue of
the pool if the counter would become larger than 0 and does not increment the counter if this
operation is done.
sem_t::try_dec() -> bool
Non-blocking; if the semaphore counter is positive, decrements the counter and returns true,
else returns false.
12.4 Function and coroutines/awaitables
---------------------------------------
Here -> T denotes the returned value T of a function and ~> T the return value T of a coroutine or
awaitable, i.e. it needs co_await to get the value and to execute.
create_pool() -> pool_p
Creates the pool object, returning the pool_p handle to the pool. Allocates space for the
allocator and initiates diverese functions of the pool.
get_pool() ~> pool_t *
Awaitable that returns the pointer of the pool coresponding to the coroutine from which this
function is called from
get_state() ~> state_t *
Awaitable that returns the pointer of the state_t of the current coroutine
sched(task<T> task, const modif_pack_t& v) ~> void
Does the same thing as pool_t::sched, on the running coroutine's pool.
yield() ~> void
Suspends the current coroutine and moves it to the end of the ready queue within its
associated pool.
await(Awaitable) ~> task<int>
Helper coroutine function, given an awaitable, awaits it inside the coroutine await,
usefull if the awaitable can't be decorated, bacause it isn't a coroutine.
create_timeo(task<T> t, pool, timeo_ms) -> task<std::pair<T, error_e>>
Schedules the task `t` and a timer that kills the task `t`, if `t` doesn't finish before the
timer expires in timeo_ms milliseconds. This function returns a coroutine that can be awaited
to get the return value and error value. If the error value is not ERROR_OK, than the task
`t` wasn't executed succesfully.
sleep_us ~> void
sleep_ms ~> void
sleep_s ~> void
sleep ~> void
Awaitable coroutines that sleep for the given duration in microseconds, milliseconds, seconds
or c++ duration. The precision with which this sleep occours is given by the h
create_sem(pool_p, int64_t initial_val) -> sem_p
create_sem(pool_t *, int64_t initial_val) -> sem_p
create_sem(int64_t initial_val) ~> sem_p
Those functions create a semaphore with the initial value set to initial_value. They need the
pool and the last variant of this function can deduce it from the coroutine context.
create_killer(pool_t *pool, error_e e) -> std::pair<modif_pack_t, std::function<error_e(void)>>
Creates a modification pack that can be added to only one coroutine that is associated with
the given pool. The second parameter e will be the error value of the coroutine. The
returned function can be called to kill the given coroutine and it's entire call stack (does
not kill sched stack).
create_future(pool_t *pool, task<T> t) -> task<T>
Takes a task and adds the requred modifications to it such that the returned object will be
returned once the return value of the task is available so:
1: auto t = co_task();
2: auto fut = colib::create_future(t)
3: co_await colib::sched(t);
4: // ...
5: co_await fut; // returns the value of co_task once it has finished executing
wait_all(task<ret_v>... tasks)
Wait for all the tasks to finish, the return value can be found in the respective task, killing
one kills all (sig_killer installed in all). The inheritance is the same as with 'call'.
force_stop(value) ~> errno_e
Causes the running pool::run to stop, the function will stop at this point, can be resumed with
another run
wait_event(io_desc) ~> errno_e
Waits for the described event to be available/finish, depending on the OS. Usefull for
stop_io(io_desc) -> errno_e
Stops the given I/O event, described by io_desc, by canceling it's wait and making the awaitable
give an error.
connect(handle, sockaddr *sa, socklen_t *len/int len)
accept(handle, sockaddr *sa, socklen_t *len)
Calls system connect/accept using coroutines
read(handle, buffer, len) ~> errno_e
write(handle, buffer, len) ~> errno_e
Calls the system read/write using coroutines
read_sz(handle, buffer, len) ~> errno_e
write_sz(handle, buffer, len) ~> errno_e
Same as the sistem calls, just they wait for the exact leght to be sent/received. Those also
give an error if the connection is closed during the operation.
create_modif<modif_type>(pool, modif_flags_e, cbk) -> modif_p
Creates a modification that will be executed on the given modif_type, inherited by the rules
specified inside modif_flags and on the given pool. It will execute the callback cbk at those
points.
task_modifs(task) -> modif_pack_t
task_modifs() ~> modif_pack_t
Given a task or on the running coroutine's task, get the modifications that it has.
add_modifs(pool, task<T>, std::set<modif_p> mods) -> task<T>
add_modifs(std::set<modif_p> mods) ~> task<T>
Adds the mods to the specified task (implicit or explicit), given the specified pool (implicit
or explicit) and returns the task in question.
rm_modifs(task<T>, std::set<modif_p> mods) -> task<T>
rm_modifs(std::set<modif_p> mods) ~> task<T>
Removes the modifications from the specified task (implicit or explicit) and returns the task in
question.
stop_fd(int fd) ~> error_e
Linux specific, is used to evict an fd from the epoll engine before closing it, you shouldn't
close a file descriptor before removing it from the pool.
stop_handle(HANDLE h)
Windows specific, is used to evict an HANDLE h from the iocp engine before closing it,
you shouldn't close a handle before removing it from the pool.
ConnectEx(...) ~> BOOL
WSARecv(...) ~> BOOL
WSARecvMsg(...) ~> BOOL
WSARecvFrom(...) ~> BOOL
WSASend(...) ~> BOOL
WSASendTo(...) ~> BOOL
WSASendMsg(...) ~> BOOL
WriteFile(...) ~> BOOL
WaitCommEvent(...) ~> BOOL
TransactNamedPipe(...) ~> BOOL
ReadFile(...) ~> BOOL
ReadDirectoryChangesW(...) ~> BOOL
LockFileEx(...) ~> BOOL
DeviceIoControl(...) ~> BOOL
ConnectNamedPipe(...) ~> BOOL
AcceptEx(...) ~> BOOL
All those functions are calling their WinAPI counterpart, but in coroutine context and they
all are missing the OVERLAPPED pointer because that one is owned by the I/O engine. Some
of them also offer a parameter named *offset, for functions that need the offset from inside
the OVERLAPPED structure, that pointer's contents will be copied inside the overlapped structure
and copied out ov the overlapped structure after the call is done. They require a handle that
is compatible with iocp and they will attach the handle to the iocp instance. Those are the
functions listed by msdn to work with iocp (and connect, that is part of an extension)
HEADER
====================================================================================================
====================================================================================================
====================================================================================================
*/
#include <array>
#include <chrono>
#include <cinttypes>
#include <coroutine>
#include <deque>
#include <functional>
#include <list>
#include <map>
#include <memory>
#include <set>
#include <source_location>
#include <stack>
#include <string.h>
#include <unordered_set>
#include <utility>
#include <variant>
#include <vector>
#ifndef COLIB_OS_LINUX
# define COLIB_OS_LINUX true
#endif
#ifndef COLIB_OS_WINDOWS
# define COLIB_OS_WINDOWS false
#endif
#ifndef COLIB_OS_UNKNOWN
# define COLIB_OS_UNKNOWN false
# define COLIB_OS_UNKNOWN_IMPLEMENTATION ;
# define COLIB_OS_UNKNOWN_IO_DESC ;
#endif
#if COLIB_OS_LINUX
# include <fcntl.h>
# include <unistd.h>
# include <sys/epoll.h>
# include <sys/socket.h>
# include <sys/timerfd.h>
#endif
#if COLIB_OS_WINDOWS
# include <winsock2.h>
# include <mswsock.h>
# include <windows.h>
#endif
#if COLIB_OS_UNKNOWN
/* you should include your needed files before including this file */
#endif
#ifndef COLIB_MAX_TIMER_POOL_SIZE
# define COLIB_MAX_TIMER_POOL_SIZE 64
#endif
#ifndef COLIB_MAX_FAST_FD_CACHE
# define COLIB_MAX_FAST_FD_CACHE 1024
#endif
#ifndef COLIB_ENABLE_MULTITHREAD_SCHED
# define COLIB_ENABLE_MULTITHREAD_SCHED false
#endif
#ifndef COLIB_ENABLE_LOGGING
# define COLIB_ENABLE_LOGGING true
#endif
#if COLIB_ENABLE_LOGGING
# define COLIB_DEBUG(fmt, ...) dbg(__FILE__, __func__, __LINE__, fmt, ##__VA_ARGS__)
#else
# define COLIB_DEBUG(fmt, ...) do {} while (0)
#endif
#ifndef COLIB_ENABLE_DEBUG_NAMES
# define COLIB_ENABLE_DEBUG_NAMES false
#endif
#ifndef COLIB_ENABLE_DEBUG_TRACE_ALL
# define COLIB_ENABLE_DEBUG_TRACE_ALL false
#endif
#ifndef COLIB_DISABLE_ALLOCATOR
# define COLIB_DISABLE_ALLOCATOR false
#endif
#ifndef COLIB_ALLOCATOR_SCALE
# define COLIB_ALLOCATOR_SCALE 16
#endif
#ifndef COLIB_ALLOCATOR_REPLACE
# define COLIB_ALLOCATOR_REPLACE false
# define COLIB_ALLOCATOR_REPLACE_IMPL_1
# define COLIB_ALLOCATOR_REPLACE_IMPL_2
#endif
#ifndef COLIB_WIN_ENABLE_SLEEP_AWAKE
# define COLIB_WIN_ENABLE_SLEEP_AWAKE FALSE
#endif
/* If COLIB_ENABLE_DEBUG_NAMES you can also define COLIB_REGNAME and use it to register a
coroutine's name (a colib::task<T>, std::coroutine_handle or void *) */
#if COLIB_ENABLE_DEBUG_NAMES
# ifndef COLIB_REGNAME
# define COLIB_REGNAME(thv) colib::dbg_register_name((thv), "%20s:%5d:%s", __FILE__, __LINE__, #thv)
# endif
#else
# define COLIB_REGNAME(thv) thv
#endif /* COLIB_ENABLE_DEBUG_NAMES */
namespace colib {
constexpr int MAX_TIMER_POOL_SIZE = COLIB_MAX_TIMER_POOL_SIZE;
constexpr int MAX_FAST_FD_CACHE = COLIB_MAX_FAST_FD_CACHE;
/* coroutine return type, this is the result of creating a coroutine, you can await it and also pass
it around (practically holds a std::coroutine_handle and can be awaited call the coro and to get the
return value) */
template<typename T> struct task;
/* A pool is the shared state between all coroutines. This object holds the epoll handler, timers,
queues, etc. Each corutine has a pointer to this object. You can see this object as an instance or
proxy of epoll/iocp. */
struct pool_t;
/* Modifs, corutine modifications. Those modifications controll the way a corutine behaves when
awaited or when spawning a new corutine from it. Those modifications can be inherited, making all
the corutines in the call sub-tree behave in a similar way. For example: adding a timeouts, for this
example, all the corutines that are on the same call-path(not sched-path) would have a timer, such
that the original call would not exist for more than X time units. (+/- the code between awaits) */
struct modif_t;
/* This is a private table that holds the modifications inside the corutine state */
struct modif_table_t;
/* This is a semaphore working on a pool. It can be awaited to decrement it's count and .signaled()
increments it's count from wherever. More above. */
struct sem_t;
/* Internal state of corutines that is independent of the return value of the corutine. */
struct state_t;
/* used pointers, _p marks a shared_pointer */
using sem_p = std::shared_ptr<sem_t>;
using pool_p = std::shared_ptr<pool_t>;
using modif_p = std::shared_ptr<modif_t>;
using modif_table_p = std::shared_ptr<modif_table_t>;
using modif_pack_t = std::vector<modif_p>;
/* Tasks errors */
enum error_e : int32_t {
ERROR_YIELDED = 1, /* not really an error, but used to signal that the coro yielded */
ERROR_OK = 0,
ERROR_GENERIC = -1, /* generic error, can use log_str to find the error, or sometimes errno */
ERROR_TIMEO = -2, /* the error comes from a modif, namely a timeout */
ERROR_WAKEUP = -3, /* the error comes from force awaking the awaiter */
ERROR_USER = -4, /* the error comes from a modif, namely an user defined modif, users can
use this if they wish to return from modif cbks */
ERROR_DEPEND = -5, /* the error comes from a depend modif, i.e. depended function failed */
};
/* Event loop running errors */
enum run_e : int32_t {
RUN_OK = 0, /* when the pool stopped because it ran out of things to do */
RUN_ERRORED = -1, /* comes from epoll errors */
RUN_ABORTED = -2, /* if a corutine had a stroke (some sort of internal error) */
RUN_STOPPED = -3, /* can be re-run (comes from force_stop) */
};
enum modif_e : int32_t {
/* This is called when a task is called (on the task), via 'co_await task' */
CO_MODIF_CALL_CBK = 0,
/* This is called on the corutine that is scheduled. Other mods are inherited before this is
called. The return value of the callback is ignored. */
CO_MODIF_SCHED_CBK,
/* This is called on a corutine right before it is destroyed. The return value of the callback
is ignored */
CO_MODIF_EXIT_CBK,
/* This is called on each suspended corutine. The return value of the callback is ignored*/
CO_MODIF_LEAVE_CBK,
/* This is called on a resume. The return value of the callback is ignored */
CO_MODIF_ENTER_CBK,
/* This is called when a corutine is waiting for an IO (after the leave cbk). If the return
value is not ERROR_OK, then the wait is aborted. */
CO_MODIF_WAIT_IO_CBK,
/* This is called when the io is done and the corutine that awaited it is resumed */
CO_MODIF_UNWAIT_IO_CBK,
/* This is similar to wait_io, but on a semaphore */
CO_MODIF_WAIT_SEM_CBK,
/* This is similar to unwait_io, but on a semaphore */
CO_MODIF_UNWAIT_SEM_CBK,
CO_MODIF_COUNT,
};
enum modif_flags_e : int32_t {
CO_MODIF_INHERIT_NONE = 0x0,
CO_MODIF_INHERIT_ON_CALL = 0x1,
CO_MODIF_INHERIT_ON_SCHED = 0x2,
};
/* all the internal tasks return this, namely error_e but casted to int (a lot of my old code depends
on this and I also find it in theme, as all the linux functions that I use tend to return ints) */
using task_t = task<int>;
/* some forward declarations of internal structures */
struct yield_awaiter_t;
struct sem_awaiter_t;
struct pool_internal_t;
struct sem_internal_t;
struct allocator_memory_t;
struct io_desc_t;
template <typename T>
struct sched_awaiter_t;
/*
There are two things that don't need custom allocating: lowspeed stuff and the corutine promise.
It makes no sense to allocate the corutine promise because:
1. It is a user defined type so the promise is not going to fit well in our allocator
2. I expect it be allocated rarelly
3. It would be a pain to allocate them
4. a malloc now and then is not such a big deal
For the rest of the code those 4 are not generaly true.
The idea of this allocator is that allocated objects are actually small in size and get allocated/
deallocated fast. The assumption is that there is a direct corellation with the number
num_fds+call_depth, and most of the time there aren't that many file descriptors or depth to
calls (at least from my experience).
So what it does is this: it has 5 bucket levels: 32, 64, 128, 512, 2048 (bytes), with a number of
maximum allocations 16384, 8192, 4096, 1024 and 256 of each and a stack coresponding to each of
them, that is initiated at the start to contain every index from 0 to the max amount of slots in
each bucket. When an allocation occours, an index is poped from the lowest fitting bucket, and
that slot is returned. On a free, if the memory is from a bucket, the index is calculated and pushed
back in that bucket's stack else the normal free is used.
Those bucket levels can be configured, by changing the array bellow (Obs: The buckets must be in
ascending size order).
The improvement from malloc, as a guess, (I used a profiler to see if it does something) is that
a. the memory is already there, no need to fetch it if not available and no need to check if it
is available
b. there are no locks. since we already assume that the code that does the allocations is
protected either by the absence of multithreading or by the pool's lock
c. there is no fragmentation, at least until the memory runs out, the pool's memory is localized
*/
/* No shared pointers if not needed: around 80% time spent decrease */
/* Own allocator: around 10%-30% further time decrease (not sure if it was worth the time, but at
least my test go smoother now) */
/* array of {element size, bucket size} */
constexpr std::pair<int, int> allocator_bucket_sizes[] = {
{32, COLIB_ALLOCATOR_SCALE * 1024},
{64, COLIB_ALLOCATOR_SCALE * 512},
{128, COLIB_ALLOCATOR_SCALE * 256},
{512, COLIB_ALLOCATOR_SCALE * 64},
{2048, COLIB_ALLOCATOR_SCALE * 16}
};
template <typename T>
struct allocator_t {
constexpr static int common_alignment = 16;
static_assert(common_alignment % __STDCPP_DEFAULT_NEW_ALIGNMENT__ == 0,
"ERROR: The allocator assumption is that internal data is aligned to at most 16 bytes");
using value_type = T;
allocator_t(pool_t *pool) : pool(pool) {}
template<typename U>
allocator_t(const allocator_t <U>& o) noexcept : pool(o.pool) {}
template<typename U>
allocator_t &operator = (const allocator_t <U>& o) noexcept { this->pool = o.pool; return *this; }
T* allocate(size_t n);
void deallocate(T* _p, std::size_t n) noexcept;
template <typename U>
bool operator != (const allocator_t<U>& o) noexcept { return this->pool != o.pool; }
template <typename U>
bool operator == (const allocator_t<U>& o) noexcept { return this->pool == o.pool; }
protected:
template <typename U>
friend struct allocator_t; /* lonely class */
pool_t *pool = nullptr; /* this allocator should allways be called on a valid pool */
};
template <typename T>
struct deallocator_t { /* This class is part of the allocator implementation and is here only
because a definition needs it as a full type */
pool_t *pool = nullptr;
void operator ()(T *p) { p->~T(); allocator_t<T>{pool}.deallocate(p, 1); }
};
/* having a task be called on two pools or two threads is UB */
struct pool_t {
/* you use the pointer made by create_pool */
pool_t(pool_t& sem) = delete;
pool_t(pool_t&& sem) = delete;
pool_t &operator = (pool_t& sem) = delete;
pool_t &operator = (pool_t&& sem) = delete;
~pool_t() { clear(); }
/* Same as colib::sched, but used outside of a corutine, based on the pool */
template <typename T>
void sched(task<T> task, const modif_pack_t& v = {}); /* ignores return type */
#if COLIB_ENABLE_MULTITHREAD_SCHED
template <typename T>
void thread_sched(task<T> task);
#endif /* COLIB_ENABLE_MULTITHREAD_SCHED */
/* runs the event loop */
run_e run();
/* clears all the corutines that are linked to this pool, so all waiters, all ready tasks, etc.
This will happen automatically inside the destructor */
error_e clear();
/* stops awaiters from waiting on the descriptor (this is the non-awaiting variant) */
error_e stop_io(const io_desc_t& io_desc);
/* Better to not touch this function, you need to understand the internals of pool_t to use it */
pool_internal_t *get_internal();
int64_t stopval = 0; /* a value that is set by force_stop(stopval) */
std::shared_ptr<void> user_ptr; /* the library won't touch this, do whatever with it */
protected:
template <typename T>
friend struct allocator_t;
std::unique_ptr<allocator_memory_t> allocator_memory;
friend inline std::shared_ptr<pool_t> create_pool();
pool_t();
private:
std::unique_ptr<pool_internal_t> internal;
};
struct sem_t {
struct unlocker_t{ /* compatibility with guard objects ex: std::lock_guard guard(co_await s); */
sem_t *sem;
unlocker_t(sem_t *sem) : sem(sem) {}
void lock() {}
void unlock() { sem->signal(); }
};
/* you use the pointer made by create_sem */
sem_t(sem_t& sem) = delete;
sem_t(sem_t&& sem) = delete;
sem_t &operator = (sem_t& sem) = delete;
sem_t &operator = (sem_t&& sem) = delete;
/* If the semaphore dies while waiters wait, they will all be forcefully destroyed (their entire
call stack) */
~sem_t();
sem_awaiter_t wait(); /* if awaited returns unlocker_t{} */
error_e signal(); /* returns error if the pool disapeared */
bool try_dec();
/* again, beeter don't touch, same as pool */
sem_internal_t *get_internal();
protected:
template <typename T, typename ...Args>
friend inline T *alloc(pool_t *, Args&&...);
sem_t(pool_t *pool, int64_t val = 0);
private:
std::unique_ptr<sem_internal_t, deallocator_t<sem_internal_t>> internal;
};
#if COLIB_OS_LINUX
/* This describes the async io op. */
struct io_desc_t {
int fd = -1; /* file descriptor */
uint32_t events = 0xffff'ffff; /* epoll events to be waited on the file descriptor */
bool is_valid() { return fd > 0; }
};
#endif /* COLIB_OS_LINUX */
#if COLIB_OS_WINDOWS
struct io_data_t {
enum io_flag_e : int32_t {
IO_FLAG_NONE = 0,
IO_FLAG_TIMER = 1,
IO_FLAG_ADDED = 2,
IO_FLAG_TIMER_RUN = 4,
};
OVERLAPPED overlapped = {0}; /* must be the first member of this struct */
io_flag_e flags = io_flag_e{0};
state_t *state = nullptr; /* state of the task */
DWORD recvlen = 0;
std::function<error_e(void *)> io_request; /* function to be called inside add_waiter */
void *ptr = nullptr; /* can be context for io_request or timer */
HANDLE h = NULL; /* same as bellow */
};
struct io_desc_t {
std::shared_ptr<io_data_t> data = nullptr;
HANDLE h = NULL;
bool is_valid() { return h != NULL; }
};
#endif /* COLIB_OS_WINDOWS */
#if COLIB_OS_UNKNOWN
/* This describes the async io op. */
COLIB_OS_UNKNOWN_IO_DESC
#endif /* COLIB_OS_UNKNOWN */
/* This is the state struct that each corutine has */
struct state_t {
error_e err = ERROR_OK; /* holds the error return in diverse cases */
pool_t *pool = nullptr; /* the pool of this coro */
modif_table_p modif_table; /* we allocate a table only if there are mods */
state_t *caller_state = nullptr; /* this holds the caller's state, and with it the
return path */
std::coroutine_handle<void> self; /* the coro's self handle */
std::exception_ptr exception = nullptr; /* the exception that must be propagated */
std::shared_ptr<void> user_ptr; /* this is a pointer that the user can use for whatever
he feels like. This library will not touch this pointer */
};
/* This is mostly internal */
using sem_waiter_handle_p =
std::shared_ptr< /* if this pointer is not available, the waiter was
evicted from the waiters list */
std::list< /* List with the semaphore waiters */
std::pair<
state_t *, /* The waiting corutine state */
std::shared_ptr<void> /* Where the shared_ptr is actually stored */
>,
allocator_t<std::
pair<
state_t *,
std::shared_ptr<void>
>
> /* profiling shows the default is slow */
>::iterator /* iterator in the respective list */
>;
/* A modif is a callback for a specifi stage in the corutine's flow */
struct modif_t {
using variant_t = std::variant<
std::function<error_e(state_t *)>, /* call_cbk */
std::function<error_e(state_t *)>, /* sched_cbk */
std::function<error_e(state_t *)>, /* exit_cbk */
std::function<error_e(state_t *)>, /* leave_cbk */
std::function<error_e(state_t *)>, /* enter_cbk */
std::function<error_e(state_t *, io_desc_t&)>, /* wait_io_cbk */
std::function<error_e(state_t *, io_desc_t&)>, /* unwait_io_cbk */
/* wait_sem_cbk - OBS: the std::shared_ptr<void> part can be ignored, it's internal */
std::function<error_e(state_t *, sem_t *, sem_waiter_handle_p)>,
/* unwait_sem_cbk */
std::function<error_e(state_t *, sem_t *, sem_waiter_handle_p)>
>;
variant_t cbk;
modif_e type = CO_MODIF_COUNT;
modif_flags_e flags = CO_MODIF_INHERIT_ON_CALL;
};
/* Pool & Sched functions:
------------------------------------------------------------------------------------------------ */
inline std::shared_ptr<pool_t> create_pool();
inline task<pool_t *> get_pool();
inline task<state_t *> get_state();
/* This does not stop the curent corutine, it only schedules the task, but does not yet run it. */
template <typename T>
inline sched_awaiter_t<T> sched(task<T> to_sched, const modif_pack_t& v = {});
/* This stops the current coroutine from running and places it at the end of the ready queue. */
inline yield_awaiter_t yield();
/* Modifications
------------------------------------------------------------------------------------------------ */
template <modif_e type, typename Cbk>
inline modif_p create_modif(pool_t *pool, modif_flags_e flags, Cbk&& cbk);
template <modif_e type, typename Cbk>
inline modif_p create_modif(pool_p pool, modif_flags_e flags, Cbk&& cbk);
/* This returns the internal vec that holds the different modifications of the task. Changing them
here is ill-defined */
template <typename T>
inline std::vector<modif_p> task_modifs(task<T> t);
/* This adds a modifier to the task and returns the task. Duplicates will be ignored */
template <typename T>
inline task<T> add_modifs(pool_t *pool, task<T> t, const std::set<modif_p>& mods); /* not awaitable */
/* Removes modifier from the vector */
template <typename T>
inline task<T> rm_modifs(task<T> t, const std::set<modif_p>& mods); /* not awaitable */
/* same as above, but adds them for the current task */
inline task<std::vector<modif_p>> task_modifs();
inline task_t add_modifs(const std::set<modif_p>& mods);
inline task_t rm_modifs(const std::set<modif_p>& mods);
/* calls an awaitable inside a task_t, this is done to be able to use modifs on awaitables */
template <typename Awaiter>
inline task_t await(Awaiter&& awaiter);
/* Timing
------------------------------------------------------------------------------------------------ */
/* adds a timeout modification to the task, i.e. the corutine will be stopped from executing if the
given time passes (uses create_killer) */
template <typename T>
inline task<std::pair<T, error_e>> create_timeo(
task<T> t, pool_t *pool, const std::chrono::microseconds& timeo);
/* sleep functions */
inline task_t sleep_us(uint64_t timeo_us);
inline task_t sleep_ms(uint64_t timeo_ms);
inline task_t sleep_s(uint64_t timeo_s);
inline task_t sleep(const std::chrono::microseconds& us);
/* Flow Controll:
------------------------------------------------------------------------------------------------ */
/* Creates semaphores, those need the pool to exist */
inline sem_p create_sem(pool_t *pool, int64_t val);
inline sem_p create_sem(pool_p pool, int64_t val);
inline task<sem_p> create_sem(int64_t val);
inline std::pair<modif_pack_t, std::function<error_e(void)>> create_killer(pool_t *pool, error_e e);
template <typename T>
inline task<T> create_future(pool_t *pool, task<T> t); /* not awaitable */
template <typename ...ret_v>
inline task<std::tuple<ret_v>...> wait_all(task<ret_v>... tasks);
inline task_t force_stop(int64_t stopval = 0);
/* EPOLL:
------------------------------------------------------------------------------------------------ */
inline task_t wait_event(const io_desc_t& io_desc);
/* closing a file descriptor while it is managed by the coroutine pool will break the entire system
so you must use stop_fd on the file descriptor before you close it. This makes sure the fd is
awakened and ejected from the system before closing it. For example:
co_await colib::stop_fd(fd);
close(fd);
*/
inline task_t stop_io(const io_desc_t& io_desc);
#if COLIB_OS_LINUX
/* Linux Specific:
------------------------------------------------------------------------------------------------ */
inline task_t stop_fd(int fd);
inline task_t connect(int fd, sockaddr *sa, socklen_t *len);
inline task_t accept(int fd, sockaddr *sa, socklen_t *len);
inline task<ssize_t> read(int fd, void *buff, size_t len);
inline task<ssize_t> write(int fd, const void *buff, size_t len);
inline task_t read_sz(int fd, void *buff, size_t len);
inline task_t write_sz(int fd, const void *buff, size_t len);
#endif /* COLIB_OS_LINUX */
/* Windows Specific:
------------------------------------------------------------------------------------------------ */
#if COLIB_OS_WINDOWS
/* similar to stop_io, but stops all the waiters on a handle */
inline task_t stop_handle(HANDLE h);
/* Those functions are the same as their Windows API equivalent, the difference is that they don't
expose the overlapped structure, which is used by the coro library. They require a handle that
is compatible with iocp and they will attach the handle to the iocp instance. Those are the
functions listed by msdn to work with iocp (and connect, that is part of an extension) */
inline task<BOOL> ConnectEx(SOCKET s,
const sockaddr *name,
int namelen,
PVOID lpSendBuffer,
DWORD dwSendDataLength,
LPDWORD lpdwBytesSent);
inline task<BOOL> AcceptEx(SOCKET sListenSocket,
SOCKET sAcceptSocket,
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
LPDWORD lpdwBytesReceived);
inline task<BOOL> ConnectNamedPipe(HANDLE hNamedPipe);
inline task<BOOL> DeviceIoControl(HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned);
inline task<BOOL> LockFileEx(HANDLE hFile,
DWORD dwFlags,
DWORD dwReserved,
DWORD nNumberOfBytesToLockLow,
DWORD nNumberOfBytesToLockHigh,
uint64_t *offset);
inline task<BOOL> ReadDirectoryChangesW(HANDLE hDirectory,
LPVOID lpBuffer,
DWORD nBufferLength,
BOOL bWatchSubtree,
DWORD dwNotifyFilter,
LPDWORD lpBytesReturned,
LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
inline task<BOOL> ReadFile(HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead);
inline task<BOOL> TransactNamedPipe(HANDLE hNamedPipe,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesRead);
inline task<BOOL> WaitCommEvent(HANDLE hFile,
LPDWORD lpEvtMask);
inline task<BOOL> WriteFile(HANDLE hFile,
LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten,
uint64_t *offset);
inline task<BOOL> WSASendMsg(SOCKET Handle,
LPWSAMSG lpMsg,
DWORD dwFlags,
LPDWORD lpNumberOfBytesSent,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
inline task<BOOL> WSASendTo(SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
const sockaddr *lpTo,
int iTolen,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
inline task<BOOL> WSASend(SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
inline task<BOOL> WSARecvFrom(SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
sockaddr *lpFrom,
LPINT lpFromlen,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
inline task<BOOL> WSARecvMsg(SOCKET s,
LPWSAMSG lpMsg,
LPDWORD lpdwNumberOfBytesRecvd,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
inline task<BOOL> WSARecv(SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
/* Addaptations for windows */
inline task_t connect(SOCKET s, const sockaddr *sa, uint32_t len);
inline task<SOCKET> accept(SOCKET s, sockaddr *sa, uint32_t *len);
inline task<SSIZE_T> read(HANDLE h, void *buff, size_t len);
inline task<SSIZE_T> write(HANDLE h, const void *buff, size_t len, uint64_t *offset = nullptr);
inline task_t read_sz(HANDLE h, void *buff, size_t len);
inline task_t write_sz(HANDLE h, const void *buff, size_t len, uint64_t *offset = nullptr);
#endif /* COLIB_OS_WINDOWS */
/* Unknown Specific:
------------------------------------------------------------------------------------------------ */
#if COLIB_OS_UNKNOWN
/* You implement your own */
#endif /* COLIB_OS_UNKNOWN */
/* Debug Interfaces:
------------------------------------------------------------------------------------------------ */
/* why would I replace the old string with the new one based on the allocator? because I need to
know that the library allocates only through the allocator, so debugging would interfere with
that. */
using dbg_string_t = std::basic_string<char, std::char_traits<char>, allocator_t<char>>;
template <typename... Args>
inline void dbg(const char *file, const char *func, int line, const char *fmt, Args&&... args);
/* Registers a name for a given task */
template <typename T, typename ...Args>
inline task<T> dbg_register_name(task<T> t, const char *fmt, Args&&...);
template <typename P, typename ...Args>
inline std::coroutine_handle<P> dbg_register_name(std::coroutine_handle<P> h,
const char *fmt, Args&&...);
template <typename ...Args>
inline void * dbg_register_name(void *addr, const char *fmt, Args&&...);
/* creates a modifier that traces things from corutines, mostly a convenience function, it also
uses the log_str function, or does nothing else if it isn't defined. If you don't like the
verbosity, be free to null any callback you don't care about. */
inline modif_pack_t dbg_create_tracer(pool_t *pool);
/* Obtains the name given to the respective task, handle or address */
template <typename T>
inline dbg_string_t dbg_name(task<T> t);
template <typename P>
inline dbg_string_t dbg_name(std::coroutine_handle<P> h);
inline dbg_string_t dbg_name(void *v);
/* Obtains a string from the given enum */
inline dbg_string_t dbg_enum(error_e code);
inline dbg_string_t dbg_enum(run_e code);
#if COLIB_OS_LINUX
inline dbg_string_t dbg_epoll_events(uint32_t events);
#endif /* COLIB_OS_LINUX */
/* formats a string using the C snprintf, similar in functionality to a combination of
snprintf+std::format, in the version of g++ that I'm using std::format is not available */
template <typename... Args>
inline dbg_string_t dbg_format(const char *fmt, Args&& ...args);
/* calls log_str to save the log string */
#if COLIB_ENABLE_LOGGING
inline std::function<int(const dbg_string_t&)> log_str =
[](const dbg_string_t& msg){ return printf("%s", msg.c_str()); };
#endif /* COLIB_ENABLE_LOGGING */
/* IMPLEMENTATION ... */
1 Answer 1
Use Doxygen to document your code
Your code is extensively documented, which is great! However, this is not done in a standardized format. Consider using the Doxygen format to document your code. That way, you can use Doxygen tools to verify that you documented all types and functions, and create PDFs and HTML pages from them. Also, some IDEs might be able to parse Doxygen comments and use them for tooltips.
Note that Doxygen is not only for documenting individual functions, you can also write sections that document a class a a whole, and even just write chapters containing Markdown like you do at the start.
Use high standards in your code examples
Don't take shortcuts in the example code snippets in your documentation, even if they are never going to be executed and are just part of the documentation. Keep the standard high. Some things that can be improved:
colib::task<int32_t> coro = get_messages();
Whileget_messages()
technically returns a coroutine object, I would renamecoro
tomessages
, since that's what it actually represents.for (int32_t value = co_await coro; value; value = co_await coro)
can be replaced withwhile (int32_t value = co_await messages)
.printf("main: %d\n", value);
uses a C function in C++ code. Don't do that, either writestd::cout << "main: " << value << "\n";
or once you can use C++23,std::println("main: {}", value);
You could use more auto
, but in the examples it's nice to keep writing out the full types so that it's clear what they are.
Naming things
It's great that you have put everything in a namespace.
Some things can be improved:
- Suffixes like
_t
,_p
and_e
are not really necessary. I would just avoid them. However, I appreciate that you use these suffixes consistently. - Avoid unnecessary abbreviations. Instead of
modif
, just writemodifier
. Instead ofdbg
writedebug
, and so on.
Unnecessary use of std::shared_ptr
?
You use std::shared_ptr
in many places, but is it necessary or even desired in all those places? For example, why is user_ptr
a std::shared_ptr<void>
? Does that even make sense? I can't see anything using it in the code you posted, so it's hard to review this.
More to follow later.
-
\$\begingroup\$ I've implemented some of your suggestions already and I can't wait for your follow-up, thanks for spending your time reviewing my library! \$\endgroup\$Pangi– Pangi2025年06月02日 07:58:48 +00:00Commented Jun 2 at 7:58