SaneCppFibersAsync.h is a bridge between Fibers and Async. It lets fiber tasks call async I/O helpers in synchronous-looking code while preserving the caller-owned request, buffer, and event-loop model from Async.
Dependencies
What FibersAsync Is For
FibersAsync is the I/O bridge for stackful fibers. It does not replace AsyncEventLoop and it does not replace
FiberScheduler. Instead, FiberAsyncIO wraps both:
AsyncEventLoop eventLoop;
SC_TRY(eventLoop.create());
FiberScheduler scheduler;
FiberAsyncCommand commands[8];
FiberAsyncIO io(scheduler, eventLoop, commands);
Fiber tasks can then wait for timers, sockets, files, processes, or signals by calling io.sleep(), io.receive(),
io.fileRead(), and similar methods. Internally, the lower-level AsyncRequest starts on the wrapped event loop, and
the current fiber suspends until the async completion wakes it.
Design Intent
The library keeps the spirit of Async:
FiberAsyncIOuses an externally ownedAsyncEventLoop&;FiberAsyncIOuses an externally ownedFiberScheduler&;- methods return plain
Result; - extra operation outputs are explicit caller-provided result objects;
- buffers, sockets, files, tasks, stacks, and output objects must remain valid while the operation is active;
- callback-style
Asynccode and fiber-styleFibersAsynccode can share the same event loop; - cross-thread event-loop access goes through bounded caller-provided
FiberAsyncCommandstorage.
FibersAsync is deliberately concrete for now. A more abstract FiberIO facade should wait until there is a second real
backend worth sharing behind one API.
A Sleep Example
The simplest example is a fiber that waits without blocking the OS thread:
struct State
{
FiberAsyncIO* io = nullptr;
int completed = 0;
};
AsyncEventLoop eventLoop;
SC_TRY(eventLoop.create());
FiberScheduler scheduler;
FiberAsyncIO io(scheduler, eventLoop);
FiberTask task;
char stackMemory[64 * 1024] = {};
FiberStack stack({stackMemory, sizeof(stackMemory)});
State state;
state.io = &io;
SC_TRY(scheduler.spawn(task, stack,
FiberTask::Procedure(
[&state](FiberScheduler&)
{
SC_TRY(state.io->sleep(TimeMs{1}));
state.completed++;
return Result(true);
})));
SC_TRY(io.runUntilComplete());
SC_TRY(task.result());
SC_TRY(eventLoop.close());
io.runUntilComplete() drives both sides: ready fibers through FiberScheduler, and async completions through
AsyncEventLoop.
Socket Echo Shape
Socket helpers follow the same Sane C++ result-object pattern as Async: operation status is returned as Result, and
data about the operation is written into explicit output objects.
Result echoOnce(FiberAsyncIO& io, const SocketDescriptor& socket)
{
char receiveBuffer[1024] = {};
FiberAsyncSocketReceiveResult received;
SC_TRY(io.receive(socket, {receiveBuffer, sizeof(receiveBuffer)}, received));
if (received.disconnected)
{
return Result(true);
}
FiberAsyncSocketSendResult sent;
SC_TRY(io.sendAll(socket, received.data, &sent));
return Result(true);
}
receive() writes the actual received byte range into received.data, which points into the caller-provided buffer.
sendAll() repeats lower-level send operations until the whole span is sent or an error/cancellation occurs.
File I/O Shape
File helpers also use caller-provided buffers and optional result objects:
Result copyChunk(FiberAsyncIO& io, const FileDescriptor& input, const FileDescriptor& output)
{
char readBuffer[4096] = {};
FiberAsyncFileReadResult readResult;
SC_TRY(io.fileRead(input, {readBuffer, sizeof(readBuffer)}, readResult));
if (readResult.endOfFile or readResult.data.sizeInBytes() == 0)
{
return Result(true);
}
FiberAsyncFileWriteResult writeResult;
SC_TRY(io.fileWriteAll(output, readResult.data, &writeResult));
return Result(true);
}
Offsets are explicit through fileReadAt(), fileReadExactAt(), fileWriteAt(), and fileWriteAllAt(). File
readiness is available through filePoll() for the currently supported platform/backend cases.
Worker-Pool I/O
AsyncEventLoop remains owner-thread-affine. If a fiber running on a worker thread starts I/O, FiberAsyncIO posts the
start/stop command back to the owner thread through bounded FiberAsyncCommand storage.
static constexpr size_t NumWorkers = 2;
AsyncEventLoop eventLoop;
SC_TRY(eventLoop.create());
FiberScheduler scheduler;
FiberAsyncCommand commands[8];
FiberAsyncIO io(scheduler, eventLoop, commands);
FiberWorker workers[NumWorkers];
FiberWorkerThread threads[NumWorkers];
FiberWorkerPool workerPool;
SC_TRY(workerPool.start(scheduler, {workers, NumWorkers}, {threads, NumWorkers}));
SC_TRY(io.runOwnerUntilComplete());
SC_TRY(workerPool.join());
SC_TRY(eventLoop.close());
The command storage size is a real capacity limit. If cross-thread producers can submit more simultaneous starts/stops
than the storage can hold, provide more FiberAsyncCommand slots or design the producer to apply backpressure.
Cancellation
Cancellation is cooperative and result-based. A task suspended in a FiberAsyncIO operation can be canceled through the
fiber scheduler; FiberAsyncIO stops the underlying async request when needed and wakes the fiber with an error
Result.
SC_TRY(scheduler.requestCancel(task));
SC_TRY(io.runUntilComplete());
if (not task.result())
{
// The task observed cancellation or another operation error.
}
FiberAsyncIO::cancelAll() is also available as an I/O bridge helper for canceling pending async operations associated
with the bridge.
Lifetime Rules
The lifetime rules are intentionally close to Async and Fibers:
AsyncEventLoopmust outliveFiberAsyncIO;FiberSchedulermust outliveFiberAsyncIO;FiberAsyncCommandstorage must outlive cross-thread operations using it;FiberTaskandFiberStackstorage must outlive the spawned task;- sockets, files, buffers, and output result objects must outlive the operation using them;
- request objects used internally are stack-local to the suspended fiber and remain valid because the fiber stack is stable while suspended.
This is the key difference from a heap-backed async framework: the caller chooses the maximum number of simultaneous tasks, stacks, command posts, and buffers.
Allocation Model
FibersAsync does not allocate coroutine frames and does not need C++20 coroutines. It relies on the active fiber stack
to hold the operation state while the fiber is suspended. Cross-thread command posting uses caller-provided
Span<FiberAsyncCommand> storage, and normal same-thread usage can omit command storage.
The lower-level Async and Fibers rules still apply: request objects and fiber objects must be memory-stable, and any
capacity that can grow must be supplied explicitly by the caller.
Relationship To Await
Await and FibersAsync both make Async code easier to read, but they make different tradeoffs.
Await uses C++20 coroutines and explicit AwaitAllocator coroutine-frame allocation. It is a good fit when you want
co_await syntax and compiler-generated coroutine state machines.
FibersAsync uses stackful fibers. It is a good fit when you want ordinary synchronous-looking function calls, the
ability to suspend through existing nested call stacks, and explicit stack storage instead of coroutine frames.
Both libraries keep callback-style Async integration possible because both wrap an existing AsyncEventLoop& instead
of owning a separate I/O runtime.
Features
| FibersAsync API | Description |
|---|---|
| FiberAsyncIO | Synchronous-looking fiber I/O wrapper around an externally owned AsyncEventLoop. |
| FiberAsyncCommand | Bounded command slot for owner-thread I/O posting. |
| FiberAsyncSocketSendResult | Result object populated by send, sendTo, and sendAll. |
| FiberAsyncSocketReceiveResult | Result object populated by receive. |
| FiberAsyncSocketReceiveFromResult | Result object populated by receiveFrom. |
| FiberAsyncFileWriteResult | Result object populated by file write helpers. |
| FiberAsyncFileReadResult | Result object populated by file read helpers. |
| FiberAsyncFileSendOptions | Options for fileSend. |
| FiberAsyncFileSendResult | Result object populated by fileSend. |
| FiberAsyncProcessExitResult | Result object populated by processExit. |
| FiberAsyncSignalResult | Result object populated by signal. |
Complete Examples
Examples/FibersDemoshows two sleeping fiber tasks driven by oneFiberAsyncIO, then the same bridge used with a worker pool.Tests/Libraries/FibersAsync/FibersAsyncTest.cppcontains focused examples for sleeps, sockets, UDP, files,fileSend, process exit, signals, cancellation, command queue overflow, and worker-pool cross-thread operation posting.Examples/FibersBenchmarkfocuses on CPU scheduling rather than I/O, but it is useful context for how the same scheduler scales task execution.
Status
Draft
Current support includes:
sleep();- socket
accept()andconnect(); - socket
receive(); - one-shot socket
send(); - socket
sendAll(); - datagram socket
sendTo()andreceiveFrom(); - file
fileRead(),fileReadAt(),fileReadExact(),fileReadExactAt(); - file
fileWrite(),fileWriteAt(),fileWriteAll(), andfileWriteAllAt(); - file readiness through
filePoll()where supported by the underlyingAsyncbackend; - file-to-socket transfer through
fileSend(); - process exit waiting through
processExit(); - one-shot signal waiting through
signal(); - owner-thread diagnostics through release assertions and
isOwnerThread(); - bounded cross-thread command posting from worker fibers to the event-loop owner thread;
- cooperative cancellation of pending operations through
FiberScheduler::requestCancel*andFiberAsyncIO::cancelAll(); - focused macOS, Linux, and Windows test coverage in
FibersAsyncTest.
Roadmap
- Add DNS and stream helpers when real examples justify the API.
- Explore blocking/threaded backends only after the concrete
FiberAsyncIObridge remains stable. - Keep expanding cancellation and shutdown stress tests around stop/complete races.
- Revisit integration adapters between
AwaitTaskand fiber tasks after both libraries stabilize. - Consider a runtime-selectable
FiberIOfacade only after at least two real backends exist.