/* 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 buffer; bool finReceived = false; bool closed = false; // Pending one-shot receive. RecvMode mode = RecvMode::None; std::uint32_t target = 0; std::function)> cb; ClientQUIC* connection = nullptr; }; struct ConnectionState { std::int32_t handle = 0; bool ready = false; bool closed = false; std::function onStream; std::function)> onDatagram; }; // Handle → state. Allocated on the heap so the pointer is stable // across QUICStream / ClientQUIC moves (each holds a unique_ptr // that wraps a pointer into these maps via its handle). inline std::unordered_map& Streams() { static std::unordered_map m; return m; } inline std::unordered_map& Connections() { static std::unordered_map m; return m; } inline std::unordered_map>& WriteCallbacks() { static std::unordered_map> 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 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()) {} QUICStream::QUICStream(std::int32_t streamHandle, ClientQUIC* conn, bool canSendArg, bool canReceiveArg) : canSend(canSendArg), canReceive(canReceiveArg), impl(std::make_unique()) { 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 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(buffer), static_cast(size), finish ? 1 : 0, cbId); } void QUICStream::RecieveAsync(std::function)> 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)> 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)> 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()) { // 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(hostStr.size()), static_cast(port), alpn.data(), static_cast(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(buffer), static_cast(size)); } void ClientQUIC::OnStream(std::function cb) { if (impl) impl->state.onStream = std::move(cb); } void ClientQUIC::OnDatagram(std::function)> 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 data(dataPtr, dataPtr + dataLen); std::free(dataPtr); it->second->onDatagram(std::move(data)); } }