Skip to content

[NativeAOT] Remove libc++ from libNativeAOT.so #12139

Description

@simonrozsival

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:

  1. App size — the previous prototype reduced APK size by about 12% and libNativeAOT.so by about 15%.
  2. Startup performance — fewer native pages, relocations, C++ runtime initializers, TLS/runtime setup, and page faults.
  3. 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:

  1. 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.
  2. 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

  • Introduce printf-style logging primitives

    • Add log_writev, printf-style log_* helpers, and Helpers::abort_applicationf.
    • Preserve the existing std::format path for Mono/CoreCLR.
    • Do not migrate call sites in the same PR.
  • Migrate NativeAOT-specific formatting call sites

    • Convert src/native/nativeaot/host/*.cc from {} formatting to bounded printf-style formatting.
    • Use %.*s for std::string_view; never assume .data() is NUL-terminated.
  • Migrate NativeAOT-reachable shared logging

    • 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.
  • Remove std::format from the NativeAOT compilation

    • Stop including <format> for XA_HOST_NATIVEAOT.
    • Confirm charconv, locale, floating-point to_chars, and formatting-related symbols disappear from libnaot-android.*.a.
  • Replace GC bridge C++ threading primitives

    • Replace std::thread and std::binary_semaphore with pthread/POSIX primitives.
    • Keep this separate because concurrency changes need focused validation.
  • Replace NativeAOT-reachable owning containers and strings

    • Replace std::unordered_map temporary-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.hh with direct bounded parsing.
  • Eliminate remaining C++ ABI roots

    • Remove nontrivial function-local/static objects that require __cxa_guard_*.
    • Remove pure-virtual/terminate dependencies where possible.
    • Add minimal malloc/free-backed new/delete/nothrow shims in src/native/nativeaot/host/cxx-shims.cc.
    • Avoid implementing broad libc++abi or exception-personality substitutes.
  • Add an opt-in no-libc++ NativeAOT build

    • Add internal test coverage that omits libc++_static.a and libc++abi.a while leaving the product default unchanged.
    • Build all supported ABIs and inspect the resulting link response and symbols.
  • Remove libc++ from NativeAOT link inputs by default

    • Remove libc++_static.a and libc++abi.a from both the legacy NDK/clang path and the workload NativeLinker path.
    • Keep libclang_rt.builtins and NDK libunwind.a initially.
    • Add -nostdlib++ or equivalent protection against the compiler driver adding libc++ implicitly.
  • Remove libc++ archives from NativeAOT runtime packs

    • Stop copying and packaging libc++_static.a and libc++abi.a for NativeAOT.
    • Do not remove archives still required by MonoVM or CoreCLR.
  • Audit whether NDK libunwind.a remains necessary

    • Verify whether compiler-rt or supported third-party native libraries still require _Unwind_*.
    • Remove NDK libunwind.a only 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:

  • 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: App+Library BuildIssues when building Library projects or Application projects.Area: NativeAOTIssues that only occur when using NativeAOT.drop-libcppWork to remove the libc++ dependency from Android NativeAOTneeds-triageIssues that need to be assigned.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions