mirror of
https://github.com/FEX-Emu/FEX.git
synced 2026-10-06 10:00:16 +02:00
463 lines
14 KiB
C++
463 lines
14 KiB
C++
// SPDX-License-Identifier: MIT
|
|
/**
|
|
* Helper framework to enable asynchronous IO operations on file descriptor objects (networking, files).
|
|
*
|
|
* Strongly inspired by Boost.Asio.
|
|
*/
|
|
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <chrono>
|
|
#include <optional>
|
|
#include <poll.h>
|
|
#include <span>
|
|
#include <utility>
|
|
#include <vector>
|
|
#include <unistd.h>
|
|
|
|
#include <FEXCore/fextl/functional.h>
|
|
#include <FEXCore/fextl/map.h>
|
|
#include <FEXCore/fextl/vector.h>
|
|
#include <FEXCore/Utils/LogManager.h>
|
|
|
|
namespace fasio {
|
|
|
|
enum class error {
|
|
success,
|
|
timeout, // User-specified timeout expired
|
|
eof, // Permanently reached end of data stream (e.g. because socket connection was closed by peer)
|
|
invalid, // Invalid input parameters
|
|
generic_errno // Read errno for details
|
|
};
|
|
|
|
/**
|
|
* This selects which action to trigger when returning from a reactor callback.
|
|
* The default (drop) will drop the callback so that the caller can register
|
|
* a new one.
|
|
*/
|
|
enum class post_callback {
|
|
drop, // Drop the callback
|
|
repeat, // Continue using the same callback
|
|
stop_reactor, // Triggers exit from run()
|
|
};
|
|
|
|
/**
|
|
* Core event loop for asynchronous code. Corresponds to asio::io_context,
|
|
* specialized for multiplexing file descriptors via ppoll().
|
|
*
|
|
* A reactor tracks a set of file descriptors and calls user-provided callbacks
|
|
* when they become ready. For example, the callback for a network socket will
|
|
* be called when data is ready to be reveived on the socket.
|
|
*
|
|
*/
|
|
struct poll_reactor {
|
|
private:
|
|
std::vector<pollfd> PollFDs;
|
|
std::optional<int> CurrentFD; // FD that is currently being processed
|
|
|
|
bool is_stopped = false;
|
|
int AsyncStopRequest[2] = {-1, -1};
|
|
|
|
// Maps FD to callback
|
|
fextl::map<int, fextl::move_only_function<post_callback(error)>> callbacks;
|
|
|
|
struct Event {
|
|
pollfd FD;
|
|
bool Erase = false;
|
|
bool Insert = false;
|
|
};
|
|
std::vector<Event> QueuedEvents;
|
|
|
|
public:
|
|
~poll_reactor() {
|
|
if (AsyncStopRequest[0] != -1) {
|
|
::close(AsyncStopRequest[0]);
|
|
}
|
|
if (AsyncStopRequest[1] != -1) {
|
|
::close(AsyncStopRequest[1]);
|
|
}
|
|
}
|
|
|
|
// Adds an internal FD to wake up and exit the reactor when stop_async() is called from any thread.
|
|
void enable_async_stop() {
|
|
::pipe(AsyncStopRequest);
|
|
PollFDs.push_back(pollfd {.fd = AsyncStopRequest[0], .events = POLLHUP, .revents = 0});
|
|
callbacks[AsyncStopRequest[0]] = [](error) {
|
|
return post_callback::stop_reactor;
|
|
};
|
|
}
|
|
|
|
void stop_async() {
|
|
if (AsyncStopRequest[1] == -1) {
|
|
ERROR_AND_DIE_FMT("Tried to use stop_async without calling enable_async_stop during setup");
|
|
}
|
|
// Wake up run() thread by closing this pipe endpoint
|
|
::close(AsyncStopRequest[1]);
|
|
AsyncStopRequest[1] = -1;
|
|
}
|
|
|
|
void cleanup() {
|
|
callbacks.clear();
|
|
}
|
|
|
|
[[nodiscard]]
|
|
bool stopped() const {
|
|
return is_stopped;
|
|
}
|
|
|
|
error run_one(std::optional<std::chrono::nanoseconds> Timeout = std::nullopt) {
|
|
// Process events queued before entering wait loop
|
|
update_fd_list();
|
|
|
|
timespec ts = to_timespec(Timeout.value_or(std::chrono::nanoseconds {0}));
|
|
|
|
// ppoll may return EINTR/EAGAIN, so a loop is used here. Normally, we return in the first iteration.
|
|
while (true) {
|
|
int Result = ::ppoll(PollFDs.data(), PollFDs.size(), Timeout ? &ts : nullptr, nullptr);
|
|
|
|
if (Result < 0) {
|
|
if (errno == EINTR || errno == EAGAIN) {
|
|
continue;
|
|
}
|
|
return error::generic_errno;
|
|
} else if (Result == 0) {
|
|
return error::timeout;
|
|
} else {
|
|
// Walk the FDs and see if we got any results
|
|
for (auto& ActiveFD : PollFDs) {
|
|
if (ActiveFD.revents == 0) {
|
|
continue;
|
|
}
|
|
if (Result-- == 0) {
|
|
break;
|
|
}
|
|
|
|
if (ActiveFD.revents & POLLIN) {
|
|
// NOTE: For sockets, this is triggered on close, too. Pipes only report POLLHUP, however.
|
|
CurrentFD = ActiveFD.fd;
|
|
|
|
auto Callback = std::move(callbacks[ActiveFD.fd]);
|
|
if (!Callback) {
|
|
ERROR_AND_DIE_FMT("Data available for reading on FD {} but no read callback registered", ActiveFD.fd);
|
|
}
|
|
auto Ret = Callback(error::success);
|
|
if (Ret == post_callback::repeat) {
|
|
callbacks[ActiveFD.fd] = std::move(Callback);
|
|
} else if (Ret == post_callback::drop) {
|
|
// If no new callback was registered, drop the FD from the list and skip any remaining events
|
|
if (!callbacks.contains(ActiveFD.fd)) {
|
|
QueuedEvents.push_back(Event {.FD = {.fd = ActiveFD.fd}, .Erase = true});
|
|
ActiveFD.revents = 0;
|
|
}
|
|
} else if (Ret == post_callback::stop_reactor) {
|
|
is_stopped = true;
|
|
}
|
|
CurrentFD.reset();
|
|
}
|
|
if (ActiveFD.revents & (POLLHUP | POLLERR | POLLNVAL | POLLRDHUP)) {
|
|
auto Callback = std::move(callbacks[ActiveFD.fd]);
|
|
if (Callback) {
|
|
is_stopped |= (Callback(error::eof) == post_callback::stop_reactor);
|
|
}
|
|
// Error or hangup, erase the socket from our list
|
|
QueuedEvents.push_back(Event {.FD = {.fd = ActiveFD.fd}, .Erase = true});
|
|
}
|
|
|
|
ActiveFD.revents = 0;
|
|
}
|
|
|
|
if (is_stopped) {
|
|
cleanup();
|
|
return error::success;
|
|
}
|
|
|
|
update_fd_list();
|
|
return error::success;
|
|
}
|
|
}
|
|
}
|
|
|
|
error run(std::optional<std::chrono::nanoseconds> Timeout = std::nullopt) {
|
|
while (true) {
|
|
auto Result = run_one(Timeout);
|
|
if (Result != error::success || is_stopped) {
|
|
cleanup();
|
|
return Result;
|
|
}
|
|
}
|
|
}
|
|
|
|
void bind_handler(pollfd FD, fextl::move_only_function<post_callback(error)> Callback) {
|
|
[[maybe_unused]] auto Previous = std::exchange(callbacks[FD.fd], std::move(Callback));
|
|
assert(!Previous && "May not queue multiple async operations");
|
|
|
|
// Add the FD to the poll list if it's not already contained
|
|
if (CurrentFD != FD.fd && PollFDs.end() == std::find_if(PollFDs.begin(), PollFDs.end(), [&](auto& Prev) { return FD.fd == Prev.fd; })) {
|
|
QueuedEvents.push_back(Event {.FD = FD, .Insert = true});
|
|
}
|
|
}
|
|
|
|
private:
|
|
timespec to_timespec(std::chrono::nanoseconds Duration) {
|
|
timespec Timespec {};
|
|
auto Seconds = std::chrono::duration_cast<std::chrono::seconds>(Duration);
|
|
Timespec.tv_sec = Seconds.count();
|
|
Timespec.tv_nsec = std::chrono::duration_cast<std::chrono::nanoseconds>(Duration - Seconds).count();
|
|
return Timespec;
|
|
}
|
|
|
|
void update_fd_list() {
|
|
for (auto& Event : QueuedEvents) {
|
|
if (Event.Erase) {
|
|
std::iter_swap(std::find_if(PollFDs.begin(), PollFDs.end(), [&](auto& FD) { return FD.fd == Event.FD.fd; }), std::prev(PollFDs.end()));
|
|
PollFDs.pop_back();
|
|
callbacks.erase(Event.FD.fd);
|
|
}
|
|
|
|
if (Event.Insert) {
|
|
PollFDs.push_back(Event.FD);
|
|
}
|
|
}
|
|
QueuedEvents.clear();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Corresponds to asio::mutable_buffer.
|
|
*/
|
|
struct mutable_buffer {
|
|
std::span<std::byte> Data;
|
|
mutable_buffer* Next = nullptr;
|
|
|
|
// Optional FD to send/receive via ancillary buffer.
|
|
// This may only be used with non-empty data, and there may only be up to one FD per buffer chain
|
|
std::optional<int*> FD;
|
|
|
|
size_t size() const {
|
|
size_t Ret = 0;
|
|
const mutable_buffer* Current = this;
|
|
do {
|
|
Ret += Current->Data.size_bytes();
|
|
Current = Current->Next;
|
|
} while (Current);
|
|
|
|
if (Ret == 0) {
|
|
assert(!FD);
|
|
}
|
|
return Ret;
|
|
}
|
|
|
|
int consume_fd() {
|
|
assert(FD);
|
|
return **std::exchange(FD, std::nullopt);
|
|
}
|
|
|
|
mutable_buffer& operator+=(size_t NumBytes) {
|
|
mutable_buffer* Current = this;
|
|
while (Current->Next && NumBytes >= Current->Data.size_bytes()) {
|
|
NumBytes -= Data.size_bytes();
|
|
Current = Current->Next;
|
|
assert(Current->FD == std::nullopt);
|
|
}
|
|
auto FD = std::exchange(this->FD, std::nullopt);
|
|
*this = *Current;
|
|
Data = Data.subspan(std::min(Data.size_bytes(), NumBytes));
|
|
this->FD = FD;
|
|
return *Current;
|
|
}
|
|
|
|
size_t count_chunks() const {
|
|
size_t Ret = 1;
|
|
const mutable_buffer* Current = this;
|
|
while (Current->Next) {
|
|
Current = Current->Next;
|
|
++Ret;
|
|
}
|
|
return Ret;
|
|
}
|
|
};
|
|
|
|
inline mutable_buffer Chained(std::span<mutable_buffer> Buffers) {
|
|
for (size_t i = 0; i + 1 < Buffers.size(); ++i) {
|
|
Buffers[i].Next = &Buffers[i + 1];
|
|
}
|
|
return Buffers[0];
|
|
}
|
|
|
|
/**
|
|
* Corresponds to asio::dynamic_vector_buffer.
|
|
*/
|
|
struct dynamic_vector_buffer {
|
|
fextl::vector<std::byte>& Data;
|
|
|
|
// Maximum number of bytes to grow to
|
|
size_t max_size = Data.capacity();
|
|
};
|
|
|
|
/**
|
|
* Asynchronously reads data from the given stream until MatchPredicate reports a match. The read
|
|
* is queued to the stream's reactor and will progress whenever data is available.
|
|
*
|
|
* MatchPredicate must have the signature pair<Iter, bool>(Iter, Iter):
|
|
* - The input iterators provide the range of new data bytes
|
|
* - The returned boolean indicates if a match was found
|
|
* - The returned iterator is the match location or the location at which to continue testing after the next read
|
|
*
|
|
* The read data will be appended to Buffers. Data past the match returned from the last read data will also be included.
|
|
*
|
|
* Corresponds to asio::async_read_until.
|
|
*/
|
|
template<typename AsyncReadStream, typename MatchPredicate, typename OnComplete>
|
|
requires std::is_invocable_r_v<void, OnComplete, error, size_t>
|
|
void async_read_until(AsyncReadStream& Stream, dynamic_vector_buffer Buffers, MatchPredicate Predicate, OnComplete UserCallback) {
|
|
struct Callback {
|
|
size_t BeginPos;
|
|
size_t EndPos;
|
|
AsyncReadStream& Stream;
|
|
dynamic_vector_buffer Buffers;
|
|
MatchPredicate Predicate;
|
|
OnComplete UserCallback;
|
|
|
|
void operator()(error Err, size_t BytesRead, std::optional<int> FD) {
|
|
if (Err != error::success) {
|
|
UserCallback(Err, 0);
|
|
return;
|
|
}
|
|
|
|
// Start with the predicate check to avoid fetching data unnecessarily
|
|
EndPos += BytesRead;
|
|
if (EndPos != BeginPos) {
|
|
auto Begin = Buffers.Data.begin() + BeginPos;
|
|
auto End = Buffers.Data.begin() + EndPos;
|
|
auto [It, Found] = Predicate(Begin, End);
|
|
BeginPos = It - Buffers.Data.begin();
|
|
if (Found) {
|
|
Buffers.Data.resize(EndPos); // Shrink down to size of data actually received
|
|
UserCallback(error::success, BeginPos);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fill the entire remaining capacity, or resize for a minimum of 512 bytes
|
|
auto BytesToRead = std::max<size_t>(std::min(Buffers.Data.capacity(), Buffers.max_size) - EndPos, 512);
|
|
if (Buffers.Data.size() + BytesToRead > Buffers.max_size) {
|
|
ERROR_AND_DIE_FMT("Out of buffer space");
|
|
}
|
|
|
|
Buffers.Data.resize(EndPos + BytesToRead);
|
|
|
|
// Queue data read.
|
|
// On completion, Reader will check if enough data was received and will queue more reads if needed.
|
|
Stream.async_read_some(mutable_buffer {std::span {Buffers.Data}}, *this);
|
|
}
|
|
};
|
|
|
|
// Check existing data for a predicate match, then initiate async reading if necessary
|
|
Callback {0, Buffers.Data.size(), Stream, Buffers, std::move(Predicate), std::move(UserCallback)}(error::success, 0, std::nullopt);
|
|
}
|
|
|
|
using read_callback = fextl::move_only_function<void(error, size_t, std::optional<int>)>;
|
|
|
|
/**
|
|
* Synchronously reads fixed-length data from the given Stream.
|
|
*
|
|
* The length is inferred from the size of the output buffer(s).
|
|
*
|
|
* Corresponds to asio::read.
|
|
*/
|
|
template<typename AsyncReadStream>
|
|
std::size_t read(AsyncReadStream& Stream, mutable_buffer Buffers, error& ec) {
|
|
size_t TotalBytesRead = 0;
|
|
while (Buffers.size() != 0 || Buffers.FD) {
|
|
auto BytesRead = Stream.read_some(Buffers, ec);
|
|
TotalBytesRead += BytesRead;
|
|
if (Buffers.FD) {
|
|
LOGMAN_THROW_A_FMT(**Buffers.FD != -1, "Receiver requested a file descriptor but none was sent");
|
|
(void)Buffers.consume_fd();
|
|
}
|
|
Buffers += BytesRead;
|
|
if (ec != error::success) {
|
|
return TotalBytesRead;
|
|
}
|
|
}
|
|
ec = error::success;
|
|
return TotalBytesRead;
|
|
}
|
|
|
|
/**
|
|
* Synchronously writes fixed-length data to the given Stream.
|
|
*
|
|
* The length is inferred from the size of the input buffer(s).
|
|
*
|
|
* Corresponds to asio::write.
|
|
*/
|
|
template<typename AsyncReadStream>
|
|
std::size_t write(AsyncReadStream& Stream, mutable_buffer Buffers, error& ec) {
|
|
size_t TotalBytesWritten = 0;
|
|
while (Buffers.size() != 0 || Buffers.FD) {
|
|
auto BytesWritten = Stream.write_some(Buffers, ec);
|
|
TotalBytesWritten += BytesWritten;
|
|
if (Buffers.FD) {
|
|
(void)Buffers.consume_fd();
|
|
}
|
|
Buffers += BytesWritten;
|
|
if (ec != error::success) {
|
|
return TotalBytesWritten;
|
|
}
|
|
}
|
|
ec = error::success;
|
|
return TotalBytesWritten;
|
|
}
|
|
|
|
/**
|
|
* Owning RAII wrapper around a file descriptor.
|
|
*
|
|
* Corresponds to asio::posix::descriptor.
|
|
*/
|
|
struct posix_descriptor {
|
|
poll_reactor* Reactor = nullptr;
|
|
int FD = -1;
|
|
|
|
posix_descriptor(poll_reactor& Reactor, int FD)
|
|
: Reactor(&Reactor)
|
|
, FD(FD) {}
|
|
|
|
posix_descriptor(posix_descriptor&& Other)
|
|
: Reactor(Other.Reactor)
|
|
, FD(std::exchange(Other.FD, -1)) {}
|
|
|
|
posix_descriptor& operator=(posix_descriptor&& Other) {
|
|
if (&Other == this) {
|
|
return *this;
|
|
}
|
|
posix_descriptor::~posix_descriptor();
|
|
Reactor = Other.Reactor;
|
|
FD = std::exchange(Other.FD, -1);
|
|
return *this;
|
|
}
|
|
|
|
~posix_descriptor() {
|
|
if (FD != -1) {
|
|
::close(FD);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait until there is data available to read on this object, then execute the given callback
|
|
*/
|
|
template<typename Fn>
|
|
requires std::is_invocable_r_v<post_callback, Fn, error>
|
|
void async_wait(Fn Callback) {
|
|
Reactor->bind_handler(
|
|
pollfd {
|
|
.fd = FD,
|
|
.events = POLLIN,
|
|
.revents = 0,
|
|
},
|
|
std::move(Callback));
|
|
}
|
|
};
|
|
|
|
} // namespace fasio
|