443 lines
18 KiB
C++
443 lines
18 KiB
C++
|
|
/*
|
||
|
|
Crafter®.Network
|
||
|
|
Copyright (C) 2026 Catcrafts®
|
||
|
|
Catcrafts.net
|
||
|
|
|
||
|
|
This library is free software; you can redistribute it and/or
|
||
|
|
modify it under the terms of the GNU Lesser General Public
|
||
|
|
License as published by the Free Software Foundation; either
|
||
|
|
version 3.0 of the License, or (at your option) any later version.
|
||
|
|
|
||
|
|
This library is distributed in the hope that it will be useful,
|
||
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
|
|
Lesser General Public License for more details.
|
||
|
|
|
||
|
|
You should have received a copy of the GNU Lesser General Public
|
||
|
|
License along with this library; if not, write to the Free Software
|
||
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||
|
|
*/
|
||
|
|
|
||
|
|
// CRAFTER_NETWORK_BROWSER implementation of ClientQUIC / QUICStream backed
|
||
|
|
// by the browser's WebTransport API. WebTransport is HTTP/3-based and
|
||
|
|
// expects a URL — we use https://${host}:${port}/${alpn}. Native msquic
|
||
|
|
// is not linked into the browser build.
|
||
|
|
//
|
||
|
|
// Async-only: synchronous send/receive methods on QUICStream are not
|
||
|
|
// compiled (gated out in the interface). Everything goes through the
|
||
|
|
// existing *Async / OnStream / OnDatagram callbacks. The JS bridge
|
||
|
|
// (additional/network-env.js) runs the WebTransport reader loops and
|
||
|
|
// dispatches each chunk back through the wasm exports declared below.
|
||
|
|
|
||
|
|
module;
|
||
|
|
module Crafter.Network:ClientQUIC_impl;
|
||
|
|
import :ClientQUIC;
|
||
|
|
import std;
|
||
|
|
|
||
|
|
using namespace Crafter;
|
||
|
|
|
||
|
|
namespace Crafter::NetworkBrowserBindings {
|
||
|
|
__attribute__((import_module("env"), import_name("crafterNetworkWtConnect")))
|
||
|
|
std::int32_t crafterNetworkWtConnect(
|
||
|
|
const char* host, std::int32_t hostLen,
|
||
|
|
std::int32_t port,
|
||
|
|
const char* alpn, std::int32_t alpnLen,
|
||
|
|
const std::uint8_t* certHash, std::int32_t certHashLen);
|
||
|
|
|
||
|
|
__attribute__((import_module("env"), import_name("crafterNetworkWtClose")))
|
||
|
|
void crafterNetworkWtClose(std::int32_t handle);
|
||
|
|
|
||
|
|
__attribute__((import_module("env"), import_name("crafterNetworkWtOpenStream")))
|
||
|
|
std::int32_t crafterNetworkWtOpenStream(std::int32_t handle, std::int32_t unidirectional);
|
||
|
|
|
||
|
|
__attribute__((import_module("env"), import_name("crafterNetworkWtStreamWrite")))
|
||
|
|
void crafterNetworkWtStreamWrite(std::int32_t streamId,
|
||
|
|
const char* buf, std::int32_t bufLen,
|
||
|
|
std::int32_t finish,
|
||
|
|
std::int32_t callbackId);
|
||
|
|
|
||
|
|
__attribute__((import_module("env"), import_name("crafterNetworkWtStreamStop")))
|
||
|
|
void crafterNetworkWtStreamStop(std::int32_t streamId);
|
||
|
|
|
||
|
|
__attribute__((import_module("env"), import_name("crafterNetworkWtSendDatagram")))
|
||
|
|
void crafterNetworkWtSendDatagram(std::int32_t handle,
|
||
|
|
const char* buf, std::int32_t bufLen);
|
||
|
|
}
|
||
|
|
|
||
|
|
namespace Crafter::NetworkBrowser {
|
||
|
|
// ─── Receive state machine per stream ─────────────────────────────────
|
||
|
|
//
|
||
|
|
// The JS reader loop pushes every chunk it sees through
|
||
|
|
// CrafterNetworkOnWtStreamChunk. We buffer them until the user calls
|
||
|
|
// one of the Receive*Async variants — at which point we either
|
||
|
|
// dispatch immediately (chunks already queued) or wait for the next
|
||
|
|
// chunk to arrive. A FIN signal marks the end of the peer's send-
|
||
|
|
// side; further chunks after FIN do not arrive.
|
||
|
|
//
|
||
|
|
// StreamState / ConnectionState live in a named namespace (not
|
||
|
|
// anonymous) because they are referenced from the QUICStream::Impl
|
||
|
|
// and ClientQUIC::Impl definitions below — private nested types of
|
||
|
|
// exported classes can't have TU-local member types without a
|
||
|
|
// diagnostic.
|
||
|
|
|
||
|
|
enum class RecvMode { None, Once, UntilClose, UntilFull };
|
||
|
|
|
||
|
|
struct StreamState {
|
||
|
|
// 0 means destroyed / closed.
|
||
|
|
std::int32_t handle = 0;
|
||
|
|
|
||
|
|
// Buffered chunks not yet delivered to a pending callback.
|
||
|
|
std::vector<char> buffer;
|
||
|
|
bool finReceived = false;
|
||
|
|
bool closed = false;
|
||
|
|
|
||
|
|
// Pending one-shot receive.
|
||
|
|
RecvMode mode = RecvMode::None;
|
||
|
|
std::uint32_t target = 0;
|
||
|
|
std::function<void(std::vector<char>)> cb;
|
||
|
|
|
||
|
|
ClientQUIC* connection = nullptr;
|
||
|
|
};
|
||
|
|
|
||
|
|
struct ConnectionState {
|
||
|
|
std::int32_t handle = 0;
|
||
|
|
bool ready = false;
|
||
|
|
bool closed = false;
|
||
|
|
std::function<void(QUICStream)> onStream;
|
||
|
|
std::function<void(std::vector<char>)> onDatagram;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Handle → state. Allocated on the heap so the pointer is stable
|
||
|
|
// across QUICStream / ClientQUIC moves (each holds a unique_ptr<Impl>
|
||
|
|
// that wraps a pointer into these maps via its handle).
|
||
|
|
inline std::unordered_map<std::int32_t, StreamState*>& Streams() {
|
||
|
|
static std::unordered_map<std::int32_t, StreamState*> m;
|
||
|
|
return m;
|
||
|
|
}
|
||
|
|
inline std::unordered_map<std::int32_t, ConnectionState*>& Connections() {
|
||
|
|
static std::unordered_map<std::int32_t, ConnectionState*> m;
|
||
|
|
return m;
|
||
|
|
}
|
||
|
|
inline std::unordered_map<std::int32_t, std::function<void()>>& WriteCallbacks() {
|
||
|
|
static std::unordered_map<std::int32_t, std::function<void()>> m;
|
||
|
|
return m;
|
||
|
|
}
|
||
|
|
inline std::int32_t NextWriteCallbackId() {
|
||
|
|
static std::int32_t counter = 0;
|
||
|
|
return ++counter;
|
||
|
|
}
|
||
|
|
|
||
|
|
inline void TryDispatchRecv(StreamState& s) {
|
||
|
|
if (s.mode == RecvMode::None) return;
|
||
|
|
|
||
|
|
if (s.mode == RecvMode::Once) {
|
||
|
|
if (!s.buffer.empty()) {
|
||
|
|
auto chunk = std::move(s.buffer);
|
||
|
|
s.buffer.clear();
|
||
|
|
auto cb = std::move(s.cb);
|
||
|
|
s.mode = RecvMode::None;
|
||
|
|
if (cb) cb(std::move(chunk));
|
||
|
|
} else if (s.finReceived || s.closed) {
|
||
|
|
auto cb = std::move(s.cb);
|
||
|
|
s.mode = RecvMode::None;
|
||
|
|
if (cb) cb({});
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (s.mode == RecvMode::UntilClose) {
|
||
|
|
if (s.finReceived || s.closed) {
|
||
|
|
auto chunk = std::move(s.buffer);
|
||
|
|
s.buffer.clear();
|
||
|
|
auto cb = std::move(s.cb);
|
||
|
|
s.mode = RecvMode::None;
|
||
|
|
if (cb) cb(std::move(chunk));
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (s.mode == RecvMode::UntilFull) {
|
||
|
|
if (s.buffer.size() >= s.target) {
|
||
|
|
std::vector<char> chunk(s.buffer.begin(), s.buffer.begin() + s.target);
|
||
|
|
s.buffer.erase(s.buffer.begin(), s.buffer.begin() + s.target);
|
||
|
|
auto cb = std::move(s.cb);
|
||
|
|
s.mode = RecvMode::None;
|
||
|
|
if (cb) cb(std::move(chunk));
|
||
|
|
} else if (s.finReceived || s.closed) {
|
||
|
|
// Peer closed before we got the requested byte count —
|
||
|
|
// deliver whatever's left. Mirrors the native variant's
|
||
|
|
// "throws if peer closes early" only loosely (we have no
|
||
|
|
// exception channel from JS).
|
||
|
|
auto chunk = std::move(s.buffer);
|
||
|
|
s.buffer.clear();
|
||
|
|
auto cb = std::move(s.cb);
|
||
|
|
s.mode = RecvMode::None;
|
||
|
|
if (cb) cb(std::move(chunk));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// All implementation-private state types live in Crafter::NetworkBrowser
|
||
|
|
// (above). Pull them in unqualified for readability.
|
||
|
|
using Crafter::NetworkBrowser::StreamState;
|
||
|
|
using Crafter::NetworkBrowser::ConnectionState;
|
||
|
|
using Crafter::NetworkBrowser::RecvMode;
|
||
|
|
using Crafter::NetworkBrowser::Streams;
|
||
|
|
using Crafter::NetworkBrowser::Connections;
|
||
|
|
using Crafter::NetworkBrowser::WriteCallbacks;
|
||
|
|
using Crafter::NetworkBrowser::NextWriteCallbackId;
|
||
|
|
using Crafter::NetworkBrowser::TryDispatchRecv;
|
||
|
|
|
||
|
|
// ─── QUICStream::Impl ────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
struct QUICStream::Impl {
|
||
|
|
StreamState state;
|
||
|
|
};
|
||
|
|
|
||
|
|
QUICStream::QUICStream() : impl(std::make_unique<Impl>()) {}
|
||
|
|
|
||
|
|
QUICStream::QUICStream(std::int32_t streamHandle, ClientQUIC* conn,
|
||
|
|
bool canSendArg, bool canReceiveArg)
|
||
|
|
: canSend(canSendArg), canReceive(canReceiveArg),
|
||
|
|
impl(std::make_unique<Impl>())
|
||
|
|
{
|
||
|
|
connection = conn;
|
||
|
|
impl->state.handle = streamHandle;
|
||
|
|
impl->state.connection = conn;
|
||
|
|
Streams()[streamHandle] = &impl->state;
|
||
|
|
}
|
||
|
|
|
||
|
|
QUICStream::QUICStream(QUICStream&&) noexcept = default;
|
||
|
|
QUICStream& QUICStream::operator=(QUICStream&&) noexcept = default;
|
||
|
|
|
||
|
|
QUICStream::~QUICStream() {
|
||
|
|
if (impl && impl->state.handle != 0) {
|
||
|
|
Streams().erase(impl->state.handle);
|
||
|
|
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(impl->state.handle);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void QUICStream::SendAsync(const void* buffer, std::uint32_t size, bool finish,
|
||
|
|
std::function<void()> onSent) {
|
||
|
|
if (!impl || impl->state.handle == 0 || impl->state.closed) {
|
||
|
|
if (onSent) onSent();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
std::int32_t cbId = 0;
|
||
|
|
if (onSent) {
|
||
|
|
cbId = NextWriteCallbackId();
|
||
|
|
WriteCallbacks()[cbId] = std::move(onSent);
|
||
|
|
}
|
||
|
|
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamWrite(
|
||
|
|
impl->state.handle,
|
||
|
|
static_cast<const char*>(buffer),
|
||
|
|
static_cast<std::int32_t>(size),
|
||
|
|
finish ? 1 : 0,
|
||
|
|
cbId);
|
||
|
|
}
|
||
|
|
|
||
|
|
void QUICStream::RecieveAsync(std::function<void(std::vector<char>)> cb) {
|
||
|
|
if (!impl) { if (cb) cb({}); return; }
|
||
|
|
impl->state.mode = RecvMode::Once;
|
||
|
|
impl->state.cb = std::move(cb);
|
||
|
|
TryDispatchRecv(impl->state);
|
||
|
|
}
|
||
|
|
|
||
|
|
void QUICStream::RecieveUntilCloseAsync(std::function<void(std::vector<char>)> cb) {
|
||
|
|
if (!impl) { if (cb) cb({}); return; }
|
||
|
|
impl->state.mode = RecvMode::UntilClose;
|
||
|
|
impl->state.cb = std::move(cb);
|
||
|
|
TryDispatchRecv(impl->state);
|
||
|
|
}
|
||
|
|
|
||
|
|
void QUICStream::RecieveUntilFullAsync(std::uint32_t bufferSize,
|
||
|
|
std::function<void(std::vector<char>)> cb) {
|
||
|
|
if (!impl) { if (cb) cb({}); return; }
|
||
|
|
impl->state.mode = RecvMode::UntilFull;
|
||
|
|
impl->state.target = bufferSize;
|
||
|
|
impl->state.cb = std::move(cb);
|
||
|
|
TryDispatchRecv(impl->state);
|
||
|
|
}
|
||
|
|
|
||
|
|
void QUICStream::Stop() {
|
||
|
|
if (impl && impl->state.handle != 0) {
|
||
|
|
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(impl->state.handle);
|
||
|
|
Streams().erase(impl->state.handle);
|
||
|
|
impl->state.handle = 0;
|
||
|
|
impl->state.closed = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── ClientQUIC::Impl ────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
struct ClientQUIC::Impl {
|
||
|
|
ConnectionState state;
|
||
|
|
};
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
QUICStream MakeStreamFromHandle(std::int32_t streamId, ClientQUIC* conn,
|
||
|
|
bool canSend, bool canReceive) {
|
||
|
|
return QUICStream{streamId, conn, canSend, canReceive};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ClientQUIC::ClientQUIC(const char* host, std::uint16_t port, std::string alpnArg,
|
||
|
|
QUICClientCredentials creds)
|
||
|
|
: alpn(std::move(alpnArg)), impl(std::make_unique<Impl>()) {
|
||
|
|
// Zeroed hash means "no pinning" — JS passes an empty array, browser
|
||
|
|
// falls back to its trust store. A non-zero hash is forwarded as a
|
||
|
|
// serverCertificateHashes entry (Chrome only, < 14 day cert validity).
|
||
|
|
const std::uint8_t* hashPtr = nullptr;
|
||
|
|
std::int32_t hashLen = 0;
|
||
|
|
for (std::uint8_t b : creds.serverCertificateHash) {
|
||
|
|
if (b != 0) { hashPtr = creds.serverCertificateHash.data(); hashLen = 32; break; }
|
||
|
|
}
|
||
|
|
std::string hostStr = host;
|
||
|
|
impl->state.handle = Crafter::NetworkBrowserBindings::crafterNetworkWtConnect(
|
||
|
|
hostStr.data(), static_cast<std::int32_t>(hostStr.size()),
|
||
|
|
static_cast<std::int32_t>(port),
|
||
|
|
alpn.data(), static_cast<std::int32_t>(alpn.size()),
|
||
|
|
hashPtr, hashLen);
|
||
|
|
// wasm builds run with -fno-exceptions; a failed JS-side allocation
|
||
|
|
// leaves the connection in a closed state and the first operation will
|
||
|
|
// produce a sentinel result (OpenStream returns a default-constructed
|
||
|
|
// QUICStream, SendDatagram silently drops, OnStream/OnDatagram never
|
||
|
|
// fire). The constructor cannot signal the failure synchronously.
|
||
|
|
if (impl->state.handle == 0) {
|
||
|
|
impl->state.closed = true;
|
||
|
|
} else {
|
||
|
|
Connections()[impl->state.handle] = &impl->state;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
ClientQUIC::ClientQUIC(std::string host, std::uint16_t port, std::string alpnArg,
|
||
|
|
QUICClientCredentials creds)
|
||
|
|
: ClientQUIC(host.c_str(), port, std::move(alpnArg), std::move(creds)) {}
|
||
|
|
|
||
|
|
ClientQUIC::ClientQUIC(ClientQUIC&&) noexcept = default;
|
||
|
|
|
||
|
|
ClientQUIC::~ClientQUIC() {
|
||
|
|
if (impl && impl->state.handle != 0) {
|
||
|
|
Connections().erase(impl->state.handle);
|
||
|
|
Crafter::NetworkBrowserBindings::crafterNetworkWtClose(impl->state.handle);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
QUICStream ClientQUIC::OpenStream(bool unidirectional) {
|
||
|
|
if (!impl || impl->state.handle == 0 || impl->state.closed) {
|
||
|
|
return QUICStream{}; // default-constructed: closed sentinel
|
||
|
|
}
|
||
|
|
std::int32_t streamId = Crafter::NetworkBrowserBindings::crafterNetworkWtOpenStream(
|
||
|
|
impl->state.handle, unidirectional ? 1 : 0);
|
||
|
|
if (streamId == 0) {
|
||
|
|
return QUICStream{};
|
||
|
|
}
|
||
|
|
return MakeStreamFromHandle(streamId, this,
|
||
|
|
/*canSend=*/true,
|
||
|
|
/*canReceive=*/!unidirectional);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ClientQUIC::SendDatagram(const void* buffer, std::uint32_t size) {
|
||
|
|
if (!impl || impl->state.handle == 0 || impl->state.closed) return;
|
||
|
|
Crafter::NetworkBrowserBindings::crafterNetworkWtSendDatagram(
|
||
|
|
impl->state.handle,
|
||
|
|
static_cast<const char*>(buffer),
|
||
|
|
static_cast<std::int32_t>(size));
|
||
|
|
}
|
||
|
|
|
||
|
|
void ClientQUIC::OnStream(std::function<void(QUICStream)> cb) {
|
||
|
|
if (impl) impl->state.onStream = std::move(cb);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ClientQUIC::OnDatagram(std::function<void(std::vector<char>)> cb) {
|
||
|
|
if (impl) impl->state.onDatagram = std::move(cb);
|
||
|
|
}
|
||
|
|
|
||
|
|
void ClientQUIC::Stop() {
|
||
|
|
if (impl && impl->state.handle != 0) {
|
||
|
|
Crafter::NetworkBrowserBindings::crafterNetworkWtClose(impl->state.handle);
|
||
|
|
Connections().erase(impl->state.handle);
|
||
|
|
impl->state.handle = 0;
|
||
|
|
impl->state.closed = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── WASM exports the JS bridge dispatches back through ──────────────────
|
||
|
|
|
||
|
|
extern "C" {
|
||
|
|
__attribute__((export_name("CrafterNetworkOnWtReady")))
|
||
|
|
void CrafterNetworkOnWtReady(std::int32_t connectionHandle) {
|
||
|
|
auto it = Connections().find(connectionHandle);
|
||
|
|
if (it == Connections().end()) return;
|
||
|
|
it->second->ready = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
__attribute__((export_name("CrafterNetworkOnWtClosed")))
|
||
|
|
void CrafterNetworkOnWtClosed(std::int32_t connectionHandle,
|
||
|
|
char* messagePtr, std::int32_t /*messageLen*/) {
|
||
|
|
std::free(messagePtr);
|
||
|
|
auto it = Connections().find(connectionHandle);
|
||
|
|
if (it == Connections().end()) return;
|
||
|
|
it->second->closed = true;
|
||
|
|
it->second->ready = false;
|
||
|
|
// The JS bridge dispatches CrafterNetworkOnWtStreamChunk(_, nullptr, 0,
|
||
|
|
// fin=1) for each stream that belonged to this connection so pending
|
||
|
|
// receivers terminate. We don't iterate stream state here.
|
||
|
|
}
|
||
|
|
|
||
|
|
__attribute__((export_name("CrafterNetworkOnWtIncomingStream")))
|
||
|
|
void CrafterNetworkOnWtIncomingStream(std::int32_t connectionHandle,
|
||
|
|
std::int32_t streamId,
|
||
|
|
std::int32_t bidirectional) {
|
||
|
|
auto it = Connections().find(connectionHandle);
|
||
|
|
if (it == Connections().end() || !it->second->onStream) {
|
||
|
|
// No registered handler — close the stream JS-side to free
|
||
|
|
// resources. Mirrors what the native msquic backend does
|
||
|
|
// when a stream arrives before OnStream is registered (it
|
||
|
|
// queues, but in the browser we don't have a backing buffer
|
||
|
|
// to queue against without leaking).
|
||
|
|
Crafter::NetworkBrowserBindings::crafterNetworkWtStreamStop(streamId);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
QUICStream stream = MakeStreamFromHandle(streamId, /*conn=*/nullptr,
|
||
|
|
/*canSend=*/bidirectional != 0,
|
||
|
|
/*canReceive=*/true);
|
||
|
|
it->second->onStream(std::move(stream));
|
||
|
|
}
|
||
|
|
|
||
|
|
__attribute__((export_name("CrafterNetworkOnWtStreamChunk")))
|
||
|
|
void CrafterNetworkOnWtStreamChunk(std::int32_t streamId,
|
||
|
|
char* dataPtr, std::int32_t dataLen,
|
||
|
|
std::int32_t fin) {
|
||
|
|
auto it = Streams().find(streamId);
|
||
|
|
if (it == Streams().end()) { std::free(dataPtr); return; }
|
||
|
|
StreamState& s = *it->second;
|
||
|
|
if (dataPtr && dataLen > 0) {
|
||
|
|
s.buffer.insert(s.buffer.end(), dataPtr, dataPtr + dataLen);
|
||
|
|
}
|
||
|
|
std::free(dataPtr);
|
||
|
|
if (fin) s.finReceived = true;
|
||
|
|
TryDispatchRecv(s);
|
||
|
|
}
|
||
|
|
|
||
|
|
__attribute__((export_name("CrafterNetworkOnWtStreamWriteComplete")))
|
||
|
|
void CrafterNetworkOnWtStreamWriteComplete(std::int32_t callbackId) {
|
||
|
|
auto it = WriteCallbacks().find(callbackId);
|
||
|
|
if (it == WriteCallbacks().end()) return;
|
||
|
|
auto cb = std::move(it->second);
|
||
|
|
WriteCallbacks().erase(it);
|
||
|
|
if (cb) cb();
|
||
|
|
}
|
||
|
|
|
||
|
|
__attribute__((export_name("CrafterNetworkOnWtDatagram")))
|
||
|
|
void CrafterNetworkOnWtDatagram(std::int32_t connectionHandle,
|
||
|
|
char* dataPtr, std::int32_t dataLen) {
|
||
|
|
auto it = Connections().find(connectionHandle);
|
||
|
|
if (it == Connections().end() || !it->second->onDatagram) {
|
||
|
|
std::free(dataPtr);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
std::vector<char> data(dataPtr, dataPtr + dataLen);
|
||
|
|
std::free(dataPtr);
|
||
|
|
it->second->onDatagram(std::move(data));
|
||
|
|
}
|
||
|
|
}
|