Repository navigation
[CoreCLR] Remove libc++ from the CoreCLR host #12533
Description
Follow-up to #12139, which removed the libc++/libunwind link-time dependency from NativeAOT applications. This issue tracks doing the same for the CoreCLR host.
Current state
NativeRuntimeComponents.cs still links the C++ runtime into every CoreCLR app:
// C++ standard library
new CplusPlusArchive ("libc++_static.a"),
new CplusPlusArchive ("libc++abi.a"),
...
new CplusPlusArchive ("libunwind.a"), // techically it's from clangThe mechanism already exists
Each runtime lane gets its own shared/log_types.hh via XA_RUNTIME_INCLUDE_DIR. The NativeAOT one is a 5-line stub; the CoreCLR one is 119 lines of std::format wrappers.
The consequence is measurable. I audited freshly built android-arm64 Release archives with llvm-nm --undefined-only. bridge-processing.cc — literally the same source file, compiled into both lanes — has zero libc++ dependencies under NativeAOT and the full std::format payload under CoreCLR:
| Lane | libc++/ABI undefined symbols in bridge-processing.cc.o |
|---|---|
| NativeAOT | none |
| CoreCLR | to_chars ×9 (float/double/long double), std::locale ×4, numpunct<char>::id, use_facet, basic_string::append/push_back, __libcpp_verbose_abort, operator new, operator delete |
The whole NativeAOT host archive has no real libc++ undefined symbols. So this is a tractable, already-proven path — not a rewrite.
What actually blocks CoreCLR
Measured across libnet-android, libruntime-base, libruntime-base-common (arm64, Release):
| # | Blocker | Where | Proposed fix |
|---|---|---|---|
| 1 | std::format — 21 sites. Drags to_chars<float/double/long double> (Ryu tables), locale/numpunct/use_facet, std::string |
assembly-store.cc, host.cc, fastdev-assemblies.cc, pinvoke-override/precompiled.cc, timing-internal.cc, mainthread-dso-loader.hh |
18 of 21 are Helpers::abort_application (...) error paths → fixed char [] + snprintf. No perf concern |
| 2 | std::string (43 refs) |
mostly format results + assembly-store.cc path building (std::to_string (getpid ()), std::format ("{:x}")) |
Fixed buffers + the format_X ()/build_X () stack→heap retry convention |
| 3 | std::function |
AssemblyStore::configure_from_payload, FastTiming::dump |
Function pointer + void* context, or a template parameter |
| 4 | std::shared_ptr<Timing> |
host.hh |
Single owner → raw pointer / static |
| 5 | std::mutex ×9 |
assembly-store.cc, monodroid-dl.hh, startup-aware-lock.hh |
pthread_mutex_t (Bionic) |
| 6 | std::unordered_map<std::string, std::string> bundled_properties |
clr/include/runtime-base/android-system.hh |
Currently fully inlined (emits no symbols) but drags <string> → sorted static array + binary search |
| 7 | std::stack / std::vector / steady_clock |
timing-internal.hh, timing.hh |
Fixed-capacity array + clock_gettime (CLOCK_MONOTONIC) |
| 8 | Residue: operator new/delete, __cxa_guard_*, __cxa_thread_atexit, __libcpp_verbose_abort, __cxx_atomic_notify_all |
all TUs | Small shim TU (malloc/free) — unless CoreCLR already provides them |
Amplifier worth fixing early
common/include/runtime-base/mainthread-dso-loader.hh uses std::format in a header, reaching android-system.cc and typemap.cc through dso-loader.hh → monodroid-dl.hh. Fixing that one header shrinks the blast radius considerably.
Good news
There are no C++ exceptions anywhere in the host — zero _Unwind_*, __cxa_throw, typeinfo or vtable symbols. -fno-exceptions is doing its job, so libunwind.a is only a question for CoreCLR itself, not for our code.
Gating unknown ⚠️
Does libcoreclr_static.a itself require libc++? CoreCLR's VM and PAL are heavily C++, so it may, regardless of what we do to our host.
For NativeAOT the trick was that dotnet/runtime ships libstdc++compat.a, which satisfied operator new (see #12523 (comment)). CoreCLR has no equivalent, so we would likely need our own shim TU.
This needs to be answered before step 5 below, otherwise we could do all the work and still link libc++:
llvm-nm --undefined-only libcoreclr_static.a \
| grep -cE '__ndk1|^_ZSt|^_ZNSt|^_Zn[wa]m|^_Zd[la]Pv'(libcoreclr_static.a is not in the CoreCLR runtime pack — microsoft.netcore.app.runtime.android-arm64 ships only the BCL natives — so this needs a build that produces it.)
Proposed sequencing
Mirroring the stack that worked for NativeAOT (#12513 → #12524):
Helpers::abort_applicationnon-format overload + convert the 18 abort sitesmainthread-dso-loader.hh+assembly-store.ccpath building- Timing:
std::stack/std::vector/std::function/std::shared_ptr/steady_clock std::mutex+bundled_properties- Shims + drop the archives from
NativeRuntimeComponents.csand CMake
Steps 1–4 are worth doing on their own merits (they remove allocation and std::string from startup paths). Step 5 is the one gated on the question above.
Also spotted
format_managed_type_name in clr/host/typemap.cc is dead code — it produces an -Wunused-function warning on every CoreCLR build.
Activity
simonrozsival commentedon Aug 27, 2026
First step: #12534 removes
std::formatfrom the CoreCLR host.Measured on the arm64 Release archive, undefined libc++ refs drop 144 → 78 (−46%), and the
std::formatfingerprint (to_chars<float/double/long double>+locale/numpunct/use_facet) is gone from every object file.It also fixes a latent bug where
bridge-processing.cclogged a literal{}in NativeAOT builds.Remaining work tracked here:
common/+mono/call sites (must land together)std::formatmachinery fromclr//mono/log_types.hhstd::string/std::function/std::mutexfromhost.cc+assembly-store.cc(the 78 remaining refs)libcoreclr_static.aitself needs libc++CplusPlusArchiveentries fromNativeRuntimeComponents.cs[native] Remove std::format from the CoreCLR host
simonrozsival commentedon Aug 27, 2026
#12534 now also deletes the
std::formatmachinery fromclr/include/shared/log_types.hh(that file is now identical to the NativeAOT stub) and removes thestd::string_viewoverload oflog_writeentirely.std::formatis now completely absent from theclr/,common/andnativeaot/trees:std::format#include <format>clr/common/(shared)nativeaot/mono/Only MonoVM still uses it.
Unrelated dead-code finding while building this:
format_managed_type_nameinclr/host/typemap.cc(introduced in #12518) sits outside the#if defined(DEBUG)block, but its only callermanaged_to_java_debugis inside it. So it's compiled but unused in Release builds and trips-Wunused-function. Moving it inside the guard would fix it — worth folding into #12518.[native] Remove std::format from the CoreCLR host
simonrozsival commentedon Aug 27, 2026
Correction to my previous comment: the
format_managed_type_namedead-code warning was introduced byb5b42f9eb("Retry formatting oversized CLR strings"), which belongs to #12517, not #12518. Sorry for the noise.While auditing the stack for stack-buffer overflows I found one more thing in the same commit, in
clr/include/runtime-base/android-system.hh:The two lanes diverge in behaviour.
determine_primary_override_dir()(CoreCLR/MonoVM) does the usual stack-buffer-then-mallocretry, so an overlong path is handled. The NativeAOT branch writes into a fixedchar primary_override_dir[Constants::SENSIBLE_PATH_MAX]and aborts the app instead of falling back.This is memory-safe —
snprintfbounds the write andformat_primary_override_dirreturns the required capacity, soabort_unlesscatches the overflow rather than smashing the buffer. And withSENSIBLE_PATH_MAX == 1024against a path of<app files dir>/.__override__/<abi>, it should never trigger in practice. But it is an unnecessary difference in behaviour between the runtimes, and the fixed buffer is only there because the NativeAOT lane avoidsstd::string.Not fixing it as part of #12534, since it is unrelated to the
std::formatwork.[native] Remove std::format from the CoreCLR host
[native] Remove std::format from the CoreCLR host
simonrozsival commentedon Aug 27, 2026
Correction to the earlier symbol counts, and a roadmap
The "76 libc++ references" figure I posted earlier was inflated. The measurement script matched any demangled symbol containing
std::__ndk1, which also catches our own functions whose mangled names merely embedstd::parameter types — e.g.xamarin::android::Helpers::abort_application(..., std::string_view)accounted for 9 of them on its own. Those are ordinary cross-TU references resolved inside our archive, not libc++ dependencies.Filtering to symbols that actually start with
std::__ndk1::,operator new/delete,__cxa_guard,__cxa_thread_atexitor__libcpp_:The 55 are six root causes, not six files
A partial removal buys nothing: the link requirement only drops when a whole cause reaches zero. So the follow-up PRs are sliced by cause rather than by file.
std::shared_ptr(__shared_weak_count)std::mutexoperator new/delete[]__cxa_guard_*(function-local statics)std::string(+to_string)__libcpp_verbose_abort/__cxa_thread_atexitProgress
55 → 48 (#12540) → 40 (#12541).
Remaining plan
thread_local std::stack<TimingEvent*>intiming-internal.hh→ fixed-capacity array stack. Kills__cxa_thread_atexitandstd::deque's allocations.std::function<std::string()>inAssemblyStore::configure_from_payload→ plain callback + caller-provided buffer.std::string— the big one. NoteTimingEvent::more_infois astd::string*, so this needs a decision on ownership.new/deleteand the remainingunique_ptr<T[]>allocations →malloc/free.Once CoreCLR reaches 0, the
CplusPlusArchiveentries can come out ofNativeRuntimeComponents.cs. MonoVM's 45std::formatuses remain a separate track.[native] Remove std::format from the CoreCLR host
[native] Remove std::format from the CoreCLR host
[native] Remove std::format from the CoreCLR host
[native] Remove std::format from the CoreCLR host
6 remaining items
[native] Fix a JNI reference leak and drop new[] from jstring_array_w…
[native] Replace the bundled properties map with a linked list (#12551)
[native] Remove the libc++ dependencies from the CoreCLR host
[native] Drop std::binary_semaphore and the last string in the DSO lo…
[native] Replace std::mutex with pthread_mutex_t in the CoreCLR host (#…
[native] Replace the AndroidSystem path statics with a POD buffer (#1…
[native] Remove the libc++ dependencies from the CoreCLR host's start…
[native] Remove the libc++ dependencies from the CoreCLR host's timing (
[native] Remove the last libc++ dependencies from the CoreCLR host
[native] Remove libc++ dependencies from the CoreCLR assembly store (#…
[native] Stop linking libc++ into the CoreCLR and NativeAOT runtimes (#…