Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
490 changes: 62 additions & 428 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/.vscode/
/build/
/.cache/
/build/*
!/build/Jamfile
!/build/brotli.jam
/out/
Expand Down
28 changes: 19 additions & 9 deletions include/boost/capy/concept/executor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,25 @@ namespace capy {
`on_work_started()` on an equal executor.

@li `dispatch(h)` - Execute a coroutine, potentially immediately
if the executor determines it is safe to do so.
if the executor determines it is safe to do so. The executor
may block forward progress of the caller until execution
completes.

@li `post(h)` - Queue a coroutine for later execution. Shall not
block forward progress of the caller.
@li `post(h)` - Queue a coroutine for later execution. The
executor shall not block forward progress of the caller
pending completion.

@li `defer(h)` - Queue a coroutine for later execution, with
a hint that the caller prefers deferral. Semantically
identical to `post`, but conveys that the coroutine is a
continuation of the current call context.
@li `defer(h)` - Queue a coroutine for later execution. The
executor shall not block forward progress of the caller
pending completion. Semantically identical to `post`, but
conveys a preference that the coroutine is a continuation
of the current call context. The executor may use this
information to optimize or otherwise adjust invocation.

@par Synchronization

The invocation of `dispatch`, `post`, or `defer` synchronizes
with the invocation of the coroutine.

@par No-Throw Guarantee

Expand Down Expand Up @@ -78,8 +88,8 @@ concept executor =
std::copy_constructible<E> &&
std::equality_comparable<E> &&
requires(E& e, E const& ce, std::coroutine_handle<> h) {
// Execution context access
{ ce.context() } -> std::same_as<decltype(ce.context())&>;
// Execution context access (must not throw)
{ ce.context() } noexcept -> std::same_as<decltype(ce.context())&>;

// Work tracking (must not throw)
{ ce.on_work_started() } noexcept;
Expand Down
1 change: 1 addition & 0 deletions include/boost/capy/ex/async_run.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ struct async_run_task
Dispatcher d_;
Handler handler_;
std::exception_ptr ep_;
std::optional<task<T>> t_;

template<typename D, typename H, typename... Args>
promise_type(D&& d, H&& h, Args&&...)
Expand Down
202 changes: 0 additions & 202 deletions include/boost/capy/ex/execution_context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

#include <boost/capy/detail/config.hpp>
#include <boost/capy/concept/executor.hpp>
#include <boost/capy/core/intrusive_queue.hpp>
#include <concepts>
#include <mutex>
#include <tuple>
Expand Down Expand Up @@ -159,207 +158,6 @@ class BOOST_CAPY_DECL

//------------------------------------------------

/** Abstract base class for completion handlers.

Handlers are continuations that execute after an asynchronous
operation completes. They can be queued for deferred invocation,
allowing callbacks and coroutine resumptions to be posted to an
executor.

Handlers should execute quickly - typically just initiating
another I/O operation or suspending on a foreign task. Heavy
computation should be avoided in handlers to prevent blocking
the event loop.

Handlers may be heap-allocated or may be data members of an
enclosing object. The allocation strategy is determined by the
creator of the handler.

@par Ownership Contract

Callers must invoke exactly ONE of `operator()` or `destroy()`,
never both:

@li `operator()` - Invokes the handler. The handler is
responsible for its own cleanup (typically `delete this`
for heap-allocated handlers). The caller must not call
`destroy()` after invoking this.

@li `destroy()` - Destroys an uninvoked handler. This is
called when a queued handler must be discarded without
execution, such as during shutdown or exception cleanup.
For heap-allocated handlers, this typically calls
`delete this`.

@par Exception Safety

Implementations of `operator()` must perform cleanup before
any operation that might throw. This ensures that if the handler
throws, the exception propagates cleanly to the caller of
`run()` without leaking resources. Typical pattern:

@code
void operator()() override
{
auto h = h_;
delete this; // cleanup FIRST
h.resume(); // then resume (may throw)
}
@endcode

This "delete-before-invoke" pattern also enables memory
recycling - the handler's memory can be reused immediately
by subsequent allocations.

@note Callers must never delete handlers directly with `delete`;
use `operator()` for normal invocation or `destroy()` for cleanup.

@note Heap-allocated handlers are typically allocated with
custom allocators to minimize allocation overhead in
high-frequency async operations.

@note Some handlers (such as those owned by containers like
`std::unique_ptr` or embedded as data members) are not meant to
be destroyed and should implement both functions as no-ops
(for `operator()`, invoke the continuation but don't delete).

@see queue
*/
class handler : public intrusive_queue<handler>::node
{
public:
virtual void operator()() = 0;
virtual void destroy() = 0;

/** Returns the user-defined data pointer.

Derived classes may set this to store auxiliary data
such as a pointer to the most-derived object.

@par Postconditions
@li Initially returns `nullptr` for newly constructed handlers.
@li Returns the current value of `data_` if modified by a derived class.

@return The user-defined data pointer, or `nullptr` if not set.
*/
void* data() const noexcept
{
return data_;
}

protected:
~handler() = default;

void* data_ = nullptr;
};

//------------------------------------------------

/** An intrusive FIFO queue of handlers.

This queue stores handlers using an intrusive linked list,
avoiding additional allocations for queue nodes. Handlers
are popped in the order they were pushed (first-in, first-out).

The destructor calls `destroy()` on any remaining handlers.

@note This is not thread-safe. External synchronization is
required for concurrent access.

@see handler
*/
class queue
{
intrusive_queue<handler> q_;

public:
/** Default constructor.

Creates an empty queue.

@post `empty() == true`
*/
queue() = default;

/** Move constructor.

Takes ownership of all handlers from `other`,
leaving `other` empty.

@param other The queue to move from.

@post `other.empty() == true`
*/
queue(queue&& other) noexcept
: q_(std::move(other.q_))
{
}

queue(queue const&) = delete;
queue& operator=(queue const&) = delete;
queue& operator=(queue&&) = delete;

/** Destructor.

Calls `destroy()` on any remaining handlers in the queue.
*/
~queue()
{
while(auto* h = q_.pop())
h->destroy();
}

/** Return true if the queue is empty.

@return `true` if the queue contains no handlers.
*/
bool
empty() const noexcept
{
return q_.empty();
}

/** Add a handler to the back of the queue.

@param h Pointer to the handler to add.

@pre `h` is not null and not already in a queue.
*/
void
push(handler* h) noexcept
{
q_.push(h);
}

/** Splice all handlers from another queue to the back.

All handlers from `other` are moved to the back of this
queue. After this call, `other` is empty.

@param other The queue to splice from.

@post `other.empty() == true`
*/
void
push(queue& other) noexcept
{
q_.splice(other.q_);
}

/** Remove and return the front handler.

@return Pointer to the front handler, or `nullptr`
if the queue is empty.
*/
handler*
pop() noexcept
{
return q_.pop();
}
};

//------------------------------------------------

execution_context(execution_context const&) = delete;

execution_context& operator=(execution_context const&) = delete;
Expand Down
Loading
Loading