platform¶
The operating-system layer: clocks, wakeups, signals, pipes, the file system, the environment and
paths, each behind an interface with a test double. Namespace core::platform, directory
src/core/platform/, target core::platform. It links base (for Generator, among
other things), and ws2_32 on Windows.
Imported from the generic half of endo's src/platform at f774a210, with one clock merged from
endo's, contour's (src/net/platform/Clock.hpp at 6777ff05) and fastcached's
(src/FastCache/Core/Clock.hpp at b461e8b6), and SystemPipe merged with contour's copy.
endo's shell-specific platform code stays in endo: processes and the pipes to them
(Process, Pipe, WaitResult, ProcessProvider), the project file tree, install paths and
the interrupt throttle.
| Header | What it has |
|---|---|
<core/platform/Clock.hpp> |
the clock seams, see below |
<core/platform/Types.hpp> |
NativeHandle, InvalidHandle, ProcessId, the standard handles, platformRead()/platformWrite()/platformClose(), isTerminal(), nativeHandleToNumber() |
<core/platform/PlatformError.hpp> |
PlatformError, the error of this module's fallible operations, and toString() |
<core/platform/Wakeup.hpp> |
Wakeup, a signal one thread raises to wake another out of poll() or WaitForMultipleObjects(): an eventfd on Linux, a self-pipe on macOS and the BSDs, an event on Windows |
<core/platform/SystemPipe.hpp> |
createSystemPipe(): an in-process byte channel whose read end an event loop can wait on, on every platform; ChannelResult, what one read of it produced |
<core/platform/WinsockInit.hpp> |
ensureWinsockInitialized(), once per process; a no-op off Windows |
<core/platform/SignalHandler.hpp> |
SignalHandler: SIGCHLD, SIGTSTP, SIGCONT and SIGINT through signalfd on Linux and handlers elsewhere, Ctrl+C and Ctrl+Break on Windows, and an optional Wakeup to raise on an interrupt |
<core/platform/MessageQueue.hpp> |
MessageQueue<T>, a thread-safe queue that can raise a Wakeup on every push |
<core/platform/FileSystem.hpp>, <core/platform/NativeFileSystem.hpp> |
the FileSystem interface, errors as std::expected, a lazy recursive walk as a core::Generator; NativeFileSystem over std::filesystem |
<core/platform/FileInfoProvider.hpp> |
FileInfoProvider, a directory listing with stat(2) metadata (FileEntry), a single file or a glob pattern |
<core/platform/EnvironmentProvider.hpp> |
EnvironmentProvider: variables with a set-then-export model, the working directory, homeDirectory(), userName(), configHome() |
<core/platform/UserPaths.hpp> |
homeDirectory() and configHome() over a core::Environment, by default the process environment |
<core/platform/PathUtils.hpp> |
path spelling: normalizePath(), joinPath(), absolutePath(), canonicalCasePath(), stripTrailingSeparator(), isCaseOnlyRename(), resolveDevicePath() |
<core/platform/GlobMatch.hpp> |
globMatchFilename() (*, ?, [...]) and containsGlobChars() |
<core/platform/FileUri.hpp> |
RFC 3986 percent-encoding and RFC 8089 file:// URIs |
<core/platform/SystemInfo.hpp> |
hostName() and cachedHostName() |
<core/platform/StringUtils.hpp> |
trimInPlace() |
The test doubles are in testing/ and in core::platform::testing:
testing::InMemoryFileSystem (a FileSystem held in maps, with symlinks, permissions and
refused paths), testing::MockFileInfoProvider and testing::TestEnvironmentProvider, which never
touches the process environment. The native implementations of FileInfoProvider and
EnvironmentProvider are in the private posix/ and windows/ directories, which no consumer
includes; a composition root gets them from nativeEnvironmentProvider() and
nativeFileInfoProvider(), each a std::unique_ptr to the interface:
| Factory | Windows | Linux, macOS, the BSDs | Emscripten |
|---|---|---|---|
nativeEnvironmentProvider() (<core/platform/EnvironmentProvider.hpp>) |
GetEnvironmentVariableA/SetEnvironmentVariableA, names case-insensitive |
the POSIX provider: reads through core::LiveEnvironment, exports through core::setProcessEnvironmentVariable() |
the POSIX provider, over the environment Emscripten's libc keeps for the module (under node a fixed default set, not the host's) |
nativeFileInfoProvider() (<core/platform/FileInfoProvider.hpp>) |
std::filesystem: the read-only flag as permissions, no blocks, device or inode |
the POSIX provider: lstat(2) for every field, symlinks as links with their targets, and the blocks, device and inode |
the POSIX provider, over Emscripten's virtual filesystem; a relative symlink target reads resolved against the link's directory there (3.1.56 at least) |
The POSIX file-info provider was endo's LinuxFileInfoProvider, which used nothing Linux-specific;
it is one implementation, PosixFileInfoProvider, for every POSIX system. Each call makes a new
provider.
Clocks¶
Logic that schedules against a deadline takes an IClock& rather than calling
std::chrono::steady_clock::now(), so a test can drive time.
IClockanswersnow(), aSteadyTimePoint. Its virtualrefresh()does nothing by default; an event loop calls it at fixed points of each turn (after the blocking wait returns, and before it computes the next timeout), which is what makes a caching clock correct.SteadyClockreads the OS clock on every call.defaultSteadyClock()is a process-wide one, for default arguments.CachedClockwraps another clock and answers the sample its lastrefresh()took, so a turn that reads the clock a thousand times pays for one read. Several loops may share one; it never moves backwards.ManualClockmoves only onadvance()andsetNow().IWallClock,SystemWallClock,ManualWallClockanddefaultSystemWallClock()are the same seam forstd::chrono::system_clock, for inputs that are wall-clock instants (an absolute expiry, a log line's date). Internal scheduling never reads the wall clock.WallClockRefis how a type keeps a borrowedIWallClock: it binds to a named clock and refuses a temporary, and because it is carried by value the refusal survives a forwarding constructor, which a deletedT(IWallClock const&&)overload does not (fastcached#1028).
Behaviour worth knowing¶
SystemPipenever blocks. On POSIX both ends are non-blocking and close-on-exec. A write that the full channel refuses reports success, because the bytes already pending wake the reader just as well. On Windows the channel is a loopback TCP pair whose read end is mapped to a waitable event.SystemPipe::read()tells three outcomes apart, in aChannelResult, before any failure: the bytes it read (bytesRead()), nothing yet (empty(): the channel is empty and the writer still there, so wait for readiness and read again) and the end of the stream (isEndOfStream(): the writer has closed and every byte it wrote has been read). Only a read that fails is aPlatformError(IoError), so a loop draining the channel never mistakes an empty channel for a broken one, or a closed writer for either.Types.hppdoes not include<Windows.h>.NativeHandleisvoid*andProcessIdunsigned longthere, and the calls into the Windows API are out of line. endo's copy definedSTDIN_FILENOand theSIG*numbers on Windows for its process code; core-cpp's does not.- The process environment is written in one place.
PosixEnvironmentProviderexports throughcore::setProcessEnvironmentVariable()(in base), neversetenv(). UserPathsreads through acore::Environment, so a test passes acore::testing::FakeEnvironment. Its default argument is acore::LiveEnvironment, sohomeDirectory()andconfigHome()called without one read the process environment (on Windows the operating system's block) through the same body the tests run.Wakeup's constructor throwsstd::runtime_errorwhen the operating system refuses the eventfd, self-pipe or event, which it does only when descriptors, handles or kernel memory are exhausted. That is unrecoverable, since no event loop can run without its wakeup channel, so it throws rather than returning an error, as the exceptions rule allows.
Under Emscripten¶
The row in the module table says PLATFORMS wasm-subset. Under single-threaded Emscripten only
Types, PlatformError, Clock, StringUtils, PathUtils, GlobMatch, FileUri and the POSIX
EnvironmentProvider and FileInfoProvider (behind nativeEnvironmentProvider() and
nativeFileInfoProvider()) build (the SOURCES_EMSCRIPTEN list), and their tests run under
node. Clock needs no threads; its test of
concurrent CachedClock refreshes is compiled only where threads exist. There is no separate
NativeHandle.hpp: NativeHandle is part of Types.hpp, as it is in endo.
A pipe has no end of file there. platformRead() is read(2), which elsewhere answers 0
for a drained pipe whose writer has closed. Emscripten's pipes behave as if the read end were
always non-blocking, so a read of an empty pipe fails with EAGAIN whether the writer is still
there or not: the drained pipe reads -1 with errno EAGAIN, never 0, and nothing tells it from a
pipe that is only empty for now. Code that must also run there learns that the writer is done
some other way, such as a length sent first, a terminator, or the writer's own completion. The
test of that case checks each platform's answer, 0 natively and -1 with EAGAIN under Emscripten.