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
8 changes: 6 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ foreach (BOOST_COROSIO_DEPENDENCY ${BOOST_COROSIO_DEPENDENCIES})
endif ()
endforeach ()

# Include asio and filesystem which are needed by capy's tests
if (BOOST_COROSIO_BUILD_TESTS)
list(APPEND BOOST_COROSIO_INCLUDE_LIBRARIES asio filesystem)
endif ()

# Complete dependency list
set(BOOST_INCLUDE_LIBRARIES ${BOOST_COROSIO_INCLUDE_LIBRARIES})
set(BOOST_EXCLUDE_LIBRARIES corosio)
Expand Down Expand Up @@ -141,8 +146,7 @@ function(boost_corosio_setup_properties target)
target_compile_features(${target} PUBLIC cxx_std_20)
target_include_directories(${target} PUBLIC "${PROJECT_SOURCE_DIR}/include")
target_include_directories(${target} PRIVATE
"${PROJECT_SOURCE_DIR}/src/corosio"
"${PROJECT_SOURCE_DIR}/src/corosio/src")
"${PROJECT_SOURCE_DIR}/src/corosio")
target_link_libraries(${target}
PUBLIC
${BOOST_COROSIO_DEPENDENCIES}
Expand Down
5 changes: 3 additions & 2 deletions include/boost/corosio/detail/scheduler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@

#include <boost/corosio/detail/config.hpp>
#include <boost/capy/ex/any_coro.hpp>
#include <boost/capy/ex/execution_context.hpp>

#include <cstddef>

namespace boost {
namespace corosio {
namespace detail {

class scheduler_op;

struct scheduler
{
virtual ~scheduler() = default;
virtual void post(capy::any_coro) const = 0;
virtual void post(capy::execution_context::handler*) const = 0;
virtual void post(scheduler_op*) const = 0;
virtual void on_work_started() noexcept = 0;
virtual void on_work_finished() noexcept = 0;
virtual bool running_in_this_thread() const noexcept = 0;
Expand Down
7 changes: 4 additions & 3 deletions src/corosio/src/detail/posix_op.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
#include <boost/capy/concept/affine_awaitable.hpp>
#include <boost/capy/ex/any_coro.hpp>
#include <boost/capy/error.hpp>
#include <boost/capy/ex/execution_context.hpp>
#include <boost/system/error_code.hpp>

#include "src/detail/scheduler_op.hpp"

#include <unistd.h>
#include <errno.h>

Expand All @@ -42,7 +43,7 @@ namespace detail {
It stores the coroutine handle, dispatcher, and result
pointers needed to complete an async operation.
*/
struct posix_op : capy::execution_context::handler
struct posix_op : scheduler_op
{
struct canceller
{
Expand Down Expand Up @@ -136,7 +137,7 @@ struct posix_op : capy::execution_context::handler
};

inline posix_op*
get_posix_op(capy::execution_context::handler* h) noexcept
get_posix_op(scheduler_op* h) noexcept
{
return static_cast<posix_op*>(h->data());
}
Expand Down
6 changes: 3 additions & 3 deletions src/corosio/src/detail/posix_scheduler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ posix_scheduler::
post(capy::any_coro h) const
{
struct post_handler final
: capy::execution_context::handler
: scheduler_op
{
capy::any_coro h_;

Expand Down Expand Up @@ -172,7 +172,7 @@ post(capy::any_coro h) const

void
posix_scheduler::
post(capy::execution_context::handler* h) const
post(scheduler_op* h) const
{
outstanding_work_.fetch_add(1, std::memory_order_relaxed);

Expand Down Expand Up @@ -411,7 +411,7 @@ do_one(long timeout_us)
return 0;

// First check if there are handlers in the queue
capy::execution_context::handler* h = nullptr;
scheduler_op* h = nullptr;
{
std::lock_guard lock(mutex_);
h = completed_ops_.pop();
Expand Down
7 changes: 3 additions & 4 deletions src/corosio/src/detail/posix_scheduler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
#include <boost/corosio/detail/config.hpp>
#include <boost/corosio/detail/scheduler.hpp>
#include <boost/capy/ex/execution_context.hpp>
#include <boost/capy/core/intrusive_queue.hpp>

#include "src/detail/scheduler_op.hpp"

#include <atomic>
#include <chrono>
Expand All @@ -25,8 +26,6 @@ namespace boost {
namespace corosio {
namespace detail {

using op_queue = capy::intrusive_queue<capy::execution_context::handler>;

// Forward declaration
struct posix_op;

Expand Down Expand Up @@ -68,7 +67,7 @@ class posix_scheduler

void shutdown() override;
void post(capy::any_coro h) const override;
void post(capy::execution_context::handler* h) const override;
void post(scheduler_op* h) const override;
void on_work_started() noexcept override;
void on_work_finished() noexcept override;
bool running_in_this_thread() const noexcept override;
Expand Down
227 changes: 227 additions & 0 deletions src/corosio/src/detail/scheduler_op.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/cppalliance/corosio
//

#ifndef BOOST_COROSIO_DETAIL_SCHEDULER_OP_HPP
#define BOOST_COROSIO_DETAIL_SCHEDULER_OP_HPP

#include <boost/corosio/detail/config.hpp>
#include <boost/capy/core/intrusive_queue.hpp>

namespace boost {
namespace corosio {
namespace detail {

/** 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 scheduler_op_queue
*/
class scheduler_op : public capy::intrusive_queue<scheduler_op>::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:
~scheduler_op() = default;

void* data_ = nullptr;
};

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

using op_queue = capy::intrusive_queue<scheduler_op>;

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

/** An intrusive FIFO queue of scheduler_ops.

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

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

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

@see scheduler_op
*/
class scheduler_op_queue
{
op_queue q_;

public:
/** Default constructor.

Creates an empty queue.

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

/** Move constructor.

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

@param other The queue to move from.

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

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

/** Destructor.

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

/** Return true if the queue is empty.

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

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

@param h Pointer to the scheduler_op to add.

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

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

All scheduler_ops 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(scheduler_op_queue& other) noexcept
{
q_.splice(other.q_);
}

/** Remove and return the front scheduler_op.

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

} // namespace detail
} // namespace corosio
} // namespace boost

#endif
Loading
Loading