core-cpp 0.3.0
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
withoutcancelRead()or awaiting the old one. Installcore::setFailHandlerto 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.
- Migration: audit every path that re-arms a read or write on a socket without the previous
- 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.
- Migration: code written against 0.2.1 that relied on a flow queued ahead of a readiness
IHostScheduler::callAftermust deliver every request it accepts exactly once, because
HostDrivenBackendnow 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.
- Migration: a host that dropped pending callbacks at shutdown leaks one small ticket per
testing::ManualHostScheduleris neither copyable nor movable, andclear()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.
- Migration: hold one per test by value or by reference, and destroy it after the backends it
Added
-
core::net::contract::SlotDirection,contract::secondOperationArmed()and
contract::describeHandle()in<core/net/SocketContract.hpp>, and an optional handle argument
(plus a defaultedstd::source_location) oncontract::claimReadSlotand
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)andtarget_link_libraries(app PRIVATE core::net), the same names as a source
build's aliases. Every module target is installed with itsHEADERSfile set (the generated
core/Config.hppincluded) in the install componentcore-cpp, with a
core-cppConfigVersion.cmakethat isSameMinorVersionwhile core-cpp is 0.x. The package
config callsfind_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, defaultPROJECT_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'sdetail/ReadyBatch.hppanddetail/ScopeGuard.hppjoined itsHEADERSfile
set:EventLoop.hppandtesting/ScriptedBackend.hppinclude them, so the installed headers
could not compile without them.core::tuilinks stb_image as$<BUILD_INTERFACE:...>, and
core::testing_mainnames its dialog object through the installedcore::testing_dialogs.core-cpp.installinstalls 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) withCORE_CPP_INSTALLon and off.
Fixed
-
core::testing::suppressWindowsDialogs()keeps abort()'s message and turns off Windows Error
Reporting's UI. It cleared_WRITE_ABORT_MSGwith_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;WerGetFlagsconfirms the flag is set), for an unhandled structured exception in a
process whose error mode was reset (found by morph).windows-dialog-canary.abortnow 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 toEventLoop::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'sAbandonIfPeerGone-- 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).resumeSoonfrom 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, andcancelPendingfinds 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::spawnunlinked a finished flow by the frame its ready entry
named, and a flow parked inside a sub-task (co_await leaf(), withleafon 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 inspawnedCount()-- until~EventLoop, on
normal completion and onrequestStopalike. A long-lived loop spawning one flow per connection
grew without bound (found by the contour migration, measured on 0.2.1).spawnnow 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 (afterco_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::claimWriteSlotand the watch slots of
EventLoop::registerParkenforced it withassertalone: underNDEBUGthe 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 throughcore::detail::fail
(so a program'score::setFailHandlerlogs it first), naming the direction and the socket's
native handle, in Debug and Release alike.claimReadSlotandclaimWriteSlottake the handle
as an optional second argument. Thesocket-contract-canaryslot 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.
- Migration: a caller that armed a second read or write over a parked one was already broken; in
-
A host-driven loop may be destroyed while a pump is out with the host.
HostDrivenBackend
handedIHostScheduler::callAfterits own address, andemscripten_async_callcannot be
retracted: aPlatformLoopdestroyed under Emscripten with a timer armed, or after any off-turn
addTimer,postor 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.IHostSchedulerstates 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::ManualHostSchedulerdelivers whatever is still pending when it is destroyed,
including whatclear()took out, which is no longernoexcept; it is no longer copyable or
movable, since a copy would deliver a ticket twice.