Skip to content

base

The generic utilities every other module may use. Namespace core, headers directly in src/core/, target core::base. It builds everywhere core-cpp does, single-threaded WebAssembly included, and links Threads::Threads except there.

Imported from contour's src/crispy at 6777ff05, with crispy's namespace renamed core, from fastcached's src/FastCache/Core at ee71f868, and Generator.hpp from endo's src/platform/Generator.hpp at f774a210.

Header What it has
<core/Assert.hpp> Require() and Guarantee(), which report through a replaceable fail handler (core::setFailHandler()) and abort; core::todo() and core::unreachable()
<core/Base64.hpp> core::base64::encode() and decode(), batch and streaming
<core/Config.hpp> generated by the configure: the version and the CORE_CPP_WITH_* switches, each 0 or 1
<core/Deferred.hpp> core::Deferred<T>, a value initialised once, after its owner
<core/Defines.hpp> CORE_PACKED, CORE_REQUIRES, CORE_CONSTEVAL, CORE_CONSTEXPR
<core/Environment.hpp> core::Environment, the injectable reader of environment variables; LiveEnvironment, CachingEnvironment, defaultEnvironment(); setProcessEnvironmentVariable() and unsetProcessEnvironmentVariable(), the writer
<core/Escape.hpp> core::escape(), unescape() and escapeMarkdown() for byte strings
<core/FNV.hpp> core::FNV, the FNV-1a hash
<core/Flags.hpp> core::Flags<Enum>, a type-safe set of bit flags, and its std::formatter
<core/Generator.hpp> core::Generator<T>, a lazy, single-pass coroutine range; see Generator below
<core/Overloaded.hpp> core::Overloaded, lambdas combined into one visitor
<core/Profiling.hpp> the CORE_ZONE_*, CORE_FRAME_MARK*, CORE_THREAD_NAME and CORE_PLOT macros; see Profiling
<core/Ranges.hpp> core::findOrNull() and findIfOrNull(), which answer a pointer; core::ranges::Iota and FoldLeft, the standard facility where the library has it and a fallback where it does not
<core/Times.hpp> core::times(), a counted range to iterate or to pipe a callable into
<core/UserInfo.hpp> the calling user's password-database entry (getpwuid_r()); none on Windows or under Emscripten
<core/Utils.hpp> string splitting, trimming, joining and case mapping, toInteger(), hex strings, ${VAR} substitution, nextPowerOfTwo(), views::enumerate(), threadName()

Generator

<core/Generator.hpp> has core::Generator<T>, a lazy, single-pass range a coroutine fills with co_yield, imported from endo (src/platform/Generator.hpp at f774a210). It moved here from core::async in Task A5b: core::async::Generator would read as an asynchronous, co_await-able stream, and this one is synchronous, needing nothing but the standard library and the compiler's coroutine language support — the one header here that does.

  • It is std::generator<T> where the standard library has <generator> and is not libstdc++, and otherwise core::detail::GeneratorFallback<T>, a small implementation over <coroutine>. libc++ has no <generator> yet (emsdk 3.1.56 ships libc++ 17), and GCC 14 reports a null coroutine_handle inside libstdc++'s own std::generator at -O2 (-Wnull-dereference), which the zero-warning policy makes an error. In practice MSVC uses std::generator and everything else the fallback.
  • The choice is read from <version>, which the header includes first, so every translation unit makes the same one. endo's copy tested __cpp_lib_generator before including anything, so the answer depended on what a file included first; a virtual function returning a Generator could then have two return types in one program.
  • CORE_GENERATOR_FORCE_FALLBACK, defined the same way in every translation unit, selects the fallback everywhere.
  • The fallback is always defined and is tested on every platform. Each yielded value lives in the coroutine frame until the next increment, so yield owning values. Breaking out of the loop destroys the suspended frame.

Assertions that log

fatal() and SoftRequire() were part of crispy's Assert.hpp. They report through the log store, which core::base may not depend on, so they are in log: <core/log/Assert.hpp>, as core::log::fatal() and the SoftRequire() macro.

The environment seam

Code that reads an environment variable takes a core::Environment const& rather than calling getenv(), so a test hands it a core::testing::FakeEnvironment (<core/testing/Environment.hpp>, in testing) instead of changing the process's environment. core::platform::EnvironmentProvider (platform) is a second such seam; core-cpp#7 merges them.

A process that must change its own environment, because a child process inherits it, calls core::setProcessEnvironmentVariable() and core::unsetProcessEnvironmentVariable(), never setenv(). On POSIX they never edit a block a reader may be walking: they publish a new environ block with one store, under the lock LiveEnvironment reads under, and never free what they published, because getenv() in another library reads without that lock. Each write that changes something so costs one block of pointers, which suits the few writes a process makes to its own environment; a write that changes nothing publishes nothing. On Windows they are SetEnvironmentVariableA(), which the CRT's getenv() does not see; read through LiveEnvironment. Both refuse an empty name and a name with = (std::errc::invalid_argument). They take a lock and allocate, so they are not for use between fork() and exec(): give the child its environment through execve() instead.

Under Emscripten

All of core::base builds for single-threaded WebAssembly. There threadName() answers an empty string, the password database has no entries, and LiveEnvironment reads Emscripten's own environment, which under node is not the host's.

crispy's renderer-side half (ring buffers, LRU caches, the aligned allocator and the like) stays in contour: it has one consumer. A file moves into core-cpp when a second project needs it.