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:
https://github.com/dotnet/android/blob/main/src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs#L129-L140
// C++ standard library
new CplusPlusArchive ("libc++_static.a"),
new CplusPlusArchive ("libc++abi.a"),
...
new CplusPlusArchive ("libunwind.a"), // techically it's from clang
The 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_application non-format overload + convert the 18 abort sites
mainthread-dso-loader.hh + assembly-store.cc path building
- Timing:
std::stack / std::vector / std::function / std::shared_ptr / steady_clock
std::mutex + bundled_properties
- Shims + drop the archives from
NativeRuntimeComponents.cs and 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.
Follow-up to #12139, which removed the
libc++/libunwindlink-time dependency from NativeAOT applications. This issue tracks doing the same for the CoreCLR host.Current state
NativeRuntimeComponents.csstill links the C++ runtime into every CoreCLR app:https://github.com/dotnet/android/blob/main/src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs#L129-L140
The mechanism already exists
Each runtime lane gets its own
shared/log_types.hhviaXA_RUNTIME_INCLUDE_DIR. The NativeAOT one is a 5-line stub; the CoreCLR one is 119 lines ofstd::formatwrappers.The consequence is measurable. I audited freshly built
android-arm64Release archives withllvm-nm --undefined-only.bridge-processing.cc— literally the same source file, compiled into both lanes — has zero libc++ dependencies under NativeAOT and the fullstd::formatpayload under CoreCLR:bridge-processing.cc.oto_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 deleteThe 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):std::format— 21 sites. Dragsto_chars<float/double/long double>(Ryu tables),locale/numpunct/use_facet,std::stringassembly-store.cc,host.cc,fastdev-assemblies.cc,pinvoke-override/precompiled.cc,timing-internal.cc,mainthread-dso-loader.hhHelpers::abort_application (...)error paths → fixedchar []+snprintf. No perf concernstd::string(43 refs)assembly-store.ccpath building (std::to_string (getpid ()),std::format ("{:x}"))format_X ()/build_X ()stack→heap retry conventionstd::functionAssemblyStore::configure_from_payload,FastTiming::dumpvoid*context, or a template parameterstd::shared_ptr<Timing>host.hhstd::mutex×9assembly-store.cc,monodroid-dl.hh,startup-aware-lock.hhpthread_mutex_t(Bionic)std::unordered_map<std::string, std::string> bundled_propertiesclr/include/runtime-base/android-system.hh<string>→ sorted static array + binary searchstd::stack/std::vector/steady_clocktiming-internal.hh,timing.hhclock_gettime (CLOCK_MONOTONIC)operator new/delete,__cxa_guard_*,__cxa_thread_atexit,__libcpp_verbose_abort,__cxx_atomic_notify_allmalloc/free) — unless CoreCLR already provides themAmplifier worth fixing early
common/include/runtime-base/mainthread-dso-loader.hhusesstd::formatin a header, reachingandroid-system.ccandtypemap.ccthroughdso-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-exceptionsis doing its job, solibunwind.ais only a question for CoreCLR itself, not for our code.Gating unknown⚠️
Does
libcoreclr_static.aitself 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 satisfiedoperator 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++:
(
libcoreclr_static.ais not in the CoreCLR runtime pack —microsoft.netcore.app.runtime.android-arm64ships 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 buildingstd::stack/std::vector/std::function/std::shared_ptr/steady_clockstd::mutex+bundled_propertiesNativeRuntimeComponents.csand CMakeSteps 1–4 are worth doing on their own merits (they remove allocation and
std::stringfrom startup paths). Step 5 is the one gated on the question above.Also spotted
format_managed_type_nameinclr/host/typemap.ccis dead code — it produces an-Wunused-functionwarning on every CoreCLR build.