Repository navigation
[NativeAOT] Remove libc++ from libNativeAOT.so #12139
Description
Summary
Remove the link-time dependency on libc++_static.a and libc++abi.a from Android NativeAOT's final libNativeAOT.so.
This is valuable independently for:
- App size — the previous prototype reduced APK size by about 12% and
libNativeAOT.soby about 15%. - Startup performance — fewer native pages, relocations, C++ runtime initializers, TLS/runtime setup, and page faults.
- Linker robustness — removing libc++ removes the primary reason the Android NDK's
libunwind.agets extracted, avoiding collisions with NativeAOT's private libunwind implementation.
Previous feasibility work: #11311.
Motivation
App-size results
PR #11311 measured the following using samples/NativeAOT/NativeAOT.csproj in Release with the trimmable typemap:
| Artifact | libc++ baseline | Without libc++ | Reduction |
|---|---|---|---|
| arm64 APK | 1,575,849 B | 1,382,336 B | 193,513 B (12.28%) |
| x64 APK | 1,639,459 B | 1,439,677 B | 199,782 B (12.19%) |
arm64 libNativeAOT.so |
3,481,880 B | 2,943,232 B | 538,648 B (15.47%) |
x64 libNativeAOT.so |
3,404,896 B | 2,866,728 B | 538,168 B (15.81%) |
This removes roughly 540 KB per ABI from the native shared library.
Startup impact
Statically linking libc++ increases startup work through:
- additional mapped/decompressed native pages;
- additional relocations;
- C++ runtime and static-initializer execution;
- thread-local/runtime exception infrastructure;
- increased page faults and resident memory.
The exact startup improvement still needs measurement, but removing this dependency reduces work on the application startup path by construction.
libunwind collisions
There are currently two distinct unwind implementations involved:
- NativeAOT contains a private LLVM libunwind implementation in
libRuntime.WorkstationGC.a. It is used byUnixNativeCodeManagerandUnwindHelpersto parse DWARF/compact-unwind information and virtually unwind managed frames for GC stack walking, exception handling, stack traces, profiling, and related runtime services. libc++abi.areferences the_Unwind_*ABI, causing the final Android link to extract objects from the Android NDK'slibunwind.a.
With NDK r29, these copies can define the same global symbols, producing the duplicate-symbol failure tracked by dotnet/runtime#121172.
The runtime-side fix, dotnet/runtime#128927, privatizes NativeAOT's libunwind symbols in .NET 11 Preview 7. That is still valuable defense-in-depth, especially for third-party native libraries.
Removing libc++ solves a different layer of the problem: it removes the main built-in consumer that causes NDK libunwind to be pulled into our application link at all.
Current dependency inventory
The Android NativeAOT host currently compiles the sources listed in:
src/native/nativeaot/host/CMakeLists.txt
The major libc++ dependency groups in the resulting libnaot-android.*.a are:
| Dependency group | Current source |
|---|---|
| charconv, locale, owning strings and formatting | std::format-based logging |
| thread, atomic wait, chrono and system errors | std::thread and std::binary_semaphore |
| hashing and allocation | std::unordered_map and owning std::string state |
| C++ ABI | new/delete, static guards, pure virtual and terminate |
std::string_view, std::array, and simple atomics are generally header-only and are not by themselves reasons to remove code. The work should be driven by the final archive's unresolved symbols rather than by a blanket ban on all std::* types.
Main source areas
Formatting and logging
src/native/clr/include/shared/log_types.hhsrc/native/clr/shared/log_functions.ccsrc/native/common/include/shared/helpers.hhsrc/native/clr/shared/helpers.ccsrc/native/nativeaot/host/bridge-processing.ccsrc/native/nativeaot/host/host.ccsrc/native/clr/host/bridge-processing.ccsrc/native/clr/host/host-shared.ccsrc/native/clr/host/os-bridge.ccsrc/native/clr/runtime-base/android-system-shared.ccsrc/native/clr/runtime-base/util.ccsrc/native/clr/include/runtime-base/util.hh
Threading
src/native/clr/include/host/gc-bridge.hhsrc/native/clr/host/gc-bridge.cc
Containers and owning state
src/native/clr/include/host/bridge-processing-shared.hhsrc/native/common/include/shared/cpp-util.hhsrc/native/clr/include/runtime-base/android-system.hhsrc/native/clr/runtime-base/android-system.ccsrc/native/clr/runtime-base/android-system-shared.ccsrc/native/clr/runtime-base/logger.cc
Linker and packaging
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targetssrc/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cssrc/androidsdk/androidsdk.targetsbuild-tools/create-packs/Microsoft.Android.Runtime.projsrc/native/nativeaot/host/CMakeLists.txt
Proposed small PR sequence
-
Introduce printf-style logging primitives (#12140)
- Add
log_writev, printf-stylelog_*helpers, andHelpers::abort_applicationf. - Preserve the existing
std::formatpath for Mono/CoreCLR. - Do not migrate call sites in the same PR.
- Add
-
Migrate NativeAOT-specific formatting call sites (#12140)
- Convert
src/native/nativeaot/host/*.ccfrom{}formatting to bounded printf-style formatting. - Use
%.*sforstd::string_view; never assume.data()is NUL-terminated.
- Convert
-
Migrate NativeAOT-reachable shared logging (#12148, #12150, #12153, #12155, #12210, #12211, #12218)
- Move reference logging (
os-bridge,logger) to the printf-style API. - Move bridge/runtime-base/util logging to the printf-style API.
- Split these by subsystem if needed to keep reviews focused.
- Move reference logging (
-
Remove
std::formatfrom the NativeAOT compilation- Stop including
<format>forXA_HOST_NATIVEAOT. - Confirm
charconv, locale, floating-pointto_chars, and formatting-related symbols disappear fromlibnaot-android.*.a.
- Stop including
-
Replace GC bridge C++ threading primitives (#12141)
- Replace
std::threadandstd::binary_semaphorewith pthread/POSIX primitives. - Keep this separate because concurrency changes need focused validation.
- Replace
-
Replace NativeAOT-reachable owning containers and strings (#12142, #12145)
- Replace
std::unordered_maptemporary-peer storage with a lightweight/vendored alternative. - Replace static/owning logger and AndroidSystem strings with fixed, bounded, or explicitly allocated buffers.
- Replace ranges/vector/string parsing in
cpp-util.hhwith direct bounded parsing.
- Replace
-
Eliminate remaining C++ ABI roots (#12523)
- Remove nontrivial function-local/static objects that require
__cxa_guard_*. - Remove pure-virtual/terminate dependencies where possible.
Add minimal malloc/free-backedNot needed. The ILC SDK already shipsnew/delete/nothrowshims insrc/native/nativeaot/host/cxx-shims.cc.libstdc++compat.a, which defines the five allocation-ABI symbols the NativeAOT runtime itself references (operator delete,operator delete[], bothnothrowoperator newoverloads, andstd::nothrow). Our targets were unconditionally removing that archive because it conflicts with static libc++; with libc++ gone there is no conflict, so we simply keep it. No shim file exists.- The only allocation-ABI use in our own sources was a dead
new char[]path inmonodroid__system_property_get, removed in [NativeAOT] Stop linking and shipping libc++ and libunwind #12523. llvm-nm --undefined-onlyonlibnaot-android.release-static-release.anow reports no__cxa_*,_ZTV/_ZTI,_Unwind_*orterminatereferences.
- Remove nontrivial function-local/static objects that require
-
Add an opt-in no-libc++ NativeAOT build— skipped deliberately. A temporary_AndroidNativeAotDropLibCxxproperty was used during prototyping, but once the link proved clean with zero shims there was no reason to ship a second configuration, so [NativeAOT] Stop linking and shipping libc++ and libunwind #12523 makes the removal unconditional instead. -
Remove libc++ from NativeAOT link inputs by default (#12523)
- Removed
libc++_static.aandlibc++abi.afrom both the legacy NDK/clang path and the workload NativeLinker path. libclang_rt.builtinsis kept. NDKlibunwind.awas dropped at the same time (see below).Add— not applicable. The final-nostdlib++.sois linked byLinkNativeAotSharedLibraryusingld.llddirectly, with fully explicit inputs andAllowUndefinedSymbols = false. No compiler driver is in a position to add libc++ implicitly, andld.lldwould reject a driver flag.LinkStandardCPlusPlusLibrary=falseremains as belt-and-braces for the ILC/SDK path.
- Removed
-
Remove libc++ archives from NativeAOT runtime packs (#12524)
- Stop copying and packaging
libc++_static.a,libc++abi.aandlibunwind.afor NativeAOT. - Adds a
CplusPlusitem kind so CoreCLR keeps them while NativeAOT still getscrtbegin_so.o,crtend_so.oandlibclang_rt.builtins-*.a. - Removes 18,398,848 B per ABI, roughly 55 MB across the three shipped ABIs.
- Stop copying and packaging
-
Audit whether NDK
libunwind.aremains necessary (#12523)libc++abi.awas the only consumer pulling it in. With libc++ gone the archive links with zero unresolved symbols under-Wl,--no-undefinedand-Wl,--error-unresolved-symbols.- The published
.sono longer contains any_Unwind_Resumereference, whereas the libc++ baseline did.
-
Measure and lock in size/startup improvements
- Application size measured on a default MAUI app (arm64, Release,
PublishAot):.so−199,592 B (−0.79%), APK −69,632 B (−0.47%). - This is well below Drop libc++ from Android NativeAOT linking #11311's −538,648 B per ABI. Likely because the earlier PRs in this sequence already removed the
std::format/charconv/locale payload, so the final cut only harvests what was left. Re-measuringsamples/NativeAOTagainst the Drop libc++ from Android NativeAOT linking #11311 baseline would let us state the cumulative win. - Startup timing, page faults, mapped native pages, relocations and working set are still unmeasured.
- APK/native-library baselines not yet updated.
- Application size measured on a default MAUI app (arm64, Release,
Validation requirements
For every supported ABI and both Debug/Release:
- link response contains no
libc++_static.aorlibc++abi.a; - final
libNativeAOT.sohas no unresolvedstd::__ndk1::*symbols; - final
libNativeAOT.sohas no unresolved C++ exception/personality machinery; - APK contains no libc++ payload;
- NativeAOT startup succeeds;
- GC bridge processing succeeds repeatedly;
- managed exceptions, stack walking, and crash handling continue to work;
- third-party native-library scenarios continue to link;
- both workload NativeLinker and legacy NDK linker paths are covered.
Runtime validation should include arm64 and x64. Where local emulator architecture is unavailable, use Helix rather than leaving the configuration untested.
Status: validated locally on arm64 (workload NativeLinker, Release) — links with no unresolved symbols, no libc++ payload in the APK, and a default MAUI app starts and runs. Remaining ABIs, Debug, and the legacy NDK linker path are covered by CI.
Lessons from previous attempts
PR #11311 proved that removal is feasible, but it touched roughly 34 files and combined:
- linker changes;
- shared logging redesign;
- GC bridge threading changes;
- container replacements;
- NativeAOT-specific startup workarounds;
- unrelated Java peer-shape concerns.
This made the PR difficult to review and caused repeated merge conflicts as NativeAOT work continued landing.
For this effort:
- each PR should remove one measurable symbol cluster;
- archive symbols should be inspected after every PR;
- unrelated typemap/startup/Java peer work should remain separate;
- every PR should build and validate independently;
- human review should happen incrementally rather than after a single large rewrite.
The previous review also found unsafe %s usage with std::string_view::data(). All replacements must use bounded formatting.
Non-goals
- Removing NativeAOT's private libunwind implementation used for managed stack walking.
- Reverting Privatize NativeAOT libunwind symbols runtime#128927 or relying on libc++ removal instead of symbol privatization.
- Unifying the CoreCLR and NativeAOT Java peer reference shapes; that is tracked separately by Unify GC bridge Java peer reference shape across NativeAOT and CoreCLR #11526.
- Broadly removing all
std::string_view,std::array, or simple atomics.
Related work
- Drop libc++ from Android NativeAOT linking #11311 — previous libc++ removal feasibility prototype
- NativeAOT TODO list #9784 — NativeAOT TODO list
- NDK default version needs bumping with NativeAOT #10539 — NDK default-version and libc++ linker issues
- Bump to NDK r29 #9926 — NDK r29 update attempt
- [NativeAOT] Fix application linking with API level > 21 #10812 — fix NativeAOT linking for Android API levels above 21
- Unify GC bridge Java peer reference shape across NativeAOT and CoreCLR #11526 — unify GC bridge Java peer reference shape
- [NAOT][android] Application linking breaks with NDK r29 because of duplicated libunwind symbols runtime#121172 — NDK r29 duplicate-libunwind failure
- Privatize NativeAOT libunwind symbols runtime#128927 — privatize NativeAOT libunwind symbols in .NET 11
- [Android NDK] Make API level configurable runtime#124461 — configurable NativeAOT Android API level
- Remove libstdc++ dependency from NativeAOT runtime#76705 — prior art for removing a C++ standard-library dependency
Activity
simonrozsival commentedon Jul 17, 2026
Additional CI and incremental-size evidence
Two independent PR builds now reproduce the exact .NET 10/NDK r29 libunwind collision:
dotnet-androidbuild #1513800dotnet-androidbuild #1513910In both builds,
MSBuild+Emulator 9fails the same test:The failing link combines:
.NET 10.0.9libRuntime.WorkstationGC.a, which defines NativeAOT's LLVM-libunwind symbols;29.0.14206865libunwind.a, which defines the same global symbols.ld.lldreports duplicates including__unw_init_local,__unw_get_reg,__unw_step,__unw_resume, andunw_local_addr_space.These PRs modify different native subsystems and neither changes the final NativeAOT linker inputs, so the independent reproductions strengthen the evidence that this is a systemic link-closure problem rather than a regression in either PR. Removing libc++/libc++abi removes the built-in
_Unwind_*consumer that currently causes the NDK copy to be extracted, while NativeAOT's private copy remains available for managed unwinding.There is also incremental size evidence from #12142 before the final libc++ removal:
std::__ndk1::__next_primeand owningbasic_stringdestructor roots removed;__cxa_*references reduced from 10 to 7.This complements the full prototype's roughly 540 KB-per-ABI result: even the first isolated symbol-cluster cleanup produces a measurable reduction.
Remove unused printf debugging helper (#12156)
[Native] Add printf-style native logging (#12140)
[NativeAOT] Use POSIX primitives for GC bridge processing (#12141)
[NativeAOT] Use indexed temporary peers in GC bridge (#12145)
[Native] Migrate timing logging to printf (#12153)
[CoreCLR/NativeAOT] Migrate JNI reference logging to printf (#12150)
[NativeAOT] Reduce owning C++ standard library state (#12142)
[Native] Migrate configuration diagnostics to printf (#12155)
[NativeAOT] Migrate runtime utility logging to printf (#12148)
10 remaining items
simonrozsival commentedon Aug 27, 2026
Opened #12533 to track the same work for the CoreCLR host.
The audit found that the mechanism we need already exists: each lane gets its own
shared/log_types.hhviaXA_RUNTIME_INCLUDE_DIR, and NativeAOT's is a 5-line stub while CoreCLR's is 119 lines ofstd::formatwrappers.bridge-processing.cc— the same file compiled into both lanes — has zero libc++ dependencies under NativeAOT and the fullstd::formatpayload under CoreCLR.std::formatis the dominant blocker (21 sites), and 18 of them areHelpers::abort_application (...)error paths, so they convert to fixed buffers cheaply. The gating unknown is whetherlibcoreclr_static.aitself needs libc++ — CoreCLR has nolibstdc++compat.aequivalent to lean on.[NativeAOT] Stop linking libc++ and libunwind
[NativeAOT] Stop shipping libc++ archives in the runtime packs
[NativeAOT] Stop linking libc++ and libunwind
[NativeAOT] Stop shipping libc++ archives in the runtime packs
[NativeAOT] Stop linking libc++ and libunwind
[NativeAOT] Stop shipping libc++ archives in the runtime packs
[NativeAOT] Stop linking libc++ and libunwind
[NativeAOT] Stop shipping libc++ archives in the runtime packs
[NativeAOT] Stop linking libc++ and libunwind
[NativeAOT] Stop shipping libc++ archives in the runtime packs
[NativeAOT] Stop linking and shipping libc++ and libunwind (#12523)
[native] Remove std::format from the CoreCLR host (#12534)