Skip to content

core-cpp 0.3.0

Choose a tag to compare

@github-actions github-actions released this 25 Sep 05:04
· 116 commits to master since this release

Breaking

Each of these is a defect fixed or a contract made explicit, and each changes what a caller can
observe; the migration is under each.

  • A second read or write armed over a parked one ends the process in every build (see
    Fixed). A Release process that used to hang there now aborts at the violation.
    • Migration: audit every path that re-arms a read or write on a socket without the previous
      operation having resolved -- in particular a retry or timeout path that starts a new read
      without cancelRead() or awaiting the old one. Install core::setFailHandler to route the
      message ("Socket contract violated: ...", naming the direction and the handle) to your logger,
      and to take a stack trace there before the abort.
  • A waiter completed by a drain-step callback resumes in the callback's position, not at the
    back of the ready queue as in 0.2.1 (see Fixed); 0.2.0's order, without resuming inline.
    • Migration: code written against 0.2.1 that relied on a flow queued ahead of a readiness
      callback running again -- after a yield -- BEFORE that callback's waiter now sees the waiter
      run first. Code written against 0.2.0 needs nothing.
  • IHostScheduler::callAfter must deliver every request it accepts exactly once, because
    HostDrivenBackend now hands it a ticket only the callback frees.
    • Migration: a host that dropped pending callbacks at shutdown leaks one small ticket per
      request dropped; deliver them (a late pump finds its backend gone and runs nothing) or accept
      the leak. A host that delivered one twice must stop.
  • testing::ManualHostScheduler is neither copyable nor movable, and clear() is no longer
    noexcept.
    Its destructor delivers what is still pending, cleared requests included.
    • Migration: hold one per test by value or by reference, and destroy it after the backends it
      serves -- declare it first.

Added

  • core::net::contract::SlotDirection, contract::secondOperationArmed() and
    contract::describeHandle()
    in <core/net/SocketContract.hpp>, and an optional handle argument
    (plus a defaulted std::source_location) on contract::claimReadSlot and
    contract::claimWriteSlot, which name it when they end the process. A transport outside
    core-cpp passes its own handle to get it in the message.

  • EventLoop::inboundFinishedRootCount(): how many spawned flows ended off the loop's thread
    and wait for the next turn to release them.

  • core-cpp installs as the CMake package core-cpp (core-cpp#5): find_package(core-cpp 0.3 CONFIG REQUIRED) and target_link_libraries(app PRIVATE core::net), the same names as a source
    build's aliases. Every module target is installed with its HEADERS file set (the generated
    core/Config.hpp included) in the install component core-cpp, with a
    core-cppConfigVersion.cmake that is SameMinorVersion while core-cpp is 0.x. The package
    config calls find_dependency() for exactly the dependency-table rows its installed targets
    link. A target that links a dependency the build fetched rather than found (libunicode, Catch2 or
    Tracy through CPM) cannot be re-found by an installed package and is left out, with a status line
    saying so. See docs/getting-started/install.md.

    • CORE_CPP_INSTALL, default PROJECT_IS_TOP_LEVEL: a vendoring or CPM consumer installs
      nothing of core-cpp's unless it asks. A parent that exports a target of its own linking
      core-cpp's turns it on, or CMake refuses the export as "not in any export set" (found by morph).
    • core::net's detail/ReadyBatch.hpp and detail/ScopeGuard.hpp joined its HEADERS file
      set: EventLoop.hpp and testing/ScriptedBackend.hpp include them, so the installed headers
      could not compile without them. core::tui links stb_image as $<BUILD_INTERFACE:...>, and
      core::testing_main names its dialog object through the installed core::testing_dialogs.
    • core-cpp.install installs the build under test into an empty prefix, builds and runs a
      consumer of the package (tests/consumer-install), checks that every core-cpp header an
      installed header includes was installed, and configures a parent exporting a target that links
      core-cpp's (tests/consumer-install-nested) with CORE_CPP_INSTALL on and off.

Fixed

  • core::testing::suppressWindowsDialogs() keeps abort()'s message and turns off Windows Error
    Reporting's UI.
    It cleared _WRITE_ABORT_MSG with _CALL_REPORTFAULT, so an aborting test
    printed nothing; only the fault report is off now, and the message goes to stderr, not a dialog.
    It also asks Windows Error Reporting for no UI, WerSetFlags(WER_FAULT_REPORTING_NO_UI) (in
    kernel32; WerGetFlags confirms the flag is set), for an unhandled structured exception in a
    process whose error mode was reset (found by morph). windows-dialog-canary.abort now requires the message in a Debug build;
    a Release UCRT writes none.

  • A waiter completed by a readiness callback resumes in the callback's position again. 0.2.1
    made a completion from a callback (ResultAwaitable::complete(), and anything a drain-step
    callback hands to EventLoop::resumeSoon) join the BACK of the ready queue, where 0.2.0 had
    resumed it inline. A flow queued ahead of the callback that yields once to let already-reported
    readiness run -- fastcached's AbandonIfPeerGone -- then resumed before the waiter and read stale
    state. The drain now puts what a callback queued at the front once the callback returns: the
    waiter resumes before anything queued after the callback, still in the drain step and never
    inside the callback (G2). resumeSoon from outside a drain-step callback stays FIFO. The
    callback's queue is a member reused across callbacks, so a readiness completion allocates
    nothing for it, and cancelPending finds a waiter a callback has queued.

  • A spawned flow that completes inside a sub-task is released, instead of leaking until the
    loop is destroyed.
    EventLoop::spawn unlinked a finished flow by the frame its ready entry
    named, and a flow parked inside a sub-task (co_await leaf(), with leaf on a socket read or a
    delay) is resumed through the sub-task's frame: it ran to its end inside that resume, by
    symmetric transfer, and stayed in the loop -- and in spawnedCount() -- until ~EventLoop, on
    normal completion and on requestStop alike. A long-lived loop spawning one flow per connection
    grew without bound (found by the contour migration, measured on 0.2.1). spawn now runs the
    flow inside a root coroutine owned by the loop -- one more frame allocation per spawn -- whose
    final suspension files it for release; the drain destroys it after the resume that finished it
    returns, in O(1) and on the loop's thread, whichever frame the resume named. A flow that ends on
    another thread (after co_await ResumeOn { pool }) hands itself over through the inbound queue
    and is released in the next turn's first step. A flow's exception ends it without being
    rethrown into the root.

  • A second operation armed over a parked one ends the process in every build, instead of hanging
    in Release.
    One read and one write operation per socket is the contract, and
    contract::claimReadSlot, contract::claimWriteSlot and the watch slots of
    EventLoop::registerPark enforced it with assert alone: under NDEBUG the second operation
    displaced the parked one, which was then never resumed -- a silent hang, and the leading suspect
    in a Release-only fastcached stall on 0.2.1. All three now terminate through core::detail::fail
    (so a program's core::setFailHandler logs it first), naming the direction and the socket's
    native handle, in Debug and Release alike. claimReadSlot and claimWriteSlot take the handle
    as an optional second argument. The socket-contract-canary slot modes, and two new ones for the
    loop's own slots (watch-read-slot, watch-write-slot), now run on the Release legs as well.

    • Migration: a caller that armed a second read or write over a parked one was already broken; in
      a Release build it now fails loudly at the violation instead of hanging later. IocpSocket's
      Release-only handling of an orphaned write, and the two tests that drove displacement under
      NDEBUG, are gone with the displacement.
  • A host-driven loop may be destroyed while a pump is out with the host. HostDrivenBackend
    handed IHostScheduler::callAfter its own address, and emscripten_async_call cannot be
    retracted: a PlatformLoop destroyed under Emscripten with a timer armed, or after any off-turn
    addTimer, post or wake, freed the backend it owns, and the browser's timer then wrote into
    it -- a heap-use-after-free (found by morph's timeout scheduler). Each pump now carries a small
    ticket holding a weak reference to the backend; a late pump finds it expired, runs nothing and
    frees the ticket. Coalescing is unchanged.

    • IHostScheduler states its contract: every request accepted is delivered exactly once, since
      its state may own storage only the callback frees. A host that drops a request leaks a ticket.
    • testing::ManualHostScheduler delivers whatever is still pending when it is destroyed,
      including what clear() took out, which is no longer noexcept; it is no longer copyable or
      movable, since a copy would deliver a ticket twice.