https://github.com/cyanide-burnout/compromise
Simple C++ coroutine helper library
https://github.com/cyanide-burnout/compromise
coroutine cpp
Last synced: 11 months ago
JSON representation
Simple C++ coroutine helper library
- Host: GitHub
- URL: https://github.com/cyanide-burnout/compromise
- Owner: cyanide-burnout
- License: mit
- Created: 2022-10-22T10:14:33.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2024-03-11T09:27:10.000Z (over 2 years ago)
- Last Synced: 2025-01-21T22:15:51.423Z (over 1 year ago)
- Topics: coroutine, cpp
- Language: C++
- Homepage:
- Size: 92.8 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Compromise
Simple C++ coroutine helper library, suitable to integrate with main loop.
Artem Prilutskiy, 2022-2025
*Please note*, examples are incomplete but functional :)
## Features
* Can be integrated to main loop
* Allows call coroutines from coroutines asynchronously (asymmetric coroutines)
* Allows proceed coroutines status in callbacks (see Hook)
* Provides base interface to create asynchronous wrappers to be called from coroutines (Compromise::Emitter)
* **Not thread safe**
## Main components
### Compromise::Task / Compromise::Future
Compromise::Future is main type that represents a coroutine, Compromise::Task is just an alias to simplify readability.
```C++
Compromise::Task TestYield()
{
co_yield std::any();
for (int number = 0; number < 10; number ++)
co_yield number;
}
```
There is no suspend on the start of coroutine. In case when you need to suspend coroutine immedietly after the start you can always use co_yield with empty std::any().
Now is about co_return. To make coroutines more generic, there is no support of return values, but you can always use co_yield to pass result before exiting.
Compromise::Future is a future class implementation no manage coroutine.
* Can be created on the stack as well as on the heap
* Controls live-cycle of coroutine
* Provides awaitable interface to the caller, which allows to call one coroutine from another one
#### Methods
* **done()** - coroutine done or not exists
* **value()** - get last value, passed by co_yield
* **resume()** - resumes coroutine
* **handle()** - get coroutine handle
* **rethrow()** - rethrows an unhandled exception (if any)
* **release()** - releases binding between coroutine and future
* **operator bool()** - coroutine exists and not done
* **operator ()()** - resumes coroutine and returns a value, passed by co_yield (synchronous call, value may be not set in case of incomplete execution)
* **operator co_await()** - resumes coroutine and returns a value, passed by co_yield (asynchronous call)
```C++
Compromise::Task TestInvokeFromCoroutine()
{
auto routine = TestYield();
while (routine)
{
auto& data = co_await routine;
if (data.has_value()) printf("Result: %d\n", std::any_cast(data));
}
}
void TestInvokeFromFunction()
{
auto routine = TestYield();
while (routine)
{
auto& data = routine();
if (data.has_value()) printf("Result: %d\n", std::any_cast(data));
}
}
Compromise::Task taskOnHeap = new Compromise::Task(TestYield());
```
#### Exception forwarding and handling
Any unhandled exception in coroutine code will be forwarded to the main code.
```C++
Compromise::Task TestException()
{
throw std::exception();
}
auto routine = TestException();
try
{
routine.rethrow();
}
catch (const std::exception& exception)
{
printf("Unhandled exception: %s\n", exception.what());
}
```
#### Hook
Compromise::Future allows you to hook events of coroutine in callback manner. For example for cases, when you need to wait a result without pooling a coroutine or collect garbage.
```C++
auto routine = new Compromise::Future(TestYield());
routine->hook = [] (Compromise::Future* future, Compromise::Status status) -> bool
{
if (status == Compromise::Yield)
{
// This code will be called on co_yield
auto& data = future->value();
if (data.has_value()) printf("Test hook: %d\n", std::any_cast(data));
return true; // Don't suspend a coroutine, the result is already handled
}
if (status == Compromise::Return)
{
// This code will be called after the end of coroutine's execution
printf("Test hook done\n");
future->release(); // Unbind coroutine from the future to prevent collision and fault on future's destruction
delete future; // Since we created future on the heap we have to delete it
return true; // Allow coroutine to destruct itself
}
return false; // Suspend a coroutine (just an example)
};
// To avoid a loose of hook, coroutine used call co_yield Compromise::Empty() to suspend its execution immediately after creation
routine->resume();
```
### Compromise::Emitter
Compromise::Emitter is a wrapper to transform callback-style code into an awaitable, where Type is a type of object to return in co_await.
#### Methods:
* **wake(value)** - resumes coroutine
* **update(value&)** - optional virtual method, called by Emitter to initiate asynchronous call or update the value immediately
```C++
Compromise::Task TestClient(CloudClient* context)
{
auto result = co_await CoCloud::Client(context, "https://ya.ru/robots.txt");
if (result.code >= 0) printf("HTTP Code %d: %s\n", result.code, result.data);
}
Compromise::Task TestResolver(struct ResolverState* context)
{
CoResolver::Query query1(context, "127.0.0.1");
CoResolver::Query query2(context, "rotate.aprs.net", AF_INET);
PrintHostAddress(co_await query1);
for (int count = 0; count < 10; count ++) PrintHostAddress(co_await query2);
}
```
Here is an example of wrapper code:
```C++
struct ResolverEvent
{
int status;
int timeouts;
struct hostent* entry;
};
class Query : public Compromise::Emitter
{
public:
Query(struct ResolverState* state, const char* name, int family = AF_UNSPEC) : state(state), name(name), family(family)
{
ClearHostEntryIterator(&iterator);
}
private:
int family;
const char* name;
struct ResolverState* state;
struct HostEntryIterator iterator;
bool update(Event& data)
{
ResolveHostAddress(state, name, family, invoke, this, &iterator);
return false; // Update is incomplete, suspend coroutine and wait for call to wake()
}
static void invoke(void* argument, int status, int timeouts, struct hostent* entry)
{
CoResolver::Query* self = static_cast(argument);
ValidateHostEntry(&status, entry, &self->iterator);
self->wake({ status, timeouts, entry });
}
};
struct PollEvent
{
int handle;
uint32_t flags;
uint64_t options;
};
class CoPoll : public Compromise::Emitter
{
public:
CoPoll(struct FastPoll* poll) : poll(poll) { }
~CoPoll() { RemoveAllEventHandlers(poll, invoke, this); }
void add(int handle, uint64_t flags) { AddEventHandler(poll, handle, flags, invoke, this); }
void modify(int handle, uint64_t flags) { ManageEventHandler(poll, handle, flags, invoke, this); }
void remove(int handle) { RemoveEventHandler(poll, handle); }
private:
struct FastPoll* poll;
static void invoke(int handle, uint32_t flags, void* data, uint64_t options)
{
static_cast(data)->wake({ handle, flags, options });
}
};
```