Design Rationale

This page explains the key design decisions in Corosio and the trade-offs considered.

Coroutine-First Design

Decision

Every I/O operation returns an awaitable. There is no callback-based API.

Rationale

  • Simplicity: One interface, not two parallel APIs

  • Optimal codegen: No compatibility layer between callbacks and coroutines

  • Natural error handling: Structured bindings and exceptions work directly

  • Composability: Awaitables compose with standard coroutine patterns

Trade-off

Users without C20 coroutine support cannot use the library. This is intentional—Corosio targets modern C exclusively.

Affine Awaitable Protocol

Decision

Executor affinity propagates through await_suspend parameters rather than thread-local storage or coroutine promise members.

Rationale

  • Explicit data flow: The dispatcher visibly flows through the code

  • No hidden state: No surprises from thread-local or global state

  • Compatibility: Works with any coroutine framework that calls await_suspend

  • Efficient: Symmetric transfer works automatically when appropriate

Trade-off

Implementing affine awaitables requires additional await_suspend overloads. The complexity is contained in the library; users just co_await.

io_result<T> Type

Decision

Operations return io_result<T> which combines an error code with optional values and supports both structured bindings and exceptions.

Rationale

  • Flexibility: Users choose error handling style per-callsite

  • Zero overhead: No exception overhead when using structured bindings

  • No information loss: Byte count available even on error

  • Clean syntax: auto [ec, n] = co_await …​ is concise

Trade-off

The .value() method name might conflict with users' expectations from std::optional (which throws on empty). Here it throws on error, which is semantically similar but contextually different.

Type-Erased Dispatchers

Decision

Socket implementations use capy::any_dispatcher internally rather than templating on the executor type.

Rationale

  • Binary size: Only one implementation per I/O object

  • Compile time: No template instantiation explosion

  • Virtual interface: Enables platform-specific implementations

Trade-off

Small runtime overhead from type erasure. For I/O-bound code, this is negligible compared to actual I/O latency (microseconds vs. nanoseconds).

Inheritance Hierarchy

Decision

socket inherits from io_stream which inherits from io_object.

io_object
    ├── acceptor
    ├── resolver
    ├── timer
    ├── signal_set
    └── io_stream
            ├── socket
            └── wolfssl_stream

Rationale

  • Polymorphism: Code accepting io_stream& works with any stream type

  • Code reuse: read() and write() free functions work with all streams

  • Future extensibility: New stream types fit naturally

Trade-off

Virtual function overhead for read_some()/write_some(). Acceptable because I/O operations are inherently expensive.

Buffer Type Erasure (any_bufref)

Decision

Buffer sequences are type-erased at the I/O boundary using any_bufref.

Rationale

  • Non-template implementations: Scheduler and I/O objects aren’t templates

  • ABI stability: Buffer types can change without recompilation

  • Reduced binary size: Single implementation handles all buffer types

Trade-off

One level of indirection when copying buffer descriptors. The copy is into a small fixed-size array, so overhead is minimal.

consuming_buffers for Composed Operations

Decision

The read() and write() composed operations use consuming_buffers to track progress through buffer sequences.

Rationale

  • Efficiency: Avoids copying buffer sequences

  • Correctness: Handles partial reads/writes across multiple buffers

  • Reusability: Can be used directly by advanced users

Trade-off

More complex than repeatedly constructing sub-buffers, but more efficient for multi-buffer sequences.

Separate open() and connect()

Decision

Sockets require explicit open() before connect().

Rationale

  • Explicit resource management: Clear when system resources are allocated

  • Error handling: Open errors distinct from connect errors

  • Consistency: Matches acceptor pattern (explicit listen())

Trade-off

Two calls instead of one. A connect(endpoint) overload that opens automatically could be added if users prefer.

Move-Only I/O Objects

Decision

Sockets, timers, and other I/O objects are move-only.

Rationale

  • Ownership semantics: I/O objects own system resources

  • No accidental copies: Prevents resource leaks

  • Efficient transfer: Moving is cheap (pointer swap)

Trade-off

Cannot store in containers that require copyability. Use std::unique_ptr or move-aware containers.

Context-Locked Move Assignment

Decision

Moving an I/O object to another with a different execution context throws.

Rationale

  • Safety: Prevents dangling references to old context’s services

  • Simplicity: No need for detach/reattach mechanism

Trade-off

Cannot move objects between contexts. Create new objects instead.

Platform-Specific Backends

Decision

Windows uses IOCP directly. Linux will use io_uring. macOS will use kqueue.

Rationale

  • Performance: Native backends are fastest

  • Scalability: Platform-optimized for thousands of connections

  • Features: Full access to platform capabilities

Trade-off

More implementation work per platform. Epoll fallback could be added for broader Linux compatibility.

WolfSSL for TLS

Decision

TLS is provided through WolfSSL rather than OpenSSL.

Rationale

  • Small footprint: WolfSSL is more compact

  • Clean API: Modern C++ friendly

  • Licensing: Flexible licensing options

Trade-off

OpenSSL is more widely deployed. Users who need OpenSSL can create their own stream wrapper following the io_stream interface.

No UDP (Yet)

Decision

Only TCP is currently supported.

Rationale

  • Focus: TCP covers most use cases

  • Complexity: UDP requires different abstractions (datagrams vs. streams)

  • Priority: Get TCP right first

Trade-off

Users needing UDP must use other libraries. UDP support is planned.

Single-Header Include

Decision

<boost/corosio.hpp> includes core functionality but not everything.

Rationale

  • Convenience: Easy to get started

  • Control: Advanced headers included explicitly

  • Compile time: Full include not excessive

The main header includes:

  • io_context

  • socket

  • endpoint

  • resolver

  • read/write

Not included (explicit include required):

  • acceptor

  • timer

  • signal_set

  • wolfssl_stream

  • test/mocket

Summary

Corosio’s design prioritizes:

  1. Simplicity: One way to do things, not two

  2. Performance: Zero-overhead abstractions where possible

  3. Safety: Ownership semantics prevent resource leaks

  4. Composability: Works with standard C++ patterns

  5. Extensibility: Clean hierarchy for new types

Trade-offs generally favor correctness and clarity over maximum flexibility.