// The CultMesh MsQuic bridge: one MsQuic body, two roles, two platforms. // // This file owns MsQuic registration, configuration, listener, connection and // stream handles, TLS credential loading, the 1-byte-kind plus 4-byte-LE-length // QUIC stream framing, and a single event queue per runtime. It owns transport // only. It never decodes a CultMesh frame, never decides delivery semantics, // never coalesces, never reconnects, and in the v2 path never accepts a // certificate on its own: the host answers, through // `cultmesh_quic_connection_certificate_complete`. // // No host callbacks. Every crossing is the host calling in, including the one // blocking wait (`cultmesh_quic_next_event`). MsQuic's worker threads only ever // push onto the queue and wake that wait, so the bridge is runtime-neutral: it // includes no Node headers, no Unity headers, and nothing platform-specific // outside the `_WIN32` block at the bottom of this file. // // Two ABIs live here. v2 is the queue API above, for any host. v1 is the five // exports the Unity managed connector already imports by name // (`CultMeshNativeQuicRealtimeTransport.cs`, `NativeMethods`), kept Windows-only // and implemented on top of a private v2 runtime. v1 is the only place a // certificate pin is compared in C, because that is the contract Unity was // shipped with; it delegates everything else and decides nothing new. // The host-facing contract lives in include/cultmesh_quic_native.h and is // included here so this file cannot drift from it: the event layout, the // exports, the return codes, the lifetime rules and the threading rules are // stated there once and asserted at compile time. #define CULTMESH_QUIC_BUILD 1 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define CULTMESH_API extern "C" CULTMESH_QUIC_API // Development builds only, and never in a shipped binary: after // `cultmesh_quic_runtime_close` has quiesced, no host call may still be inside // the library. The wait is what makes that true, and its absence is otherwise // visible only as a use-after-free a sanitizer catches by luck — a poller has to // be preempted in the right window. With pollers blocked, this states the // invariant directly, so a build without the wait dies on the first close rather // than on the run that happens to be unlucky. #if defined(CULTMESH_QUIC_DEBUG_ASSERTS) #define CULTMESH_QUIC_ASSERT_QUIESCED(count) \ do { \ const int cultmesh_quic_remaining = (count); \ if (cultmesh_quic_remaining != 0) { \ std::fprintf(stderr, \ "cultmesh_quic_runtime_close: %d host call(s) still inside the " \ "library after the quiesce\n", cultmesh_quic_remaining); \ std::fflush(stderr); \ std::abort(); \ } \ } while (false) #else #define CULTMESH_QUIC_ASSERT_QUIESCED(count) ((void)0) #endif // Development builds only, and never in a shipped binary: the seam the scenario // runner drives the quiesce through. A scenario that only sleeps and hopes its // pollers are inside the library cannot tell a bridge that counts host calls // from one that does not, so the bridge says so itself: // // - PEAK raises the high-water mark of calls counted inside, so a scenario can // assert that the calls it launched were counted rather than assumed. // - AT_CLOSE records how many were counted when the close began its wait, which // is the number the wait exists for. // - HELD parks a call inside the library until the scenario releases it, so the // close races calls that are genuinely there for as long as the scenario // wants them there, instead of for as long as the schedule happens to give. // It returns true when it parked, and a held call makes no further call of // its own; what it does still do is leave its CallScope, which takes the gate, // decrements the count and notifies. That is the ordinary exit every export // makes, and it is the bridge's wait — not the hold — that keeps the runtime // alive underneath it. // - RECORD_WAIT wraps the whole duration handed to the condition wait in // `cultmesh_quic_next_event`, in place, as a pass-through: it stores that // duration's count and returns the duration unchanged, so a mutation to // what is actually waited on changes what this reports rather than the two // ever being able to diverge. It wraps `timeout_ms` after conversion to // `std::chrono::milliseconds`, not before — Self's ruling of 2026-09-22, // after arithmetic applied to the converted duration survived a seam that // only recorded the raw integer underneath it. A scenario reads it and // asserts equality against the timeout it asked for, deterministically and // with no timing tolerance, in place of inferring the same fact from how // long a call took to return. #if defined(CULTMESH_QUIC_DEBUG_ASSERTS) #define CULTMESH_QUIC_DEBUG_PEAK(count) DebugRaisePeak(count) #define CULTMESH_QUIC_DEBUG_AT_CLOSE(count) DebugRecordCallsAtClose(count) #define CULTMESH_QUIC_DEBUG_HELD(lock) DebugHoldCall(lock) #define CULTMESH_QUIC_DEBUG_RECORD_WAIT(duration) DebugRecordWait(duration) #else #define CULTMESH_QUIC_DEBUG_PEAK(count) ((void)0) #define CULTMESH_QUIC_DEBUG_AT_CLOSE(count) ((void)0) #define CULTMESH_QUIC_DEBUG_HELD(lock) (false) #define CULTMESH_QUIC_DEBUG_RECORD_WAIT(duration) (duration) #endif namespace { constexpr uint32_t kApiVersion = 2; constexpr uint64_t kConnectionCloseCode = 0x43554c54; constexpr uint64_t kStreamAbortCode = 0x53544154; constexpr uint8_t kReliableStream = CULTMESH_QUIC_STREAM_RELIABLE; constexpr uint8_t kLatestOnlyStream = CULTMESH_QUIC_STREAM_LATEST_ONLY; constexpr uint32_t kMaximumEncodedFrameBytes = (64u * 1024u * 1024u) + 37u + (3u * 65535u); constexpr char kAlpn[] = "cultmesh-state-v1"; // Mirrors the C# server and connector: CultMeshQuicRealtimeTransport.cs:77,:202 // for the stream ceiling, and the idle/keep-alive pair the v1 bridge already // used (:335-338). constexpr uint32_t kIdleTimeoutMs = 30000; constexpr uint32_t kKeepAliveIntervalMs = 5000; constexpr uint16_t kPeerUnidiStreamCount = 1024; // If the host never answers the certificate event, this is what closes the // connection instead of leaking it: MsQuic's own handshake timeout fires and // the shutdown arrives as event 4. constexpr uint32_t kHandshakeIdleTimeoutMs = 10000; // Short names for the header's event codes; the header is the contract. constexpr uint32_t kEventListenerNewConnection = CULTMESH_QUIC_EVENT_LISTENER_NEW_CONNECTION; constexpr uint32_t kEventConnectionConnected = CULTMESH_QUIC_EVENT_CONNECTION_CONNECTED; constexpr uint32_t kEventConnectionCertificateReceived = CULTMESH_QUIC_EVENT_CONNECTION_CERTIFICATE_RECEIVED; constexpr uint32_t kEventConnectionShutdown = CULTMESH_QUIC_EVENT_CONNECTION_SHUTDOWN; constexpr uint32_t kEventStreamStarted = CULTMESH_QUIC_EVENT_STREAM_STARTED; constexpr uint32_t kEventStreamFrame = CULTMESH_QUIC_EVENT_STREAM_FRAME; constexpr uint32_t kEventStreamSendComplete = CULTMESH_QUIC_EVENT_STREAM_SEND_COMPLETE; constexpr uint32_t kEventStreamShutdown = CULTMESH_QUIC_EVENT_STREAM_SHUTDOWN; constexpr uint32_t kEventListenerStopped = CULTMESH_QUIC_EVENT_LISTENER_STOPPED; struct Runtime; struct Connection; struct QueuedEvent { cultmesh_quic_event header{}; std::vector payload; }; // Objects are shared-owned, and that is the whole lifetime model. The runtime's // maps hold one reference each; an export that names an id takes its own // reference under the lock and holds it for the entire MsQuic call. MsQuic's // worker, on shutdown-complete, drops the map's reference only. The handle // follows the object: it is closed exactly once, in the destructor, by whichever // holder drops last, under a one-shot exchange guard. See section 4 of // include/cultmesh_quic_native.h, which is where this is promised to hosts. struct Stream { Runtime* runtime = nullptr; // Shared, not raw: this is what keeps a connection's handle open for as long // as any of its streams still exist, so stream handles always close first. std::shared_ptr owner; HQUIC handle = nullptr; uint64_t id = 0; uint8_t kind = 0; bool inbound = false; bool has_kind = false; std::atomic handle_closed{false}; // Inbound reassembly only. MsQuic delivers stream bytes in arbitrary // chunks, so the length prefix and the frame it introduces can arrive in // separate receives. std::vector buffered; ~Stream(); }; struct Connection : std::enable_shared_from_this { Runtime* runtime = nullptr; HQUIC handle = nullptr; uint64_t id = 0; uint64_t listener_id = 0; bool inbound = false; // Written by the host thread in `cultmesh_quic_connection_shutdown`, read on // an MsQuic worker when the shutdown completes: the code the host passed is // what its own event 4 has to carry. std::atomic closing{false}; std::atomic shutdown_code{0}; // Why it ended, recorded by whichever initiated-by callback saw it and read // by shutdown-complete, which is the single emitter of event 4. Both run on // this connection's own worker, serialized. std::string shutdown_reason; int32_t shutdown_status = 0; // Set when MsQuic is closing the handle itself (an app close already in // progress), so the destructor's one-shot close stands down. std::atomic handle_closed{false}; // v1 only. A connection carrying a pin has its certificate answered in C // against that pin; a v2 connection never sets this and is always answered // by the host. Nothing outside the `_WIN32` block below ever sets it. bool has_pin = false; std::array pin{}; // v1 diagnostics: the MsQuic event sequence, which the v1 error string // reports and nothing in v2 reads. std::string trace; ~Connection(); }; struct Listener { Runtime* runtime = nullptr; HQUIC handle = nullptr; HQUIC configuration = nullptr; uint64_t id = 0; std::atomic handle_closed{false}; ~Listener(); }; struct Runtime { const QUIC_API_TABLE* api = nullptr; HQUIC registration = nullptr; // One client configuration serves every outbound connection: the ALPN, // settings and credential flags are identical for all of them. HQUIC client_configuration = nullptr; std::mutex gate; std::condition_variable signal; std::deque events; std::unordered_map> listeners; std::unordered_map> connections; std::unordered_map> streams; uint64_t next_id = 1; std::string error; // The raw platform status behind the last MsQuic refusal, kept because the // portable return code deliberately throws its bits away. int32_t last_status = 0; bool closing = false; // Host calls currently inside the library. `cultmesh_quic_runtime_close` // waits for this to reach zero before it touches a handle. int active_calls = 0; }; #if defined(CULTMESH_QUIC_DEBUG_ASSERTS) // The development seam behind the three CULTMESH_QUIC_DEBUG_ macros. None of it // is compiled into a shipped library. std::atomic debug_peak_calls{0}; std::atomic debug_calls_at_close{-1}; std::mutex debug_hold_gate; std::condition_variable debug_hold_signal; bool debug_hold_armed = false; std::atomic debug_last_wait_ms{-1}; // Separate from the value above on purpose: once the seam records the whole // duration (not just a millisecond count already known to be positive), a // mutation could in principle make a wait get recorded at exactly -1, and an // int32_t alone cannot tell that apart from "nothing recorded yet". This flag // is the "recorded" bit; the getter below is the only thing that still hands // out -1 for the unset case. std::atomic debug_wait_recorded{false}; void DebugRaisePeak(int count) { int peak = debug_peak_calls.load(std::memory_order_relaxed); while (count > peak && !debug_peak_calls.compare_exchange_weak(peak, count)) { } } void DebugRecordCallsAtClose(int count) { debug_calls_at_close.store(count); } // Parks the calling host thread inside the library until the scenario releases // it. The runtime gate is dropped first, so nothing else is blocked behind the // held call, and it is never retaken: the call is still counted, because its // CallScope is still alive, and that is the whole point. bool DebugHoldCall(std::unique_lock& lock) { std::unique_lock hold(debug_hold_gate); if (!debug_hold_armed) return false; lock.unlock(); debug_hold_signal.wait(hold, [] { return !debug_hold_armed; }); return true; } // A pass-through: it stores the duration's count and returns the duration // unmodified, so wrapping it around the whole expression handed to the // condition wait makes the two impossible to desync by construction. Whatever // that expression evaluates to — the plain converted argument today, or a // derivation a future change or a mutation introduces inside this call — is // what gets recorded, because it is the same evaluation. Anything applied // outside this call, after it returns, is what the wall-clock scenarios exist // to catch instead; see their generous-tolerance comment. std::chrono::milliseconds DebugRecordWait(std::chrono::milliseconds duration) { debug_last_wait_ms.store(static_cast(duration.count())); debug_wait_recorded.store(true); return duration; } #endif // Every host-facing export enters through this. It refuses once // `cultmesh_quic_runtime_close` has started, and otherwise holds the runtime // open for the length of the call. class CallScope { public: explicit CallScope(Runtime* runtime) : runtime_(runtime) { if (runtime_ == nullptr) return; std::lock_guard lock(runtime_->gate); if (runtime_->closing) return; ++runtime_->active_calls; CULTMESH_QUIC_DEBUG_PEAK(runtime_->active_calls); entered_ = true; } // The notify stays under `gate`, and that is the whole point of it. Released // first, the closer's predicate is already true, so it can return from its // wait, run the teardown and destroy the runtime — condition variable // included — before this thread reaches the notify. Holding the lock across // both means the closer cannot leave `signal.wait` until this scope has let // go, so the condition variable is still alive when it is signalled. ~CallScope() { if (!entered_) return; std::lock_guard lock(runtime_->gate); --runtime_->active_calls; runtime_->signal.notify_all(); } CallScope(const CallScope&) = delete; CallScope& operator=(const CallScope&) = delete; bool entered() const { return entered_; } private: Runtime* runtime_ = nullptr; bool entered_ = false; }; Stream::~Stream() { if (handle != nullptr && runtime != nullptr && runtime->api != nullptr && !handle_closed.exchange(true)) runtime->api->StreamClose(handle); } Connection::~Connection() { if (handle != nullptr && runtime != nullptr && runtime->api != nullptr && !handle_closed.exchange(true)) runtime->api->ConnectionClose(handle); } Listener::~Listener() { if (runtime == nullptr || runtime->api == nullptr) return; if (handle != nullptr && !handle_closed.exchange(true)) runtime->api->ListenerClose(handle); if (configuration != nullptr) runtime->api->ConfigurationClose(configuration); } // Every send buffer outlives its `StreamSend` call: MsQuic reads the bytes // asynchronously and only releases them at SEND_COMPLETE, where this is freed. struct SendRequest { QUIC_BUFFER buffer{}; std::vector bytes; // Which send this was. The bridge writes the stream's kind byte itself, and // that completion is not the host's to hear about; event 7 is one per // `cultmesh_quic_stream_send_frame` and nothing else. bool is_frame = false; }; // A `QUIC_STATUS` reaches this through `uint32_t`, never straight to `uint64_t`: // it is a signed HRESULT on Windows, and widening it signed printed every // Windows failure as `0xFFFFFFFF80072B18`. The status is 32 bits on both // platforms and is read as 32 bits here. std::string Hex(uint64_t value) { std::ostringstream text; text << "0x" << std::hex << std::uppercase << value; return text.str(); } void SetError(Runtime* runtime, const std::string& message) { if (runtime == nullptr) return; std::lock_guard lock(runtime->gate); runtime->error = message; } // `QUIC_STATUS` never reaches a host. It is a negative HRESULT on Windows and a // positive errno on POSIX, so a host that checked for a negative value saw every // Linux failure as success. One negative space on both platforms instead; the // raw bits go to `cultmesh_quic_last_status`. See section 3 of the header. int32_t MsQuicCode(QUIC_STATUS status) { return CULTMESH_QUIC_RESULT_MSQUIC_BASE - static_cast(static_cast(status) & 0xffffu); } int32_t Refuse(Runtime* runtime, QUIC_STATUS status, const std::string& message) { if (runtime != nullptr) { std::lock_guard lock(runtime->gate); runtime->error = message; runtime->last_status = static_cast(status); } return MsQuicCode(status); } // The only way anything reaches the host. Callers must not hold `gate`. void Publish(Runtime* runtime, const cultmesh_quic_event& header, const uint8_t* payload, size_t payload_length) { QueuedEvent queued; queued.header = header; queued.header.payload_length = static_cast(payload_length); if (payload != nullptr && payload_length > 0) queued.payload.assign(payload, payload + payload_length); { std::lock_guard lock(runtime->gate); if (runtime->closing) return; runtime->events.push_back(std::move(queued)); } runtime->signal.notify_all(); } void PublishSimple(Runtime* runtime, uint32_t type, uint64_t listener_id, uint64_t connection_id, uint64_t stream_id, uint64_t code, int32_t status, uint32_t stream_kind = 0) { cultmesh_quic_event header{}; header.type = type; header.stream_kind = stream_kind; header.listener_id = listener_id; header.connection_id = connection_id; header.stream_id = stream_id; header.code = code; header.status = status; Publish(runtime, header, nullptr, 0); } // v1 diagnostics only, and bounded. A v2 connection has no reader for this, and // a long-lived one appended to it once per MsQuic event for as long as it lived. constexpr size_t kMaximumTraceBytes = 512; void AppendTrace(Connection* connection, const std::string& event) { if (connection == nullptr || !connection->has_pin) return; std::lock_guard lock(connection->runtime->gate); if (connection->trace.size() >= kMaximumTraceBytes) return; if (!connection->trace.empty()) connection->trace += ","; connection->trace += event; } uint32_t ReadUInt32LittleEndian(const uint8_t* value) { return static_cast(value[0]) | (static_cast(value[1]) << 8) | (static_cast(value[2]) << 16) | (static_cast(value[3]) << 24); } void WriteUInt32LittleEndian(uint8_t* destination, uint32_t value) { destination[0] = static_cast(value & 0xff); destination[1] = static_cast((value >> 8) & 0xff); destination[2] = static_cast((value >> 16) & 0xff); destination[3] = static_cast((value >> 24) & 0xff); } uint64_t NextId(Runtime* runtime) { std::lock_guard lock(runtime->gate); return runtime->next_id++; } // The three lookups. Each returns a reference the caller owns for as long as it // holds it, taken under the lock, so nothing the caller then names can be freed // under it. A raw pointer here was the use-after-free. std::shared_ptr FindConnection(Runtime* runtime, uint64_t id) { std::lock_guard lock(runtime->gate); const auto found = runtime->connections.find(id); return found == runtime->connections.end() ? nullptr : found->second; } std::shared_ptr FindStream(Runtime* runtime, uint64_t id) { std::lock_guard lock(runtime->gate); const auto found = runtime->streams.find(id); return found == runtime->streams.end() ? nullptr : found->second; } // Takes the object out of its map and returns it, so the caller's reference — // and any destructor it ends up running — lives outside the lock. template auto Detach(std::mutex& gate, Map& map, uint64_t id) { typename Map::mapped_type detached; std::lock_guard lock(gate); const auto found = map.find(id); if (found == map.end()) return detached; detached = std::move(found->second); map.erase(found); return detached; } void ShutdownConnectionForProtocolFault(const std::shared_ptr& connection, const std::string& message) { if (connection == nullptr) return; SetError(connection->runtime, message); if (connection->runtime->api != nullptr && connection->handle != nullptr) connection->runtime->api->ConnectionShutdown( connection->handle, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, kStreamAbortCode); } // Reassembles whole encoded frames out of the stream byte sequence and // publishes one event 6 per frame, length prefix stripped. The first byte of an // inbound stream is its kind, which becomes event 5. bool ConsumeFrames(Stream* stream) { Runtime* runtime = stream->runtime; auto& bytes = stream->buffered; if (!stream->has_kind) { if (bytes.empty()) return true; stream->kind = bytes.front(); bytes.erase(bytes.begin()); stream->has_kind = true; if (stream->kind != kReliableStream && stream->kind != kLatestOnlyStream) { ShutdownConnectionForProtocolFault(stream->owner, "CultMesh QUIC stream kind is invalid."); return false; } PublishSimple(runtime, kEventStreamStarted, stream->owner->listener_id, stream->owner->id, stream->id, 0, 0, stream->kind); } while (bytes.size() >= sizeof(uint32_t)) { const uint32_t length = ReadUInt32LittleEndian(bytes.data()); if (length == 0 || length > kMaximumEncodedFrameBytes) { ShutdownConnectionForProtocolFault(stream->owner, "CultMesh QUIC frame length is invalid."); return false; } const size_t framed_length = sizeof(uint32_t) + static_cast(length); if (bytes.size() < framed_length) return true; cultmesh_quic_event header{}; header.type = kEventStreamFrame; header.stream_kind = stream->kind; header.listener_id = stream->owner->listener_id; header.connection_id = stream->owner->id; header.stream_id = stream->id; Publish(runtime, header, bytes.data() + sizeof(uint32_t), length); bytes.erase(bytes.begin(), bytes.begin() + static_cast(framed_length)); } return true; } // Drops the map's reference and nothing else. If the map was the last holder the // object dies here, outside the lock, and its destructor closes the handle; if a // host call is still inside, the object outlives this by exactly that long. void DestroyStream(Runtime* runtime, uint64_t stream_id) { [[maybe_unused]] const auto detached = Detach(runtime->gate, runtime->streams, stream_id); } QUIC_STATUS QUIC_API StreamCallback(HQUIC, void* context, QUIC_STREAM_EVENT* event) { auto* stream = static_cast(context); Runtime* runtime = stream->runtime; switch (event->Type) { case QUIC_STREAM_EVENT_RECEIVE: for (uint32_t index = 0; index < event->RECEIVE.BufferCount; ++index) { const auto& buffer = event->RECEIVE.Buffers[index]; stream->buffered.insert(stream->buffered.end(), buffer.Buffer, buffer.Buffer + buffer.Length); } ConsumeFrames(stream); break; case QUIC_STREAM_EVENT_SEND_COMPLETE: { // Which send finished is the client context's job to say. Without it the // bridge's own kind byte completed as an anonymous event 7 and the host // counted six completions for five frames. auto* request = static_cast(event->SEND_COMPLETE.ClientContext); const bool is_frame = request != nullptr && request->is_frame; delete request; if (is_frame) PublishSimple(runtime, kEventStreamSendComplete, stream->owner->listener_id, stream->owner->id, stream->id, event->SEND_COMPLETE.Canceled ? CULTMESH_QUIC_SEND_CANCELED : CULTMESH_QUIC_SEND_COMPLETED, 0, stream->kind); break; } case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN: if (!stream->buffered.empty()) ShutdownConnectionForProtocolFault(stream->owner, "CultMesh QUIC stream ended with a truncated frame."); break; case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: { // Everything this event reports is read before the map's reference goes, // because that reference may be the last one and `stream` may not // survive `DestroyStream`. const uint64_t stream_id = stream->id; const uint64_t connection_id = stream->owner->id; const uint64_t listener_id = stream->owner->listener_id; const uint32_t kind = stream->kind; // An app close already in progress means MsQuic closes this handle // itself; anything else leaves it to the last holder's destructor. if (event->SHUTDOWN_COMPLETE.AppCloseInProgress) stream->handle_closed.store(true); DestroyStream(runtime, stream_id); PublishSimple(runtime, kEventStreamShutdown, listener_id, connection_id, stream_id, 0, 0, kind); break; } default: break; } return QUIC_STATUS_SUCCESS; } void DestroyConnection(Runtime* runtime, uint64_t connection_id) { // Declared first so it dies last: whatever order these drop in, a stream // holds its connection, so every stream handle closes before the connection // handle does. std::shared_ptr connection; std::vector> streams; { std::lock_guard lock(runtime->gate); for (auto it = runtime->streams.begin(); it != runtime->streams.end();) { if (it->second->owner != nullptr && it->second->owner->id == connection_id) { streams.push_back(std::move(it->second)); it = runtime->streams.erase(it); } else { ++it; } } const auto found = runtime->connections.find(connection_id); if (found != runtime->connections.end()) { connection = std::move(found->second); runtime->connections.erase(found); } } } #if defined(_WIN32) // Declared here, defined in the v1 block at the bottom: the pin comparison is // the one piece of the certificate path that is Windows-only and v1-only. bool CertificateMatchesPin(Connection* connection, QUIC_CERTIFICATE* certificate); #endif QUIC_STATUS QUIC_API ConnectionCallback(HQUIC handle, void* context, QUIC_CONNECTION_EVENT* event) { auto* connection = static_cast(context); Runtime* runtime = connection->runtime; AppendTrace(connection, "event=" + std::to_string(static_cast(event->Type))); switch (event->Type) { case QUIC_CONNECTION_EVENT_CONNECTED: PublishSimple(runtime, kEventConnectionConnected, connection->listener_id, connection->id, 0, 0, 0); break; case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: { auto stream = std::make_shared(); stream->runtime = runtime; stream->owner = connection->shared_from_this(); stream->handle = event->PEER_STREAM_STARTED.Stream; stream->inbound = true; stream->id = NextId(runtime); Stream* raw = stream.get(); { std::lock_guard lock(runtime->gate); runtime->streams.emplace(raw->id, std::move(stream)); } runtime->api->SetCallbackHandler( event->PEER_STREAM_STARTED.Stream, reinterpret_cast(StreamCallback), raw); break; } case QUIC_CONNECTION_EVENT_PEER_CERTIFICATE_RECEIVED: { const auto* encoded = reinterpret_cast(event->PEER_CERTIFICATE_RECEIVED.Certificate); AppendTrace(connection, "certificate-status=" + Hex(static_cast(event->PEER_CERTIFICATE_RECEIVED.DeferredStatus))); #if defined(_WIN32) if (connection->has_pin) { // The v1 contract: the pin decision is made here, in C, and the host // is never asked. Only `cultmesh_quic_open` sets `has_pin`. const bool matches = CertificateMatchesPin( connection, event->PEER_CERTIFICATE_RECEIVED.Certificate); if (!matches) SetError(runtime, "CultMesh QUIC provider certificate does not match the advertised SHA-256 pin."); const auto completion = runtime->api->ConnectionCertificateValidationComplete( handle, matches ? static_cast(1) : static_cast(0), matches ? QUIC_TLS_ALERT_CODE_SUCCESS : QUIC_TLS_ALERT_CODE_BAD_CERTIFICATE); if (QUIC_FAILED(completion)) { SetError(runtime, "CultMesh QUIC certificate validation completion failed (status=" + Hex(static_cast(completion)) + ")."); return completion; } return QUIC_STATUS_PENDING; } #endif // v2: the DER goes to the host and the handshake waits. Nothing here // decides trust. If the host never answers, MsQuic's handshake timeout // closes the connection and event 4 reports it. cultmesh_quic_event header{}; header.type = kEventConnectionCertificateReceived; header.listener_id = connection->listener_id; header.connection_id = connection->id; header.status = static_cast(event->PEER_CERTIFICATE_RECEIVED.DeferredStatus); const uint8_t* der = (encoded != nullptr) ? encoded->Buffer : nullptr; const size_t der_length = (encoded != nullptr) ? encoded->Length : 0; Publish(runtime, header, der, der_length); return QUIC_STATUS_PENDING; } // The two initiated-by events say why a connection is ending, not that it // has ended; the object and its id are still live here. They record the // reason and the code, and shutdown-complete below is the one place event 4 // is emitted, once per connection id, after the id has stopped resolving. // All three of these run on the connection's own MsQuic worker, serialized, // which is what lets these fields be plain members. case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT: connection->shutdown_reason = "CultMesh QUIC connection was shut down by the transport (status=" + Hex(static_cast(event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status)) + ", error=" + Hex(event->SHUTDOWN_INITIATED_BY_TRANSPORT.ErrorCode) + ")."; connection->shutdown_code.store(event->SHUTDOWN_INITIATED_BY_TRANSPORT.ErrorCode); connection->shutdown_status = static_cast(event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status); break; case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER: connection->shutdown_reason = "CultMesh QUIC connection was shut down by the peer."; connection->shutdown_code.store(event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode); break; case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE: { const uint64_t connection_id = connection->id; if (event->SHUTDOWN_COMPLETE.AppCloseInProgress) connection->handle_closed.store(true); // A host that ended the connection itself gets nothing else from MsQuic // — no initiated-by-peer, no initiated-by-transport — so without this // its own connection id simply stopped existing and the end of a // connection was observable only from the other side. cultmesh_quic_event header{}; header.type = kEventConnectionShutdown; header.listener_id = connection->listener_id; header.connection_id = connection_id; header.code = connection->shutdown_code.load(); header.status = connection->shutdown_status; const std::string reason = connection->shutdown_reason.empty() ? std::string("CultMesh QUIC connection was shut down by the host.") : connection->shutdown_reason; DestroyConnection(runtime, connection_id); Publish(runtime, header, reinterpret_cast(reason.data()), reason.size()); break; } default: break; } return QUIC_STATUS_SUCCESS; } QUIC_STATUS QUIC_API ListenerCallback(HQUIC, void* context, QUIC_LISTENER_EVENT* event) { auto* listener = static_cast(context); Runtime* runtime = listener->runtime; switch (event->Type) { case QUIC_LISTENER_EVENT_NEW_CONNECTION: { auto connection = std::make_shared(); connection->runtime = runtime; connection->handle = event->NEW_CONNECTION.Connection; connection->inbound = true; connection->listener_id = listener->id; connection->id = NextId(runtime); Connection* raw = connection.get(); { std::lock_guard lock(runtime->gate); runtime->connections.emplace(raw->id, std::move(connection)); } runtime->api->SetCallbackHandler( event->NEW_CONNECTION.Connection, reinterpret_cast(ConnectionCallback), raw); const auto status = runtime->api->ConnectionSetConfiguration( event->NEW_CONNECTION.Connection, listener->configuration); if (QUIC_FAILED(status)) { SetError(runtime, "CultMesh QUIC inbound connection configuration failed (status=" + Hex(static_cast(status)) + ")."); // Returning a failure here is MsQuic's signal to reject and close // this connection itself, so the entry made a moment ago has to go // with it rather than sitting in the map naming a dead handle. raw->handle_closed.store(true); DestroyConnection(runtime, raw->id); return status; } PublishSimple(runtime, kEventListenerNewConnection, listener->id, raw->id, 0, 0, 0); break; } case QUIC_LISTENER_EVENT_STOP_COMPLETE: PublishSimple(runtime, kEventListenerStopped, listener->id, 0, 0, 0, 0); break; default: break; } return QUIC_STATUS_SUCCESS; } void FillSettings(QUIC_SETTINGS& settings) { settings.IdleTimeoutMs = kIdleTimeoutMs; settings.IsSet.IdleTimeoutMs = 1; settings.KeepAliveIntervalMs = kKeepAliveIntervalMs; settings.IsSet.KeepAliveIntervalMs = 1; settings.PeerUnidiStreamCount = kPeerUnidiStreamCount; settings.IsSet.PeerUnidiStreamCount = 1; settings.HandshakeIdleTimeoutMs = kHandshakeIdleTimeoutMs; settings.IsSet.HandshakeIdleTimeoutMs = 1; } QUIC_BUFFER AlpnBuffer() { return QUIC_BUFFER{ static_cast(sizeof(kAlpn) - 1), reinterpret_cast(const_cast(kAlpn)) }; } // Opened once per runtime, on first outbound connection. QUIC_STATUS EnsureClientConfiguration(Runtime* runtime) { if (runtime->client_configuration != nullptr) return QUIC_STATUS_SUCCESS; QUIC_SETTINGS settings{}; FillSettings(settings); QUIC_BUFFER alpn = AlpnBuffer(); HQUIC configuration = nullptr; auto status = runtime->api->ConfigurationOpen( runtime->registration, &alpn, 1, &settings, sizeof(settings), nullptr, &configuration); if (QUIC_FAILED(status)) return status; QUIC_CREDENTIAL_CONFIG credentials{}; credentials.Type = QUIC_CREDENTIAL_TYPE_NONE; credentials.Flags = static_cast( QUIC_CREDENTIAL_FLAG_CLIENT | QUIC_CREDENTIAL_FLAG_INDICATE_CERTIFICATE_RECEIVED | QUIC_CREDENTIAL_FLAG_DEFER_CERTIFICATE_VALIDATION | QUIC_CREDENTIAL_FLAG_USE_PORTABLE_CERTIFICATES); status = runtime->api->ConfigurationLoadCredential(configuration, &credentials); if (QUIC_FAILED(status)) { runtime->api->ConfigurationClose(configuration); return status; } runtime->client_configuration = configuration; return QUIC_STATUS_SUCCESS; } // `pin` is the v1 shim's and nothing else passes it. It must be attached before // `ConnectionStart`, not after it returns: the handshake runs on an MsQuic // worker thread and reaches the certificate event immediately on loopback, so a // pin applied afterwards loses the race, the connection takes the v2 path, and // the handshake stalls waiting for an answer from a host that was never told to // give one. int32_t OpenConnection(Runtime* runtime, const char* host, uint16_t port, uint64_t* out_connection_id, const std::array* pin) { if (runtime == nullptr || host == nullptr || *host == '\0' || port == 0 || out_connection_id == nullptr) return -1; *out_connection_id = 0; auto status = EnsureClientConfiguration(runtime); if (QUIC_FAILED(status)) { return Refuse(runtime, status, "CultMesh QUIC client configuration failed (status=" + Hex(static_cast(status)) + ")."); } auto connection = std::make_shared(); connection->runtime = runtime; connection->id = NextId(runtime); if (pin != nullptr) { connection->has_pin = true; connection->pin = *pin; } Connection* raw = connection.get(); status = runtime->api->ConnectionOpen(runtime->registration, ConnectionCallback, raw, &raw->handle); if (QUIC_FAILED(status)) { return Refuse(runtime, status, "CultMesh QUIC connection open failed (status=" + Hex(static_cast(status)) + ")."); } { std::lock_guard lock(runtime->gate); runtime->connections.emplace(raw->id, connection); } status = runtime->api->ConnectionStart( raw->handle, runtime->client_configuration, QUIC_ADDRESS_FAMILY_UNSPEC, host, port); if (QUIC_FAILED(status)) { // The map's reference goes and this local one goes with the scope; the // handle is closed by whichever of the two drops last, exactly once. DestroyConnection(runtime, raw->id); return Refuse(runtime, status, "CultMesh QUIC connection start failed (status=" + Hex(static_cast(status)) + ")."); } *out_connection_id = raw->id; return 0; } } // namespace // --------------------------------------------------------------------------- // C ABI v2: the runtime-neutral bridge. // --------------------------------------------------------------------------- CULTMESH_API int32_t cultmesh_quic_runtime_open(const char* app_name, void** out_runtime) { if (out_runtime == nullptr) return -1; *out_runtime = nullptr; auto runtime = std::make_unique(); // No runtime yet to hang `last_status` on, so these two refusals lose the // raw bits. Everything after this point keeps them. auto status = MsQuicOpenVersion(kApiVersion, reinterpret_cast(&runtime->api)); if (QUIC_FAILED(status)) return MsQuicCode(status); const QUIC_REGISTRATION_CONFIG registration_config = { (app_name != nullptr && *app_name != '\0') ? app_name : "GameCult.Mesh.Quic.Native", QUIC_EXECUTION_PROFILE_TYPE_REAL_TIME }; status = runtime->api->RegistrationOpen(®istration_config, &runtime->registration); if (QUIC_FAILED(status)) { MsQuicClose(runtime->api); return MsQuicCode(status); } *out_runtime = runtime.release(); return 0; } CULTMESH_API void cultmesh_quic_runtime_close(void* handle) { auto* runtime = static_cast(handle); if (runtime == nullptr) return; // Quiesce before anything is closed or freed. Marking the runtime closing // refuses every call that has not started; waking the pollers gets the // blocked ones out of `next_event`; waiting for `active_calls` to reach zero // is what makes the teardown below safe at all. A wake without the wait is // how a poller returns into freed memory, so the wait is the contract, not // the wake. Objects are taken out of the maps here and released after the // lock, so their destructors run on this thread and close their handles. std::vector> streams; std::vector> connections; std::vector> listeners; { std::unique_lock lock(runtime->gate); if (runtime->closing) return; runtime->closing = true; runtime->signal.notify_all(); CULTMESH_QUIC_DEBUG_AT_CLOSE(runtime->active_calls); runtime->signal.wait(lock, [runtime] { return runtime->active_calls == 0; }); CULTMESH_QUIC_ASSERT_QUIESCED(runtime->active_calls); for (auto& entry : runtime->streams) streams.push_back(std::move(entry.second)); for (auto& entry : runtime->connections) connections.push_back(std::move(entry.second)); for (auto& entry : runtime->listeners) listeners.push_back(std::move(entry.second)); runtime->streams.clear(); runtime->connections.clear(); runtime->listeners.clear(); } // Bottom-up, and by the app's own hand. `RegistrationClose` blocks until // every child handle the app opened has been closed *by the app*, and a // silent shutdown delivers no shutdown events at all, so leaning on the // callbacks to do the closing deadlocks the caller forever. streams.clear(); for (auto& connection : connections) if (connection->handle != nullptr && !connection->handle_closed.load()) runtime->api->ConnectionShutdown( connection->handle, QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT, kConnectionCloseCode); connections.clear(); listeners.clear(); if (runtime->client_configuration != nullptr) { runtime->api->ConfigurationClose(runtime->client_configuration); runtime->client_configuration = nullptr; } // No callback can be running past this point. if (runtime->registration != nullptr) { runtime->api->RegistrationClose(runtime->registration); runtime->registration = nullptr; } if (runtime->api != nullptr) { MsQuicClose(runtime->api); runtime->api = nullptr; } delete runtime; } CULTMESH_API int32_t cultmesh_quic_listener_open( void* handle, const char* host, uint16_t port, const uint8_t* pkcs12, int32_t pkcs12_len, const char* password, uint64_t* out_listener_id, uint16_t* out_bound_port) { auto* runtime = static_cast(handle); if (runtime == nullptr || pkcs12 == nullptr || pkcs12_len <= 0 || out_listener_id == nullptr) return -1; CallScope scope(runtime); if (!scope.entered()) return -1; *out_listener_id = 0; if (out_bound_port != nullptr) *out_bound_port = 0; // Held here until the listener is fully started. Every failure path below // simply returns: the destructor closes whatever was opened, and a partial // open therefore leaves neither a handle nor a map entry behind. auto listener = std::make_shared(); listener->runtime = runtime; listener->id = NextId(runtime); QUIC_SETTINGS settings{}; FillSettings(settings); QUIC_BUFFER alpn = AlpnBuffer(); auto status = runtime->api->ConfigurationOpen( runtime->registration, &alpn, 1, &settings, sizeof(settings), nullptr, &listener->configuration); if (QUIC_FAILED(status)) { return Refuse(runtime, status, "CultMesh QUIC listener configuration failed (status=" + Hex(static_cast(status)) + ")."); } // PKCS12 is the one credential type Schannel and OpenSSL both take, which // is why the provider ships a PKCS12 rather than a platform store handle. QUIC_CERTIFICATE_PKCS12 pkcs12_certificate{}; pkcs12_certificate.Asn1Blob = pkcs12; pkcs12_certificate.Asn1BlobLength = static_cast(pkcs12_len); pkcs12_certificate.PrivateKeyPassword = (password != nullptr && *password != '\0') ? password : nullptr; QUIC_CREDENTIAL_CONFIG credentials{}; credentials.Type = QUIC_CREDENTIAL_TYPE_CERTIFICATE_PKCS12; credentials.CertificatePkcs12 = &pkcs12_certificate; credentials.Flags = QUIC_CREDENTIAL_FLAG_NONE; status = runtime->api->ConfigurationLoadCredential(listener->configuration, &credentials); if (QUIC_FAILED(status)) { return Refuse(runtime, status, "CultMesh QUIC listener certificate could not be loaded (status=" + Hex(static_cast(status)) + ")."); } Listener* raw = listener.get(); status = runtime->api->ListenerOpen(runtime->registration, ListenerCallback, raw, &raw->handle); if (QUIC_FAILED(status)) { return Refuse(runtime, status, "CultMesh QUIC listener open failed (status=" + Hex(static_cast(status)) + ")."); } QUIC_ADDR address{}; QuicAddrSetFamily(&address, QUIC_ADDRESS_FAMILY_UNSPEC); QuicAddrSetPort(&address, port); if (host != nullptr && *host != '\0' && !QuicAddrFromString(host, port, &address)) { SetError(runtime, "CultMesh QUIC listener host is not an IP address."); return -2; } // In the map before it starts accepting, because the callback that accepts // an inbound connection reads the listener through it. { std::lock_guard lock(runtime->gate); runtime->listeners.emplace(raw->id, listener); } QUIC_BUFFER listener_alpn = AlpnBuffer(); status = runtime->api->ListenerStart(raw->handle, &listener_alpn, 1, &address); if (QUIC_FAILED(status)) { Detach(runtime->gate, runtime->listeners, raw->id); return Refuse(runtime, status, "CultMesh QUIC listener start failed (status=" + Hex(static_cast(status)) + ")."); } // Port 0 means "pick one"; the host needs the chosen port to advertise it. QUIC_ADDR bound{}; uint32_t bound_size = sizeof(bound); status = runtime->api->GetParam(raw->handle, QUIC_PARAM_LISTENER_LOCAL_ADDRESS, &bound_size, &bound); if (QUIC_FAILED(status)) { Detach(runtime->gate, runtime->listeners, raw->id); return Refuse(runtime, status, "CultMesh QUIC listener address could not be read (status=" + Hex(static_cast(status)) + ")."); } if (out_bound_port != nullptr) *out_bound_port = QuicAddrGetPort(&bound); *out_listener_id = raw->id; return 0; } CULTMESH_API void cultmesh_quic_listener_close(void* handle, uint64_t listener_id) { auto* runtime = static_cast(handle); if (runtime == nullptr) return; CallScope scope(runtime); if (!scope.entered()) return; // Taken out of the map, then released here: ~Listener closes the handle // (which is what produces event 9) and its configuration, exactly once. [[maybe_unused]] const auto detached = Detach(runtime->gate, runtime->listeners, listener_id); } CULTMESH_API int32_t cultmesh_quic_connection_open( void* handle, const char* host, uint16_t port, uint64_t* out_connection_id) { auto* runtime = static_cast(handle); if (runtime == nullptr) return -1; CallScope scope(runtime); if (!scope.entered()) return -1; return OpenConnection(runtime, host, port, out_connection_id, nullptr); } CULTMESH_API int32_t cultmesh_quic_connection_certificate_complete( void* handle, uint64_t connection_id, int32_t accept) { auto* runtime = static_cast(handle); if (runtime == nullptr) return -1; CallScope scope(runtime); if (!scope.entered()) return -1; const auto connection = FindConnection(runtime, connection_id); if (connection == nullptr) return -1; const auto status = runtime->api->ConnectionCertificateValidationComplete( connection->handle, accept != 0 ? static_cast(1) : static_cast(0), accept != 0 ? QUIC_TLS_ALERT_CODE_SUCCESS : QUIC_TLS_ALERT_CODE_BAD_CERTIFICATE); if (QUIC_FAILED(status)) { return Refuse(runtime, status, "CultMesh QUIC certificate validation completion failed (status=" + Hex(static_cast(status)) + ")."); } return 0; } CULTMESH_API void cultmesh_quic_connection_shutdown(void* handle, uint64_t connection_id, uint64_t code) { auto* runtime = static_cast(handle); if (runtime == nullptr) return; CallScope scope(runtime); if (!scope.entered()) return; const auto connection = FindConnection(runtime, connection_id); if (connection == nullptr || connection->closing.exchange(true)) return; // Kept for the shutdown-complete callback: this is the code the initiator's // own event 4 carries, matching the one the peer sees. connection->shutdown_code.store(code); if (connection->handle != nullptr) runtime->api->ConnectionShutdown(connection->handle, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, code); } CULTMESH_API int32_t cultmesh_quic_stream_open( void* handle, uint64_t connection_id, uint8_t kind, uint64_t* out_stream_id) { auto* runtime = static_cast(handle); if (runtime == nullptr || out_stream_id == nullptr) return -1; CallScope scope(runtime); if (!scope.entered()) return -1; *out_stream_id = 0; if (kind != kReliableStream && kind != kLatestOnlyStream) return -2; const auto connection = FindConnection(runtime, connection_id); if (connection == nullptr) return -1; auto stream = std::make_shared(); stream->runtime = runtime; stream->owner = connection; stream->kind = kind; stream->has_kind = true; stream->id = NextId(runtime); Stream* raw = stream.get(); auto status = runtime->api->StreamOpen( connection->handle, QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL, StreamCallback, raw, &raw->handle); if (QUIC_FAILED(status)) { return Refuse(runtime, status, "CultMesh QUIC stream open failed (status=" + Hex(static_cast(status)) + ")."); } // In the map before it starts. `StreamStart` can reach shutdown-complete on // an MsQuic worker before it returns here, and that callback has to find the // stream to report it and to drop the map's reference. { std::lock_guard lock(runtime->gate); runtime->streams.emplace(raw->id, stream); } status = runtime->api->StreamStart(raw->handle, QUIC_STREAM_START_FLAG_IMMEDIATE); if (QUIC_FAILED(status)) { DestroyStream(runtime, raw->id); return Refuse(runtime, status, "CultMesh QUIC stream start failed (status=" + Hex(static_cast(status)) + ")."); } // The kind byte is the first thing on the stream, which is what the peer's // reassembly reads before any length prefix. auto* request = new SendRequest(); request->bytes.assign(1, kind); request->buffer.Buffer = request->bytes.data(); request->buffer.Length = 1; status = runtime->api->StreamSend(raw->handle, &request->buffer, 1, QUIC_SEND_FLAG_NONE, request); if (QUIC_FAILED(status)) { delete request; return Refuse(runtime, status, "CultMesh QUIC stream kind could not be sent (status=" + Hex(static_cast(status)) + ")."); } *out_stream_id = raw->id; return 0; } CULTMESH_API int32_t cultmesh_quic_stream_send_frame( void* handle, uint64_t stream_id, const uint8_t* encoded_frame, int32_t length, int32_t fin) { auto* runtime = static_cast(handle); if (runtime == nullptr || encoded_frame == nullptr || length <= 0) return -1; CallScope scope(runtime); if (!scope.entered()) return -1; if (static_cast(length) > kMaximumEncodedFrameBytes) return -2; // The reference is held for the whole `StreamSend` below, so the worker // cannot free the stream or close its handle underneath this call. const auto stream = FindStream(runtime, stream_id); if (stream == nullptr || stream->handle == nullptr) return -1; // The bytes are copied because MsQuic reads them after this returns; the // 4-byte LE length prefix is the QUIC stream framing, and it is added here // so that framing lives in one place for both directions. auto* request = new SendRequest(); request->is_frame = true; request->bytes.resize(sizeof(uint32_t) + static_cast(length)); WriteUInt32LittleEndian(request->bytes.data(), static_cast(length)); std::memcpy(request->bytes.data() + sizeof(uint32_t), encoded_frame, static_cast(length)); request->buffer.Buffer = request->bytes.data(); request->buffer.Length = static_cast(request->bytes.size()); const auto flags = (fin != 0) ? QUIC_SEND_FLAG_FIN : QUIC_SEND_FLAG_NONE; const auto status = runtime->api->StreamSend(stream->handle, &request->buffer, 1, flags, request); if (QUIC_FAILED(status)) { delete request; return Refuse(runtime, status, "CultMesh QUIC frame send failed (status=" + Hex(static_cast(status)) + ")."); } return 0; } CULTMESH_API void cultmesh_quic_stream_shutdown(void* handle, uint64_t stream_id, uint64_t code) { auto* runtime = static_cast(handle); if (runtime == nullptr) return; CallScope scope(runtime); if (!scope.entered()) return; const auto stream = FindStream(runtime, stream_id); if (stream == nullptr || stream->handle == nullptr) return; runtime->api->StreamShutdown(stream->handle, QUIC_STREAM_SHUTDOWN_FLAG_ABORT, code); } // The one blocking crossing; see section 6 of the header for its returns. The // call scope is what `cultmesh_quic_runtime_close` waits on: a poller blocked // here is woken by `closing` and counted out before anything is freed. CULTMESH_API int32_t cultmesh_quic_next_event( void* handle, int32_t timeout_ms, cultmesh_quic_event* out_event, uint8_t* payload, int32_t payload_capacity, int32_t* out_required) { auto* runtime = static_cast(handle); if (runtime == nullptr || out_event == nullptr || out_required == nullptr || payload_capacity < 0) return -1; // A null buffer with a non-zero capacity is a lie about the buffer. Taking // it meant the event was consumed and its payload silently dropped. if (payload == nullptr && payload_capacity > 0) return -1; CallScope scope(runtime); if (!scope.entered()) return -1; *out_required = 0; std::unique_lock lock(runtime->gate); if (runtime->events.empty() && timeout_ms > 0) { // The host's timeout bounds the wait; an event or the close ends it // sooner, and `woken` is which of the two happened. RECORD_WAIT wraps // this exact argument; see the development seam note near its macros. const bool woken = runtime->signal.wait_for( lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)), [runtime] { return !runtime->events.empty() || runtime->closing; }); // Development builds only; a no-op unless a scenario has armed the hold. // A held call returns the same 0 an empty queue would have, and makes no // further call of its own. Only a woken call is held: one whose own // timeout expired is on its way out, and parking it would let a scenario // report a call the bridge kept when what kept it was the fixture. if (woken && CULTMESH_QUIC_DEBUG_HELD(lock)) return 0; } if (runtime->events.empty()) return 0; const QueuedEvent& queued = runtime->events.front(); const auto required = static_cast(queued.payload.size()); if (required > payload_capacity) { *out_required = required; return 2; } *out_event = queued.header; if (required > 0 && payload != nullptr) std::memcpy(payload, queued.payload.data(), static_cast(required)); *out_required = required; runtime->events.pop_front(); return 1; } CULTMESH_API int32_t cultmesh_quic_last_error(void* handle, char* destination, int32_t capacity) { auto* runtime = static_cast(handle); if (runtime == nullptr || destination == nullptr || capacity <= 0) return -1; CallScope scope(runtime); if (!scope.entered()) return -1; std::lock_guard lock(runtime->gate); const size_t count = (std::min)(runtime->error.size(), static_cast(capacity - 1)); std::memcpy(destination, runtime->error.data(), count); destination[count] = '\0'; return static_cast(count); } CULTMESH_API int32_t cultmesh_quic_last_status(void* handle) { auto* runtime = static_cast(handle); if (runtime == nullptr) return 0; CallScope scope(runtime); if (!scope.entered()) return 0; std::lock_guard lock(runtime->gate); return runtime->last_status; } // --------------------------------------------------------------------------- // C ABI v1: the Unity shim. Windows only, by the same construction as its only // caller (`CultMeshNativeQuicRealtimeTransport.CanConnect` is Windows-gated). // // These five exports are the contract the shipped Unity package imports by // name. They are preserved byte for byte in signature and behaviour, and they // decide nothing the v2 path does not: a v1 client is a private runtime with one // connection. The single exception is the pin, which v1 compares here in C // because that is what the managed connector was built against. // --------------------------------------------------------------------------- #if defined(_WIN32) #include #include #include namespace { enum class ClientState : int32_t { Connecting = 0, Connected = 1, Failed = 2, Closed = 3, }; struct V1Client { Runtime* runtime = nullptr; uint64_t connection_id = 0; ClientState state = ClientState::Connecting; std::deque> frames; std::string error; bool closing = false; // The managed connector polls `state` and `poll` from its own loop and may // read `error` from the caller's thread, so the drain and everything it // mutates sit behind one lock. Held only around `Pump` and the fields; it is // always taken before the runtime's, never after. std::mutex gate; }; std::string LoadedMsQuicPath() { const auto module = GetModuleHandleW(L"msquic.dll"); if (module == nullptr) return "unresolved"; std::array path{}; const auto length = GetModuleFileNameW(module, path.data(), static_cast(path.size())); if (length == 0 || length >= path.size()) return "unresolved"; const auto required = WideCharToMultiByte( CP_UTF8, 0, path.data(), static_cast(length), nullptr, 0, nullptr, nullptr); if (required <= 0) return "unresolved"; std::string utf8(static_cast(required), '\0'); WideCharToMultiByte( CP_UTF8, 0, path.data(), static_cast(length), utf8.data(), required, nullptr, nullptr); return utf8; } bool ParseHexNibble(char value, uint8_t& nibble) { if (value >= '0' && value <= '9') nibble = static_cast(value - '0'); else if (value >= 'a' && value <= 'f') nibble = static_cast(value - 'a' + 10); else if (value >= 'A' && value <= 'F') nibble = static_cast(value - 'A' + 10); else return false; return true; } bool ParsePin(const char* value, std::array& pin) { if (value == nullptr || std::strlen(value) != 64) return false; for (size_t index = 0; index < pin.size(); ++index) { uint8_t high = 0; uint8_t low = 0; if (!ParseHexNibble(value[index * 2], high) || !ParseHexNibble(value[(index * 2) + 1], low)) return false; pin[index] = static_cast((high << 4) | low); } return true; } // Drains the runtime's event queue into the v1 client's own shape: frames for // `poll`, a state for `state`, an error string for `error`. This is where the // v1 contract is reconstructed from v2 events, and the only place it is. void Pump(V1Client* client) { cultmesh_quic_event header{}; std::vector payload; for (;;) { int32_t required = 0; auto result = cultmesh_quic_next_event( client->runtime, 0, &header, payload.empty() ? nullptr : payload.data(), static_cast(payload.size()), &required); if (result == 2) { payload.resize(static_cast(required)); continue; } if (result != 1) break; switch (header.type) { case kEventConnectionConnected: if (client->state == ClientState::Connecting) client->state = ClientState::Connected; break; case kEventStreamFrame: client->frames.emplace_back(payload.begin(), payload.begin() + header.payload_length); break; case kEventConnectionShutdown: if (client->closing) { client->state = ClientState::Closed; } else if (client->state != ClientState::Failed) { client->state = ClientState::Failed; if (client->error.empty()) { char runtime_error[1024]{}; cultmesh_quic_last_error(client->runtime, runtime_error, sizeof(runtime_error)); // The pin refusal is the runtime's, and the managed // connector matches on its wording; anything else is the // transport's own reason plus where msquic came from. client->error = runtime_error[0] != '\0' ? std::string(runtime_error) : std::string(reinterpret_cast(payload.data()), static_cast(header.payload_length)); client->error += " (msquic=" + LoadedMsQuicPath() + ")"; } } break; default: break; } } } bool CertificateMatchesPin(Connection* connection, QUIC_CERTIFICATE* certificate) { if (certificate == nullptr) return false; const auto* encoded = reinterpret_cast(certificate); if (encoded->Buffer == nullptr || encoded->Length == 0) return false; BYTE digest[32]{}; DWORD digest_length = sizeof(digest); if (!CryptHashCertificate2( BCRYPT_SHA256_ALGORITHM, 0, nullptr, encoded->Buffer, encoded->Length, digest, &digest_length) || digest_length != sizeof(digest)) return false; return std::memcmp(digest, connection->pin.data(), sizeof(digest)) == 0; } } // namespace CULTMESH_API int32_t cultmesh_quic_open( const char* host, uint16_t port, const char* certificate_sha256, V1Client** result) { if (host == nullptr || *host == '\0' || port == 0 || result == nullptr) return -1; *result = nullptr; std::array pin{}; if (!ParsePin(certificate_sha256, pin)) return -2; auto client = std::make_unique(); void* runtime_handle = nullptr; const auto opened = cultmesh_quic_runtime_open("GameCult.Mesh.Quic.Native", &runtime_handle); if (opened != 0) return opened; client->runtime = static_cast(runtime_handle); // The pin goes in before the connection starts; see OpenConnection. const auto status = OpenConnection(client->runtime, host, port, &client->connection_id, &pin); if (status != 0) { cultmesh_quic_runtime_close(client->runtime); return status; } *result = client.release(); return 0; } CULTMESH_API int32_t cultmesh_quic_state(V1Client* client) { if (client == nullptr) return static_cast(ClientState::Failed); std::lock_guard lock(client->gate); Pump(client); return static_cast(client->state); } CULTMESH_API int32_t cultmesh_quic_poll( V1Client* client, uint8_t* destination, int32_t destination_length, int32_t* required_length) { if (client == nullptr || required_length == nullptr || destination_length < 0) return -1; std::lock_guard lock(client->gate); Pump(client); if (client->frames.empty()) { *required_length = 0; return client->state == ClientState::Failed ? -2 : 0; } const auto& frame = client->frames.front(); if (frame.size() > static_cast(INT32_MAX)) return -3; *required_length = static_cast(frame.size()); if (destination == nullptr || destination_length < *required_length) return 2; std::memcpy(destination, frame.data(), frame.size()); client->frames.pop_front(); return 1; } CULTMESH_API int32_t cultmesh_quic_error(V1Client* client, char* destination, int32_t destination_length) { if (client == nullptr || destination == nullptr || destination_length <= 0) return -1; std::lock_guard lock(client->gate); Pump(client); const size_t count = (std::min)(client->error.size(), static_cast(destination_length - 1)); std::memcpy(destination, client->error.data(), count); destination[count] = '\0'; return static_cast(count); } CULTMESH_API void cultmesh_quic_close(V1Client* client) { if (client == nullptr) return; { std::lock_guard lock(client->gate); client->closing = true; } // `runtime_close` already shuts the connection down silently with the same // CULT close code and then closes the handle, which is what v1 did before // it delegated. Shutting it down here as well would only race that. cultmesh_quic_runtime_close(client->runtime); client->state = ClientState::Closed; delete client; } #endif // _WIN32 #if defined(CULTMESH_QUIC_DEBUG_ASSERTS) // The development seam's three exports. They exist only in a build configured // with CULTMESH_QUIC_DEBUG_ASSERTS; a shipped library exports neither these nor // the counters behind them. See section 7 of the header. CULTMESH_API void cultmesh_quic_debug_hold_calls(int32_t hold) { if (hold != 0) { debug_peak_calls.store(0); debug_calls_at_close.store(-1); } { std::lock_guard lock(debug_hold_gate); debug_hold_armed = hold != 0; } debug_hold_signal.notify_all(); } CULTMESH_API int32_t cultmesh_quic_debug_peak_calls(void) { return debug_peak_calls.load(); } CULTMESH_API int32_t cultmesh_quic_debug_calls_at_close(void) { return debug_calls_at_close.load(); } CULTMESH_API int32_t cultmesh_quic_debug_last_wait_ms(void) { return debug_wait_recorded.load() ? debug_last_wait_ms.load() : -1; } CULTMESH_API void cultmesh_quic_debug_reset_last_wait_ms(void) { debug_wait_recorded.store(false); debug_last_wait_ms.store(-1); } #endif