Mods: NetService and WebSocketService (#2385)

This commit is contained in:
Luke Street
2026-09-04 00:33:31 -06:00
committed by GitHub
parent fa70bb8fe8
commit 3efba4ccbd
33 changed files with 1979 additions and 324 deletions
+145
View File
@@ -0,0 +1,145 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define NET_SERVICE_ID "dev.twilitrealm.dusklight.net"
#define NET_SERVICE_MAJOR 1u
#define NET_SERVICE_MINOR 0u
/** 0 is never a valid handle. */
typedef uint64_t NetHandle;
typedef enum NetError {
NET_ERROR_NONE = 0,
NET_ERROR_INVALID_ENDPOINT = 1,
NET_ERROR_RESOLVE = 2,
NET_ERROR_TIMEOUT = 3,
NET_ERROR_REFUSED = 4,
NET_ERROR_UNREACHABLE = 5,
NET_ERROR_RESET = 6,
NET_ERROR_ADDRESS_IN_USE = 7,
NET_ERROR_PERMISSION = 8,
NET_ERROR_TOO_LARGE = 9,
NET_ERROR_CANCELED = 10,
NET_ERROR_NETWORK = 11,
} NetError;
#define NET_ENDPOINT_MAX 80
/** NUL-terminated tcp://host:port or udp://host:port endpoint. */
typedef struct NetEndpoint {
char text[NET_ENDPOINT_MAX];
} NetEndpoint;
typedef struct NetConnectDesc {
uint32_t struct_size;
/** TCP endpoint. Hostnames and IP literals are accepted. */
const char* endpoint;
/** 0 defaults to 10 seconds. Resolution is included. */
uint32_t connect_timeout_ms;
/** 0 defaults to 5 seconds for flush and peer EOF. */
uint32_t close_timeout_ms;
/** 0 defaults to 1 MiB. Maximum of 8 MiB. */
size_t max_send_queue_bytes;
bool no_delay;
/** Sampled when each event is polled. */
void* user_data;
} NetConnectDesc;
#define NET_CONNECT_DESC_INIT {sizeof(NetConnectDesc), NULL, 0u, 0u, 0u, true, NULL}
typedef struct NetListenDesc {
uint32_t struct_size;
/** TCP endpoint with an IP literal. Port 0 requests an ephemeral port. */
const char* bind;
uint32_t close_timeout_ms;
size_t max_send_queue_bytes;
bool no_delay;
void* user_data;
} NetListenDesc;
#define NET_LISTEN_DESC_INIT {sizeof(NetListenDesc), NULL, 0u, 0u, true, NULL}
typedef struct NetDatagramDesc {
uint32_t struct_size;
/** UDP endpoint with an IP literal. Port 0 requests an ephemeral port. */
const char* bind;
size_t max_send_queue_bytes;
void* user_data;
} NetDatagramDesc;
#define NET_DATAGRAM_DESC_INIT {sizeof(NetDatagramDesc), NULL, 0u, NULL}
typedef enum NetEventType {
NET_EVENT_NONE = 0,
NET_EVENT_CONNECTED = 1,
NET_EVENT_ACCEPTED = 2,
NET_EVENT_STREAM_DATA = 3,
NET_EVENT_DATAGRAM = 4,
NET_EVENT_DROPPED = 5,
NET_EVENT_RESOLVED = 6,
NET_EVENT_CLOSED = 7,
} NetEventType;
typedef struct NetEvent {
uint32_t struct_size;
NetEventType type;
/** Source handle. A CLOSED or RESOLVED handle is invalid after poll_event returns it. */
NetHandle handle;
void* user_data;
NetHandle accepted;
/** Peer, datagram source, or resolved endpoint as applicable. */
NetEndpoint endpoint;
/** Valid until this mod's next poll_event call or deactivation. */
const void* data;
size_t size;
uint32_t dropped;
NetError error;
/** Never NULL. Valid until this mod's next poll_event call or deactivation. */
const char* error_message;
} NetEvent;
#define NET_EVENT_INIT \
{sizeof(NetEvent), NET_EVENT_NONE, 0u, NULL, 0u, {{0}}, NULL, 0u, 0u, NET_ERROR_NONE, ""}
typedef struct NetStats {
uint32_t struct_size;
size_t queued_send_bytes;
uint64_t inbound_dropped;
uint64_t send_failures;
uint64_t bytes_sent;
uint64_t bytes_received;
} NetStats;
#define NET_STATS_INIT {sizeof(NetStats), 0u, 0u, 0u, 0u, 0u}
typedef struct NetService {
ServiceHeader header;
/** Starts an asynchronous TCP connection. */
ModResult (*connect)(ModContext* ctx, const NetConnectDesc* desc, NetHandle* out_handle);
/** Opens a TCP listener and returns its local endpoint. */
ModResult (*listen)(ModContext* ctx, const NetListenDesc* desc, NetHandle* out_handle,
NetEndpoint* out_local, NetError* out_error);
/** Opens a UDP socket and returns its local endpoint. */
ModResult (*open_datagram)(ModContext* ctx, const NetDatagramDesc* desc, NetHandle* out_handle,
NetEndpoint* out_local, NetError* out_error);
/** Resolves a TCP or UDP endpoint asynchronously. */
ModResult (*resolve)(
ModContext* ctx, const char* endpoint, void* user_data, NetHandle* out_handle);
/** Returns MOD_OK and NET_EVENT_NONE when the queue is empty. */
ModResult (*poll_event)(ModContext* ctx, NetEvent* out_event);
/** Copies bytes to a connected stream's outbound queue. */
ModResult (*send)(ModContext* ctx, NetHandle stream, const void* data, size_t size);
/** Copies one datagram for a literal UDP destination. The maximum size is 65,507 bytes. */
ModResult (*send_to)(
ModContext* ctx, NetHandle socket, const char* endpoint, const void* data, size_t size);
ModResult (*set_user_data)(ModContext* ctx, NetHandle handle, void* user_data);
ModResult (*stats)(ModContext* ctx, NetHandle handle, NetStats* out_stats);
ModResult (*close)(ModContext* ctx, NetHandle handle);
} NetService;
MOD_DECLARE_SERVICE(NetService, svc_net, NET_SERVICE_ID, NET_SERVICE_MAJOR, NET_SERVICE_MINOR);
+189
View File
@@ -0,0 +1,189 @@
#pragma once
#include <mods/svc/net.h>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <utility>
namespace mods::net {
class Socket {
public:
Socket() = default;
Socket(NetHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
~Socket() { reset(); }
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
Socket(Socket&& other) noexcept { *this = std::move(other); }
Socket& operator=(Socket&& other) noexcept {
if (this != &other) {
reset();
mHandle = std::exchange(other.mHandle, 0);
mResult = other.mResult;
}
return *this;
}
explicit operator bool() const { return mResult == MOD_OK && mHandle != 0; }
ModResult result() const { return mResult; }
NetHandle handle() const { return mHandle; }
ModResult send(std::span<const std::byte> bytes) const {
return svc_net != nullptr && mHandle != 0 ?
svc_net->send(mod_ctx, mHandle, bytes.data(), bytes.size()) :
MOD_UNAVAILABLE;
}
ModResult send_to(std::string_view endpoint, std::span<const std::byte> bytes) const {
if (svc_net == nullptr || mHandle == 0) {
return MOD_UNAVAILABLE;
}
const std::string endpointText{endpoint};
return svc_net->send_to(mod_ctx, mHandle, endpointText.c_str(), bytes.data(), bytes.size());
}
std::optional<NetStats> stats() const {
if (svc_net == nullptr || mHandle == 0) {
return std::nullopt;
}
NetStats value = NET_STATS_INIT;
if (svc_net->stats(mod_ctx, mHandle, &value) != MOD_OK) {
return std::nullopt;
}
return value;
}
ModResult set_user_data(void* userData) const {
return svc_net != nullptr && mHandle != 0 ?
svc_net->set_user_data(mod_ctx, mHandle, userData) :
MOD_UNAVAILABLE;
}
ModResult close() {
if (mHandle == 0) {
return mResult == MOD_OK ? MOD_OK : MOD_UNAVAILABLE;
}
mResult = svc_net != nullptr ? svc_net->close(mod_ctx, mHandle) : MOD_UNAVAILABLE;
mHandle = 0;
return mResult;
}
void detach() { mHandle = 0; }
private:
void reset() { (void)close(); }
NetHandle mHandle = 0;
ModResult mResult = MOD_UNAVAILABLE;
};
struct BindOutcome {
std::string local;
NetError error = NET_ERROR_NONE;
};
inline Socket connect(std::string_view endpoint, NetConnectDesc options = NET_CONNECT_DESC_INIT) {
if (svc_net == nullptr) {
return {0, MOD_UNAVAILABLE};
}
const std::string endpointText{endpoint};
options.struct_size = sizeof(options);
options.endpoint = endpointText.c_str();
NetHandle handle = 0;
const ModResult result = svc_net->connect(mod_ctx, &options, &handle);
return {handle, result};
}
inline Socket listen(std::string_view bind, BindOutcome* out = nullptr,
NetListenDesc options = NET_LISTEN_DESC_INIT) {
if (svc_net == nullptr) {
return {0, MOD_UNAVAILABLE};
}
const std::string bindText{bind};
options.struct_size = sizeof(options);
options.bind = bindText.c_str();
NetHandle handle = 0;
NetEndpoint local{};
NetError error = NET_ERROR_NONE;
const ModResult result = svc_net->listen(mod_ctx, &options, &handle, &local, &error);
if (out != nullptr) {
*out = {.local = local.text, .error = error};
}
return {handle, result};
}
inline Socket open_datagram(std::string_view bind, BindOutcome* out = nullptr,
NetDatagramDesc options = NET_DATAGRAM_DESC_INIT) {
if (svc_net == nullptr) {
return {0, MOD_UNAVAILABLE};
}
const std::string bindText{bind};
options.struct_size = sizeof(options);
options.bind = bindText.c_str();
NetHandle handle = 0;
NetEndpoint local{};
NetError error = NET_ERROR_NONE;
const ModResult result = svc_net->open_datagram(mod_ctx, &options, &handle, &local, &error);
if (out != nullptr) {
*out = {.local = local.text, .error = error};
}
return {handle, result};
}
inline Socket resolve(std::string_view endpoint, void* userData = nullptr) {
if (svc_net == nullptr) {
return {0, MOD_UNAVAILABLE};
}
const std::string endpointText{endpoint};
NetHandle handle = 0;
const ModResult result = svc_net->resolve(mod_ctx, endpointText.c_str(), userData, &handle);
return {handle, result};
}
inline Socket adopt(NetHandle accepted) {
return {accepted, accepted != 0 ? MOD_OK : MOD_INVALID_ARGUMENT};
}
struct Event {
NetEventType type = NET_EVENT_NONE;
NetHandle handle = 0;
void* userData = nullptr;
NetHandle accepted = 0;
std::string_view endpoint;
std::span<const std::byte> data;
uint32_t dropped = 0;
NetError error = NET_ERROR_NONE;
std::string_view message;
};
inline bool poll(Event& out) {
out = {};
if (svc_net == nullptr) {
return false;
}
NetEvent raw = NET_EVENT_INIT;
if (svc_net->poll_event(mod_ctx, &raw) != MOD_OK || raw.type == NET_EVENT_NONE) {
return false;
}
out.type = raw.type;
out.handle = raw.handle;
out.userData = raw.user_data;
out.accepted = raw.accepted;
out.endpoint = raw.endpoint.text;
if (raw.data != nullptr && raw.size != 0) {
out.data = {static_cast<const std::byte*>(raw.data), raw.size};
}
out.dropped = raw.dropped;
out.error = raw.error;
out.message = raw.error_message != nullptr ? raw.error_message : "";
return true;
}
} // namespace mods::net
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include <mods/api.h>
#include <mods/svc/http.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#endif
#define WEBSOCKET_SERVICE_ID "dev.twilitrealm.dusklight.websocket"
#define WEBSOCKET_SERVICE_MAJOR 1u
#define WEBSOCKET_SERVICE_MINOR 0u
/** Generational connection handle. Zero is never valid. */
typedef uint64_t WebSocketHandle;
/** Connection outcome. Callers must tolerate values added by later service minors. */
typedef enum WebSocketError {
WEBSOCKET_ERROR_NONE = 0,
WEBSOCKET_ERROR_INVALID_URL = 1,
WEBSOCKET_ERROR_UNSUPPORTED_SCHEME = 2,
WEBSOCKET_ERROR_TIMEOUT = 3,
WEBSOCKET_ERROR_TOO_LARGE = 4,
WEBSOCKET_ERROR_CANCELED = 5,
WEBSOCKET_ERROR_NETWORK = 6,
WEBSOCKET_ERROR_PROTOCOL = 7,
WEBSOCKET_ERROR_HANDSHAKE = 8,
} WebSocketError;
typedef enum WebSocketMessageKind {
WEBSOCKET_MESSAGE_TEXT = 0,
WEBSOCKET_MESSAGE_BINARY = 1,
} WebSocketMessageKind;
typedef struct WebSocketConnectDesc {
uint32_t struct_size;
/** wss:// URL, or ws:// for localhost, 127.0.0.1, or [::1]. */
const char* url;
/** Request headers. WebSocket handshake headers and User-Agent are reserved. */
const HttpHeader* headers;
uint32_t header_count;
const char* const* protocols;
uint32_t protocol_count;
uint32_t connect_timeout_ms;
uint32_t close_timeout_ms;
uint32_t keepalive_interval_ms;
/** 0 defaults to 1 MiB. Maximum of 16 MiB. */
size_t max_message_bytes;
/** Passed in every event. */
void* user_data;
} WebSocketConnectDesc;
#define WEBSOCKET_CONNECT_DESC_INIT \
{sizeof(WebSocketConnectDesc), NULL, NULL, 0u, NULL, 0u, 0u, 0u, 0u, 0u, NULL}
typedef enum WebSocketEventType {
WEBSOCKET_EVENT_NONE = 0,
WEBSOCKET_EVENT_OPEN = 1,
WEBSOCKET_EVENT_MESSAGE = 2,
WEBSOCKET_EVENT_CLOSED = 3,
} WebSocketEventType;
typedef struct WebSocketEvent {
uint32_t struct_size;
WebSocketEventType type;
WebSocketHandle ws;
void* user_data;
const char* protocol;
/** Handshake response headers for OPEN or a handshake-rejected CLOSED event. */
const HttpHeader* headers;
uint32_t header_count;
WebSocketMessageKind message_kind;
/** Valid until this mod's next poll_event call or deactivation. */
const void* data;
size_t size;
WebSocketError error;
/** Never NULL. Valid until this mod's next poll_event call or deactivation. */
const char* error_message;
int32_t handshake_status;
uint16_t close_code;
const char* close_reason;
} WebSocketEvent;
#define WEBSOCKET_EVENT_INIT \
{sizeof(WebSocketEvent), WEBSOCKET_EVENT_NONE, 0u, NULL, "", NULL, 0u, WEBSOCKET_MESSAGE_TEXT, \
NULL, 0u, WEBSOCKET_ERROR_NONE, "", 0, 0u, ""}
typedef struct WebSocketService {
ServiceHeader header;
/** Starts a connection. */
ModResult (*connect)(
ModContext* ctx, const WebSocketConnectDesc* desc, WebSocketHandle* out_handle);
/** Returns MOD_OK and WEBSOCKET_EVENT_NONE when the queue is empty. */
ModResult (*poll_event)(ModContext* ctx, WebSocketEvent* out_event);
/** Copies a message into the outbound queue. */
ModResult (*send)(ModContext* ctx, WebSocketHandle ws, WebSocketMessageKind kind,
const void* data, size_t size);
/** Code 0 defaults to 1000; accepted: 1000, 1001, and 3000-4999. */
ModResult (*close)(ModContext* ctx, WebSocketHandle ws, uint16_t code, const char* reason);
} WebSocketService;
MOD_DECLARE_SERVICE(WebSocketService, svc_websocket, WEBSOCKET_SERVICE_ID, WEBSOCKET_SERVICE_MAJOR,
WEBSOCKET_SERVICE_MINOR);
+172
View File
@@ -0,0 +1,172 @@
#pragma once
#include <mods/svc/http.hpp>
#include <mods/svc/websocket.h>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace mods::ws {
struct Options {
std::string url;
std::vector<http::Header> headers;
std::vector<std::string> protocols;
uint32_t connectTimeoutMs = 0;
uint32_t closeTimeoutMs = 0;
uint32_t keepaliveIntervalMs = 0;
size_t maxMessageBytes = 0;
void* userData = nullptr;
};
class Connection {
public:
Connection() = default;
Connection(WebSocketHandle handle, ModResult result) : mHandle{handle}, mResult{result} {}
~Connection() { reset(); }
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete;
Connection(Connection&& other) noexcept { *this = std::move(other); }
Connection& operator=(Connection&& other) noexcept {
if (this != &other) {
reset();
mHandle = std::exchange(other.mHandle, 0);
mResult = other.mResult;
}
return *this;
}
explicit operator bool() const { return mResult == MOD_OK && mHandle != 0; }
ModResult result() const { return mResult; }
WebSocketHandle handle() const { return mHandle; }
ModResult send(WebSocketMessageKind kind, std::span<const std::byte> bytes) const {
return svc_websocket != nullptr && mHandle != 0 ?
svc_websocket->send(mod_ctx, mHandle, kind, bytes.data(), bytes.size()) :
MOD_UNAVAILABLE;
}
ModResult send_text(std::string_view text) const {
return svc_websocket != nullptr && mHandle != 0 ?
svc_websocket->send(
mod_ctx, mHandle, WEBSOCKET_MESSAGE_TEXT, text.data(), text.size()) :
MOD_UNAVAILABLE;
}
ModResult send_binary(std::span<const std::byte> bytes) const {
return send(WEBSOCKET_MESSAGE_BINARY, bytes);
}
ModResult close(uint16_t code = 1000, std::string_view reason = {}) {
if (svc_websocket == nullptr || mHandle == 0) {
return MOD_UNAVAILABLE;
}
const std::string reasonText{reason};
mResult = svc_websocket->close(mod_ctx, mHandle, code, reasonText.c_str());
if (mResult == MOD_OK) {
mHandle = 0;
}
return mResult;
}
void detach() { mHandle = 0; }
private:
void reset() {
if (mHandle != 0) {
(void)close(1001, "Connection owner released");
mHandle = 0;
}
}
WebSocketHandle mHandle = 0;
ModResult mResult = MOD_UNAVAILABLE;
};
inline Connection connect(const Options& options) {
if (svc_websocket == nullptr || options.headers.size() > std::numeric_limits<uint32_t>::max() ||
options.protocols.size() > std::numeric_limits<uint32_t>::max())
{
return {0, svc_websocket == nullptr ? MOD_UNAVAILABLE : MOD_INVALID_ARGUMENT};
}
std::vector<HttpHeader> headers;
headers.reserve(options.headers.size());
for (const auto& header : options.headers) {
headers.push_back({.name = header.name.c_str(), .value = header.value.c_str()});
}
std::vector<const char*> protocols;
protocols.reserve(options.protocols.size());
for (const auto& protocol : options.protocols) {
protocols.push_back(protocol.c_str());
}
WebSocketConnectDesc desc = WEBSOCKET_CONNECT_DESC_INIT;
desc.url = options.url.c_str();
desc.headers = headers.empty() ? nullptr : headers.data();
desc.header_count = static_cast<uint32_t>(headers.size());
desc.protocols = protocols.empty() ? nullptr : protocols.data();
desc.protocol_count = static_cast<uint32_t>(protocols.size());
desc.connect_timeout_ms = options.connectTimeoutMs;
desc.close_timeout_ms = options.closeTimeoutMs;
desc.keepalive_interval_ms = options.keepaliveIntervalMs;
desc.max_message_bytes = options.maxMessageBytes;
desc.user_data = options.userData;
WebSocketHandle handle = 0;
const ModResult result = svc_websocket->connect(mod_ctx, &desc, &handle);
return {handle, result};
}
struct Event {
WebSocketEventType type = WEBSOCKET_EVENT_NONE;
WebSocketHandle handle = 0;
void* userData = nullptr;
std::string_view protocol;
std::span<const HttpHeader> headers;
WebSocketMessageKind messageKind = WEBSOCKET_MESSAGE_TEXT;
std::span<const std::byte> data;
WebSocketError error = WEBSOCKET_ERROR_NONE;
std::string_view message;
int handshakeStatus = 0;
uint16_t closeCode = 0;
std::string_view closeReason;
};
inline bool poll(Event& out) {
out = {};
if (svc_websocket == nullptr) {
return false;
}
WebSocketEvent raw = WEBSOCKET_EVENT_INIT;
if (svc_websocket->poll_event(mod_ctx, &raw) != MOD_OK || raw.type == WEBSOCKET_EVENT_NONE) {
return false;
}
out.type = raw.type;
out.handle = raw.ws;
out.userData = raw.user_data;
out.protocol = raw.protocol != nullptr ? raw.protocol : "";
if (raw.headers != nullptr && raw.header_count != 0) {
out.headers = {raw.headers, raw.header_count};
}
out.messageKind = raw.message_kind;
if (raw.data != nullptr && raw.size != 0) {
out.data = {static_cast<const std::byte*>(raw.data), raw.size};
}
out.error = raw.error;
out.message = raw.error_message != nullptr ? raw.error_message : "";
out.handshakeStatus = raw.handshake_status;
out.closeCode = raw.close_code;
out.closeReason = raw.close_reason != nullptr ? raw.close_reason : "";
return true;
}
} // namespace mods::ws