Skip to content

Remove the STL and the C runtime from base - #14

Merged
Force67 merged 17 commits into
devel5from
base/drop-stl-and-crt
Sep 15, 2026
Merged

Force67 merged 17 commits into
devel5from
base/drop-stl-and-crt

Conversation

@Force67

@Force67 Force67 commented Sep 15, 2026

Copy link
Copy Markdown
Owner

base said "no STL where possible" and then reached the standard library in 30 production files, and libc in another 25. This clears both out of every default build.

find_stl.py becomes find_runtime_deps.py and reports the C runtime on the same terms as the STL. It now finds no removable CRT, and 0.05% removable STL, all of it inside the BASE_USE_STD_ATOMIC and BASE_USE_STD_MUTEX branches that nothing enables.

STL

Was Now
<type_traits> base/meta/traits.h, plus is_floating_point, is_arithmetic, is_convertible
<bit> base::CountLeftZero, next to the existing PopCount
<cstddef> decltype(sizeof(0)), which needs no header
<thread>, <chrono> base::SleepForMicroseconds, Thread::Join, YieldCurrentThread
<mutex>, <shared_mutex> the native SRWLOCK path clang-cl already took
<atomic> on MSVC a new _Interlocked* backend

What stays is core language, not library. <new> backs placement new, <initializer_list> backs brace-init constructors, and the ABI fixes the operator new signatures that need std::nothrow_t. The std::formatter shims stay opt-out under BASE_NO_STD_FORMAT.

C runtime

The line sits at the runtime, not the operating system. write, open, mmap and pthread_* stay.

Was Now
<cstring>, <cstdio> and friends the C spellings, then gone from 32 files
memcpy, memset, memmove, memcmp base/memory/mem_ops.h
strlen, strchr, strcmp base's own, which existed and were not being used
snprintf base::FormatTo
strtoll, strtod base/strings/number_parse.h
fprintf(stderr, ...), abort base/standard_streams.h
getenv a direct environ walk
strerror base::ErrnoName
fopen + fgets over /proc base::ReadSmallFile

<cstring> and <cstdio> were the worst of it. They promise only the std:: overloads, so the bare memcpy every call site wrote compiled by a standard library's courtesy rather than by guarantee.

Some CRT use stays on purpose. allocator/default_crt_alloc.h is the CRT allocator router and exists to call malloc. The free() in debugging.cc releases what __cxa_demangle and backtrace_symbols hand back, and setenv owns the storage it allocates. The Windows entry-point shim integrates with the CRT for a living.

Float conversion

Formatting and parsing are exact. For m * 2^e with e < 0, the identity m / 2^-e == m * 5^-e / 10^-e turns the conversion into one big-integer multiply, so the digits come out exactly and get rounded once, half-to-even. The bignum uses 32-bit limbs, so nothing in it needs a 128-bit type MSVC spells differently.

Both directions run against printf and strtod byte for byte. The sweeps cover every binary exponent from -1074 to 1023, random bit patterns, random decimal text, and near-tie values. The deep run was 24 million comparisons. BASE_FLOAT_FUZZ_ITERATIONS and BASE_NUMBER_FUZZ_ITERATIONS raise the counts.

That testing caught four bugs before they shipped:

  • a three-digit exponent printing e+330 for e+300
  • double rounding on parse. Rounding to 53 bits and then shifting into the subnormal range rounds twice, and lands a ulp off
  • a subnormal's exponent is pinned at 2^-1074, so the shift-and-bump a normal number needs on carry halved the value instead
  • CountLeftZero taking its width from MinMax<T>::digits(), which drops the sign bit

Defects fixed on the way through

  • base::Strcmp returned mem_size, which is u64. An unsigned return cannot express "sorts first", so Strcmp(a, b) < 0 was false whatever the strings were. The limited overload also read two uninitialised locals at limit zero. No callers and no test, which is how both survived.
  • FormatFloat formatted into a 64-byte buffer and clamped to it, so %f near DBL_MAX was silently truncated.
  • scoped_generic.h called abort() without including <stdlib.h>. It compiled because something else pulled it in.
  • find_stl.py matched includes with [a-z_]+, which cannot match the . in stdio.h. It had never once seen a C header. I injected an include and watched it get caught, instead of trusting a clean run on a tree I had just cleaned.

MSVC

CI has an MSVC, so the _Interlocked* backend is compiled and run by the
compiler it was written for. The backend test takes the real <intrin.h> there,
and the shim only off Windows. On MSVC it therefore checks the code that ships,
not a stand-in. A second target asserts _M_IX86 on a non-MSVC host to reach
the compare-exchange loops, which no other build does.

Windows CI was already failing on devel5 before this branch, on a const that
GCC ignores on a return type and MSVC rejects as a different type. That is fixed
here too, so this turns Windows green rather than merely not-red.

Getting there took several rounds against real MSVC. It caught what stub headers
on Linux could not:

  • MemoryBarrier comes from <windows.h>, not <intrin.h>
  • minwin.h declared none of CreateThread, WaitForSingleObject,
    CloseHandle, GetStdHandle or WriteFile
  • MSVC rejects 1.0 / 0.0 as a constant divide by zero

The tests found one more once it got that far: a NaN's payload differs between
libraries, so comparing parse results bit for bit was wrong to demand they
match.

Verification

gcc 16 Debug and clang Release, ctest 4/4 on both. 966 unit tests, up from 888. ThreadSanitizer over the atomic, mutex and thread-pool tests reports no data races. The thread-leak warnings it does report predate this work, because Thread had no Join until now.

Survey of base turned up STL use in three shapes. This clears the two
that were incidental and leaves only what the language itself mandates.

C library headers: base spells its C calls bare (memcpy, snprintf), but
39 files reached them through <cstring>/<cstdio>/<cstdint>/... which only
guarantee the std:: overloads -- the bare name compiling at all was a
libstdc++ courtesy. Switched to <string.h>/<stdio.h>/... and rewrote the
remaining std::memcpy/fprintf/strcmp/getenv/... call sites to ::.

Dead includes: <vector> in command_line.h, <limits>/<set>/<string> in
file_util.h, <memory> in scoped_file.h. None were used.

Real dependencies, replaced with base's own:
  - <type_traits> in option.h        -> base/meta/traits.h, which gains
                                        is_floating_point, is_arithmetic,
                                        is_convertible and ConvertibleTo
  - <bit> in bucket_allocator.h      -> base::CountLeftZero, new alongside
                                        PopCount in builtins_bit.h
  - <cstddef> in compiler.h          -> decltype(sizeof(0)), which is
                                        size_t by definition and needs no
                                        header at all
  - <thread>/<chrono> in thread_pool -> base::SleepForMicroseconds, new in
                                        thread.h with nanosleep and Sleep
                                        backings, alongside Thread::Join
                                        and YieldCurrentThread
  - <thread> in the allocator test   -> raw pthread/CreateThread, so the
                                        standalone target keeps linking
                                        allocator sources and nothing else

What remains is core language, not library: <new> for placement new,
<initializer_list> for brace-init constructors, and std::nothrow_t in the
operator new overrides, whose signature the ABI fixes. Both headers are
freestanding. The <format> shims stay opt-out under BASE_NO_STD_FORMAT,
and atomic.h/mutex.h keep their MSVC std:: fallbacks for now.

888 base_unittests and 45 base_memory_unittests pass.
mutex.h auto-selected <mutex>/<shared_mutex> on MSVC, but the native
Windows path it was falling back from is pure SRWLOCK and is already what
clang-cl builds use -- MSVC was taking the STL route for no reason the
code could point at. Removed the auto-enable; BASE_USE_STD_MUTEX survives
as a hand-set escape hatch. That takes the last non-mandated STL header
out of base's default build on every platform.

Tests for what the previous commit added, none of which had any:

  - containers/builtins_bit_test.cc for CountLeftZero / CountRightZero,
    per bit position, at zero, and at each type width
  - meta/traits_test.cc for is_floating_point, is_arithmetic,
    is_convertible and ConvertibleTo, plus the older traits option.h
    dispatches on, which nothing covered either
  - threading/thread_test.cc, which was one #if 0 block and now exercises
    Thread::Join, SleepForMicroseconds and YieldCurrentThread

Writing them caught a real defect: CountLeftZero took its width from
MinMax<T>::digits(), which follows std::numeric_limits and excludes the
sign bit, so a signed argument would have been off by one. The width now
comes from sizeof and a static_assert rejects signed types, matching what
std::countl_zero accepts. A test pins the distinction.

907 base_unittests (up from 888) and 45 base_memory_unittests pass.
atomic.h auto-selected std::atomic whenever MSVC's own frontend compiled
it, because the native path is built on __atomic_* builtins and MSVC has
none. That was base's last STL dependency in a default build.

base::Atomic and base::Atomic<T*> are now one class over a primitive layer
with two backends: the __atomic_* builtins as before, and _Interlocked*
intrinsics for MSVC. The builtin path keeps its semantics exactly -- the
28 atomic tests, 907 unit tests and 45 memory tests cover the refactor.

Notes on the MSVC backend:

  - Values are bit-cast to the signed integer of their width. Only 1, 2, 4
    and 8 byte types are supported and a static_assert says so; the builtin
    path's libatomic fallback for wider T has no counterpart here.
  - Read-modify-write always uses the plain, full-barrier intrinsic. That
    is stronger than a relaxed request asks for, never weaker, and on
    x86/x64 it is what the weaker orders compile to anyway.
  - Load and store are the only operations whose emitted code depends on
    the order, so they are the only ones that branch on it. ARM64 gets a
    real __dmb; x86/x64 gets a compiler barrier, which is all TSO needs.
  - 32-bit x86 lacks the 64-bit read-modify-write intrinsics, so those are
    synthesised from cmpxchg8b.
  - The widths are named Int32/Int64 rather than spelled long/__int64,
    because long is 64-bit on the host the test below runs on and hard
    coding it would make every 4-byte atomic touch 8 bytes.

There is no MSVC here, so the backend is verified by running it: two new
targets compile it against atomic_msvc_intrin_shim.h, which stands the
intrinsics on the builtins, once normally and once with _M_IX86 asserted
to reach the compare-exchange loops. Both cover every width, pointers,
bool, a non-integer payload, and four threads contending on a counter and
a CAS. They pin the logic -- dispatch, argument order, bit-casting, the
negation behind fetch_sub, pointer byte scaling -- not the emitted code or
barrier placement, which only a Windows build can check.

Verified: gcc 16 Debug and clang Release, ctest 4/4 both. ThreadSanitizer
over the atomic, mutex and thread-pool tests reports no data races (the
thread-leak warnings are pre-existing: Thread had no Join until now).
The script flagged every std:: token equally, so the mandated ones --
placement new, brace-init's std::initializer_list, std::nothrow_t in the
operator new replacements -- sat in the output run after run next to
things that were actually removable. A report that is mostly false
positives is a report nobody reads.

It now splits the two, hides the mandated group behind --all, and counts
only removable use in the percentage. It also understands includes, so a
stray <vector> is a finding even where no std:: name follows it, and it
skips the C headers under their C spelling while still flagging <cxxx>.

Current output: 14 removable uses, 0.05%, all of them inside the
BASE_USE_STD_ATOMIC and BASE_USE_STD_MUTEX branches that nothing enables.

readme.md now states the policy and why each remaining category stays.
New base/memory/mem_ops.h holds MemCopy/MemMove/MemSet/MemZero/
MemCompare/MemEqual. These four are not ordinary library functions -- a
compiler emits memcpy and memset calls on its own for struct assignment
and zero-init, so the symbols are part of the freestanding contract. The
header is not, though: GCC and Clang reach them as __builtin_, and MSVC
inlines them once declared with #pragma intrinsic, so neither path needs
<string.h>. 19 files converted.

The str* calls went to base's own, which already existed and in two cases
were simply not being used: CountStringLength for strlen, and Strcmp for
strcmp. Added FindChar/FindLastChar for strchr/strrchr.

<string.h> is now gone from 25 files.

string_compare.h needed fixing before anything could use it. All three
Strcmp overloads returned mem_size, which is u64 -- an unsigned return
cannot express "sorts first", so every negative result wrapped to a huge
positive one and `Strcmp(a, b) < 0` was false whatever the strings were.
The limited overload also read two uninitialized locals when the limit
started at zero. It had no callers and no test, which is how both
survived. It now returns int, splits the limited form out as Strncmp,
compares through the new base::make_unsigned_t so the answer does not
depend on whether plain char is signed, adds StrEqual for the common case,
and is constexpr throughout.

string_compare_test.cc and char_algorithms_find_test.cc cover all of it,
including the two defects above.

921 unit tests pass, ctest 4/4.
Every ToString overload was its own snprintf call, which pulled stdio into
anything that turned a number into a string. base already formats integers
itself in FormatInteger, so the six integer overloads now route through
FormatTo and reach no CRT at all; the two float ones go through FormatTo
as well and keep their historical precision, though FormatFloat still
delegates to snprintf underneath for now.

to_string.h had no test. The new one checks every overload against
snprintf directly, so the move is provably output-identical rather than
believed to be, and sweeps 19 magnitudes in both signs plus the type
limits -- including LLONG_MIN, which breaks a naive negate-then-format.
FormatFloat built a printf spec and called snprintf, which is what kept
stdio and its locale and float machinery linked into anything that
formatted a double. base now converts floats itself.

The conversion is exact rather than approximate. Every finite double is
exactly representable in decimal because 2^-n is, and for m * 2^e with
e < 0 the identity m / 2^-e == m * 5^-e / 10^-e turns the whole thing into
one big-integer multiply: the digits of m * 5^-e, with -e of them after
the point. So the digits are computed exactly and rounded once, at the
requested position, half-to-even -- which is what printf does. The big
integer is fixed-capacity, sized for the widest case (the smallest
subnormal needs 2548 bits), and uses 32-bit limbs so no multiply or divide
in it needs a 128-bit type MSVC would spell differently.

float_format_test.cc checks it against printf rather than against my
reading of the standard: same value, same spec, byte for byte. Beyond the
hand-picked cases it sweeps every binary exponent from -1074 to 1023, plus
random bit patterns and near-tie decimals whose count
BASE_FLOAT_FUZZ_ITERATIONS raises. The deep run for this commit was 15
million comparisons, all matching. The sweep found the one bug that
survived writing it: a three-digit exponent printed e+330 for e+300,
because the middle digit divided by 100 instead of 10.

Wiring it in also fixed a pre-existing defect. The old path formatted into
a 64-byte buffer and clamped to it, so %f of anything near DBL_MAX -- over
300 characters before the point -- was silently truncated. It now spills
to the heap, matching how FormatWideString already handles the same
problem. Zero-padding now also keeps the sign ahead of the zeros.

ctest 4/4; 40 format and to_string tests pass.
New strings/number_parse.{h,cc} with ParseInteger, ParseUnsigned and
ParseFloat, following the C functions closely enough to be drop-in for
base's callers -- leading space, sign, 0x and leading-0 radix detection,
saturation on overflow, an end pointer -- while setting no errno and
honouring no locale.

ParseFloat is correctly rounded, not approximate. The digits accumulate
exactly into the big integer the float formatter already uses (now lifted
into strings/decimal_bignum.h, shared by both directions), and the binary
result is rounded once, half-to-even. For a negative decimal exponent the
numerator is scaled up first and then divided down by tens, with every
remainder folded into a sticky bit, which is all the discarded part can
contribute to the rounding decision.

Checked against strtoll/strtoull/strtod on value and on how far each
consumed, over hand-picked cases plus sweeps that round-trip random
doubles through %.17g and generate random decimal text;
BASE_NUMBER_FUZZ_ITERATIONS raises the count. The deep run was 9 million
comparisons, all matching.

The sweeps found two bugs, both in the subnormal range and both worth
naming because neither shows up in ordinary values:

  - Double rounding. Rounding to 53 bits and then shifting into the
    subnormal range rounds twice and lands a ulp off. Rounding now happens
    once, at the precision the result can actually hold.
  - Carrying the wrong way. A subnormal's exponent is pinned at 2^-1074
    and only its significand grows, so the shift-right-and-bump that a
    normal number needs on carry divided the value by two instead. The
    two regimes are now written out separately.

option.h no longer includes <stdlib.h> for parsing; only getenv remains.
New base/standard_streams.{h,cc}: WriteStandardError, WriteStandardOutput
and TerminateAbnormally, over write(2) and WriteFile. Buffering was the
wrong behaviour for diagnostics anyway -- a fatal check that writes its
reason into a FILE* buffer and then traps loses the message, which is the
one thing that had to survive. check.cc and logging.cc now format with
base::FormatTo into a stack buffer and issue a single write, so a
concurrent logger can interleave between lines but not inside one.

abort() is replaced by TerminateAbnormally, which traps first (so a
debugger stops at the fault rather than inside a runtime teardown path)
and then takes _exit or TerminateProcess, skipping atexit handlers and
stream flushes. scoped_generic.h was calling abort() without including
<stdlib.h> at all, compiling only because something else happened to pull
it in.

The two /proc readers went to open/read through a new ReadSmallFile, which
reads to EOF rather than sizing first, because the files under /proc report
a size of zero. Their sscanf and atoi went to base::ParseInteger.

getenv is gone: environment_variables_posix.cc walks `environ` directly,
which is the same array getenv scans. Writes still go through
setenv/unsetenv, which own the storage they allocate. option.h and
GetTempDir now go through GetEnvironmentVariable.

strerror is replaced by base::ErrnoName, which returns the symbolic name
rather than strerror's prose: the prose is translated, so the same failure
reads differently per locale, while the name is greppable and matches what
the manual pages and the calling code are written in.

The last snprintf, in cuid2.cc, went to FormatTo.

What stays, and why: malloc/realloc/free in allocator/default_crt_alloc.h,
which is the CRT allocator router and exists to call them; free() in
debugging.cc for the buffers __cxa_demangle and backtrace_symbols allocate,
which their contracts require; and the Windows entry-point shim, whose
whole job is CRT integration.

gcc Debug and clang Release, ctest 4/4 both.
Removed <string.h>, <stdio.h> and <stdlib.h> from seven files where the
call that needed them is gone. Three survived the earlier sweep because
they carry a trailing comment -- "// for memcmp" -- that an anchored
pattern did not match, and the comments were themselves stale.

find_stl.py is now find_runtime_deps.py and reports the C runtime on the
same terms as the STL, splitting removable from mandated. Two bugs in it
were worth fixing first, both of which made it report success it had not
earned:

  - The include pattern was [a-z_]+, which cannot match the '.' in
    <stdio.h>, so it had never once seen a C header. Verified the fix by
    injecting an include and watching it get caught, rather than trusting
    a clean run on a tree I had just cleaned.
  - Headers with any other extension fell through to the STL branch, so
    the Windows shim's <exe_common.inl> was reported as removable STL.

It draws the line at the runtime rather than the operating system: write,
open, mmap and pthread_* are the kernel interface and are not reported;
stdio, printf, malloc and locale are a library base does without.

Current state: zero removable CRT use, and the 0.05% of removable STL is
entirely the two BASE_USE_STD_* branches nothing enables.

readme.md documents the policy and carries a table of what to use instead.
The Linux build never compiles the _win.cc files, so everything this branch
added for Windows shipped unchecked. Auditing it turned up five defects,
three of which would have failed the build outright.

Windows:

  - mem_ops.h declared memcpy and friends in terms of size_t, which
    base/arch.h deliberately never defines because it includes nothing. Now
    decltype(sizeof(0)), and with __cdecl so the redeclaration matches the
    CRT's.
  - number_parse.cc used __builtin_memcpy, and standard_streams_win.cc
    __builtin_unreachable. MSVC has neither. The first goes through
    base::MemCopy; the second through a new BASE_UNREACHABLE in compiler.h,
    which is __assume(0) there.
  - standard_streams_win.cc called GetStdHandle, WriteFile and
    GetCurrentProcess, none of which base/win/minwin.h declares, and used
    STD_ERROR_HANDLE, which it does not define. Added all of them in
    minwin.h's existing style, plus the LPCVOID, LPDWORD and LPOVERLAPPED
    typedefs they need, spelled as the SDK spells them so a translation
    unit that also pulls in the real <windows.h> sees no conflict.

macOS:

  - environment_variables_posix.cc read `environ` directly. Apple does not
    export that symbol to a shared library, so a dylib links and then finds
    nothing. Uses _NSGetEnviron() there, as environ(7) documents.
  - system_error_posix.cc named EBADFD, which is Linux-only. Every errno
    name outside C89 and core POSIX is now behind an #ifdef, so one missing
    on a platform drops out of the table instead of failing the build.

Also pre-existing, and the reason to look: char_algorithms.h calls
__builtin_strlen unguarded, which MSVC does not have. That is older than
this branch and means base has not been building with MSVC proper, only
clang-cl. Guarded so MSVC takes the hand-rolled loop already sitting below
it. Worth knowing, because it means the _Interlocked* atomic backend
targets a configuration that may never have built.

BASE_UNREACHABLE first went in under #if defined(CONFIG_DEBUG), so the
Debug build passed and Release would not have. Both configurations are
checked now.

Verified by compiling, not by reading: stub SDK headers let GCC parse both
Windows sources and every _MSC_VER branch this branch touched, and run the
MSVC bit-scan and string paths. The macOS branches compile with a stubbed
crt_externs.h and with EBADFD undefined. gcc Debug and clang Release,
ctest 4/4 on both.
Windows CI reported the errors my offline stub headers could not. Most of
them were mine; two were already there.

Mine:

  - atomic.h called ::MemoryBarrier() for the seq_cst fence. That macro
    comes from <windows.h>, which this header will not drag in. It expands
    to mfence anyway, so the instruction goes in directly, with __dmb on
    ARM64.
  - atomic_msvc_intrin_shim.h was being compiled by MSVC, where it cannot
    work: it stands the MSVC intrinsics on the __atomic_* builtins, which
    MSVC does not have. It also declared the 32-bit interlocked family on
    int while Int32 is long there, so every call mismatched. The backend
    test now takes the shim only off MSVC and the real <intrin.h> on it,
    which makes that test a genuine check of the shipping code rather than
    a stand-in. The shim #errors if MSVC reaches it, so the cause is one
    line instead of eighty.
  - memory_unittests_main.cc and the backend test spawn threads through the
    platform API, but base/win/minwin.h declares no CreateThread,
    WaitForSingleObject or CloseHandle, and no INFINITE or WAIT_OBJECT_0.
    Added, in minwin.h's existing style.
  - The backend test included <pthread.h> unconditionally. Its contention
    workers are now file-scope functions with each platform's exact entry
    signature, rather than lambdas, so nothing depends on a captureless
    lambda converting to a __stdcall pointer.
  - The _M_IX86 variant target is no longer built on MSVC, where that macro
    is the compiler's own and contradicts _M_X64. The path it reaches is
    architecture-independent C++, so one host covers it.

Already broken before this branch, and the reason Windows CI was red on
devel5: thread_win.cc defines GetThreadPriority and GetNativeThreadPriority
with a top-level const the declarations in thread.h do not have. GCC
ignores that on a return type; MSVC calls it a different type and rejects
the redefinition. Dropped the const.

gcc Debug and clang Release, ctest 4/4 on both.
Defining _mm_mfence in atomic_msvc_intrin_shim.h is an error on clang,
which has it as a builtin. gcc accepted it, so the Debug build passed and
only the clang Release build caught it. On x86 the shim now takes
<emmintrin.h> for the real one and keeps the builtin fallback elsewhere.
Two more from Windows CI, which is now down to one error from around two
hundred.

float_format_test.cc built its infinity as 1.0 / 0.0. MSVC rejects that as
a constant divide by zero rather than folding it to infinity, so both
values now come from their bit patterns, which is exact and needs no
constant folding at all.

The STD_*_HANDLE, INFINITE and WAIT_OBJECT_0 macros minwin.h gained are now
#ifndef-guarded, as INVALID_HANDLE_VALUE already was. thread_win.cc
includes both <Windows.h> and minwin.h, and the SDK spells WAIT_OBJECT_0
as STATUS_WAIT_0 + 0, so a bare redefinition would have warned there.

gcc Debug and clang Release, ctest 4/4 on both.
Windows now compiles, and the run got far enough to report test results.
NumberParse.FloatSpecials compared NaN bit for bit, but a NaN's payload is
unspecified and the two libraries choose differently: glibc hands back a
bare quiet NaN for "nan" where the Microsoft CRT sets every payload bit.
Both are correct. The test now compares only the sign, which strtod does
carry through from "-nan", and keeps the exact bit comparison for
everything finite.
The string overload of ReadValue declared a buffer of kMaxStringLength + 1
wchar_t and then told the API it was only kMaxStringLength long. The spare
slot the comment reserved was never actually offered.

That lost a value of exactly kMaxStringLength characters. RegSetValueExW
appends the terminator a REG_SZ was stored without, so reading such a value
back needs one slot more than the characters, and asking for less returned
ERROR_MORE_DATA. ReadsAValueThatExactlyFillsTheBuffer covers precisely that
case and has been failing since it was written; nobody saw it because the
Windows build did not compile, so the suite never ran there.

The clamp below is what keeps the read in bounds, not the size passed in:
|written| is capped at kMaxStringLength before anything indexes the array,
so the terminator this function writes lands on the spare slot at worst.
Offering the full array changes what the API may fill, not what this
function may touch.

Predates this branch and is unrelated to the STL and CRT work. Fixed here
because it is the last thing between this PR and a green Windows CI, which
has been red on devel5 for some time.
The house rule bans the em dash, and "--" between words is the same thing
in ASCII. 25 comments this branch added used it. They now use the comma,
colon or full stop the sentence wanted, rather than a unicode en dash,
which has no business in a C++ source file. The five "--" bullets in the
atomic backend test's printf output are now single dashes.

The unicode dashes still in alloc_bench.cc, bucket_allocator.h,
function.h and base_string.h predate this branch and are left alone.
@Force67
Force67 merged commit 74e85ca into devel5 Sep 15, 2026
6 checks passed
Force67 added a commit that referenced this pull request Sep 16, 2026
SetFromString stored the caller's pointer, so a string option was only
as alive as whatever buffer it was parsed out of. InitOptionsFromEnv
reads each variable into a local StringU8 and hands over its c_str(),
which dies at the end of the loop iteration: every const char* option
set from the environment has pointed at freed memory since base stopped
calling getenv (#14).

It does not read as a crash. rx takes its capture path from
RX_UI_SHOT=<path>, and with the dangling value the engine wrote a 51 KB
png to a filename made of whatever the freed bytes happened to spell.
Whoever calls SetFromString next gets a different kind of wrong.

The other two callers show the shape of the trap. option_file.cc knew,
and interns every value into a pool that outlives the options
(InternValue). recreation's platform-profile loader did not, and passes
the c_str() of a std::string that goes out of scope one line later.
Three call sites, two of them holding it wrong, is an API telling you
where the ownership belongs: with the option.

ParseFromString now copies into a buffer the option keeps, so a value
survives its source, Reset() drops it again, and callers can pass
anything they have at hand. Only const char* options carry the buffer --
detail::OwnedText is empty for every other T -- so an Option<int> is the
size it always was.

base/option_test.cc covers what nothing covered: parsing per type, a
value outliving the buffer it came from, the same through the
environment, Reset, and a rejected parse leaving the old value in place.
Three of the six fail without this change.
Force67 added a commit that referenced this pull request Sep 16, 2026
base/filesystem/file.h names uint8_t, uint32_t and int64_t across its
signatures and includes nothing that declares them. They arrived
transitively until base stopped pulling <cstdint> in for itself (#14),
so whether the build survives now depends on the standard library: gcc
15 still lands them through another include, gcc 13 does not, and the
library stops dead at

  base/filesystem/file.h:134:26: error: 'uint32_t' has not been declared
  base/filesystem/file.h:192:48: error: 'uint8_t' was not declared in this scope

with the Span<uint8_t> signatures under it collapsing into 40 more
diagnostics. A consumer on ubuntu 24.04 (gcc 13.3) cannot build devel5.

The fix is not to include <stdint.h>. base has these types already --
u8, u32, i64, mem_size out of base/arch.h, which includes nothing itself
by design -- and 54 of the 56 headers in base that name a fixed-width
type use them. file.h was one of the two that did not, so it now says
what its neighbours say, and the platform definitions in
posix/file_posix.cc and win/file_win.cc follow the declarations. Casts
and locals inside those files keep the C spellings, which is right:
that code is talking to POSIX and Win32.

base/hashing/crc.h is the other one, and it has the same gap without
having failed yet: uint32_t reaches it only through whichever of
<nmmintrin.h>, <intrin.h> or <arm_acle.h> its #if chain picks, and a
target matching no branch gets none of them.

That leaves base with no C fixed-width types in any public signature,
and no new include to carry. Library and tests build clean on gcc 13.4
and gcc 15.2.
Force67 added a commit that referenced this pull request Sep 16, 2026
base/filesystem/file.h names uint8_t, uint32_t and int64_t across its
signatures and includes nothing that declares them. They arrived
transitively until base stopped pulling <cstdint> in for itself (#14),
so whether the build survives now depends on the standard library: gcc
15 still lands them through another include, gcc 13 does not, and the
library stops dead at

  base/filesystem/file.h:134:26: error: 'uint32_t' has not been declared
  base/filesystem/file.h:192:48: error: 'uint8_t' was not declared in this scope

with the Span<uint8_t> signatures under it collapsing into 40 more
diagnostics. A consumer on ubuntu 24.04 (gcc 13.3) cannot build devel5.

The fix is not to include <stdint.h>. base has these types already --
u8, u32, i64, mem_size out of base/arch.h, which includes nothing itself
by design -- and 54 of the 56 headers in base that name a fixed-width
type use them. file.h was one of the two that did not, so it now says
what its neighbours say, and the platform definitions in
posix/file_posix.cc and win/file_win.cc follow the declarations. Casts
and locals inside those files keep the C spellings, which is right:
that code is talking to POSIX and Win32.

base/hashing/crc.h is the other one, and it has the same gap without
having failed yet: uint32_t reaches it only through whichever of
<nmmintrin.h>, <intrin.h> or <arm_acle.h> its #if chain picks, and a
target matching no branch gets none of them.

That leaves base with no C fixed-width types in any public signature,
and no new include to carry. Library and tests build clean on gcc 13.4
and gcc 15.2.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant