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.so by 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.a gets 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 by UnixNativeCodeManager and UnwindHelpers to 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.a references the _Unwind_* ABI, causing the final Android link to extract objects from the Android NDK's libunwind.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.hh
src/native/clr/shared/log_functions.cc
src/native/common/include/shared/helpers.hh
src/native/clr/shared/helpers.cc
src/native/nativeaot/host/bridge-processing.cc
src/native/nativeaot/host/host.cc
src/native/clr/host/bridge-processing.cc
src/native/clr/host/host-shared.cc
src/native/clr/host/os-bridge.cc
src/native/clr/runtime-base/android-system-shared.cc
src/native/clr/runtime-base/util.cc
src/native/clr/include/runtime-base/util.hh
Threading
src/native/clr/include/host/gc-bridge.hh
src/native/clr/host/gc-bridge.cc
Containers and owning state
src/native/clr/include/host/bridge-processing-shared.hh
src/native/common/include/shared/cpp-util.hh
src/native/clr/include/runtime-base/android-system.hh
src/native/clr/runtime-base/android-system.cc
src/native/clr/runtime-base/android-system-shared.cc
src/native/clr/runtime-base/logger.cc
Linker and packaging
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets
src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs
src/androidsdk/androidsdk.targets
build-tools/create-packs/Microsoft.Android.Runtime.proj
src/native/nativeaot/host/CMakeLists.txt
Proposed small PR sequence
Validation requirements
For every supported ABI and both Debug/Release:
- link response contains no
libc++_static.a or libc++abi.a;
- final
libNativeAOT.so has no unresolved std::__ndk1::* symbols;
- final
libNativeAOT.so has 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.
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
Related work
Summary
Remove the link-time dependency on
libc++_static.aandlibc++abi.afrom Android NativeAOT's finallibNativeAOT.so.This is valuable independently for:
libNativeAOT.soby about 15%.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.csprojin Release with the trimmable typemap:libNativeAOT.solibNativeAOT.soThis removes roughly 540 KB per ABI from the native shared library.
Startup impact
Statically linking libc++ increases startup work through:
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:
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.txtThe major libc++ dependency groups in the resulting
libnaot-android.*.aare:std::format-based loggingstd::threadandstd::binary_semaphorestd::unordered_mapand owningstd::stringstatenew/delete, static guards, pure virtual and terminatestd::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 allstd::*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.hhThreading
src/native/clr/include/host/gc-bridge.hhsrc/native/clr/host/gc-bridge.ccContainers 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.ccLinker 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.txtProposed small PR sequence
Introduce printf-style logging primitives
log_writev, printf-stylelog_*helpers, andHelpers::abort_applicationf.std::formatpath for Mono/CoreCLR.Migrate NativeAOT-specific formatting call sites
src/native/nativeaot/host/*.ccfrom{}formatting to bounded printf-style formatting.%.*sforstd::string_view; never assume.data()is NUL-terminated.Migrate NativeAOT-reachable shared logging
os-bridge,logger) to the printf-style API.Remove
std::formatfrom the NativeAOT compilation<format>forXA_HOST_NATIVEAOT.charconv, locale, floating-pointto_chars, and formatting-related symbols disappear fromlibnaot-android.*.a.Replace GC bridge C++ threading primitives
std::threadandstd::binary_semaphorewith pthread/POSIX primitives.Replace NativeAOT-reachable owning containers and strings
std::unordered_maptemporary-peer storage with a lightweight/vendored alternative.cpp-util.hhwith direct bounded parsing.Eliminate remaining C++ ABI roots
__cxa_guard_*.new/delete/nothrowshims insrc/native/nativeaot/host/cxx-shims.cc.Add an opt-in no-libc++ NativeAOT build
libc++_static.aandlibc++abi.awhile leaving the product default unchanged.Remove libc++ from NativeAOT link inputs by default
libc++_static.aandlibc++abi.afrom both the legacy NDK/clang path and the workload NativeLinker path.libclang_rt.builtinsand NDKlibunwind.ainitially.-nostdlib++or equivalent protection against the compiler driver adding libc++ implicitly.Remove libc++ archives from NativeAOT runtime packs
libc++_static.aandlibc++abi.afor NativeAOT.Audit whether NDK
libunwind.aremains necessary_Unwind_*.libunwind.aonly in a separate change if the complete link closure proves it unused.Measure and lock in size/startup improvements
Validation requirements
For every supported ABI and both Debug/Release:
libc++_static.aorlibc++abi.a;libNativeAOT.sohas no unresolvedstd::__ndk1::*symbols;libNativeAOT.sohas no unresolved C++ exception/personality machinery;Runtime validation should include arm64 and x64. Where local emulator architecture is unavailable, use Helix rather than leaving the configuration untested.
Lessons from previous attempts
PR #11311 proved that removal is feasible, but it touched roughly 34 files and combined:
This made the PR difficult to review and caused repeated merge conflicts as NativeAOT work continued landing.
For this effort:
The previous review also found unsafe
%susage withstd::string_view::data(). All replacements must use bounded formatting.Non-goals
std::string_view,std::array, or simple atomics.Related work