Skip to content

[NativeAOT] Stop linking and shipping libc++ and libunwind - #12523

Open
simonrozsival wants to merge 3 commits into
dev/simonrozsival/simplify-dso-namefrom
dev/simonrozsival/nativeaot-remove-libcpp
Open

[NativeAOT] Stop linking and shipping libc++ and libunwind#12523
simonrozsival wants to merge 3 commits into
dev/simonrozsival/simplify-dso-namefrom
dev/simonrozsival/nativeaot-remove-libcpp

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Aug 26, 2026

Copy link
Copy Markdown
Member

Closes #12139
Closes #12146

Removes libc++_static.a, libc++abi.a and libunwind.a from the NativeAOT link. This is the payoff for the local-string removal work in the rest of this stack: with strings.hh, dynamic_local_string and static_local_string gone, nothing in libnaot-android uses libc++ any more.

Who was actually using the C++ runtime?

Linking with libc++ removed leaves exactly six undefined symbols. Only one of them came from our code:

Symbol Referenced from
operator new[](size_t) android-system-shared.ccours
operator delete(void*) gcenv.ee.cpp, UnixNativeCodeManager.cpp
operator delete[](void*) gcenv.ee.cpp, interoplibinterface_java.cpp
operator new(size_t, nothrow_t const&) gcenv.ee.cpp, TypeManager.cpp
operator new[](size_t, nothrow_t const&) gcenv.ee.cpp, RhConfig.cpp
std::nothrow gcenv.ee.cpp, UnixNativeCodeManager.cpp

The five runtime-owned ones belong to the NativeAOT runtime from dotnet/runtime, and the ILC SDK already ships definitions for all of them in libstdc++compat.a. Our targets were unconditionally removing that archive with the comment "This library conflicts with static libc++" — which is only true while libc++ is linked. With libc++ gone there is no conflict, so we simply stop removing it.

The one symbol that was ours came from a dead code path in monodroid__system_property_get, which is now removed earlier in the stack by #12517 (the PR that makes that path unreachable in the first place).

The result is no shim code at all in this repo, and a link with zero undefined symbols.

Size impact

Default MAUI app (dotnet new maui), net11.0-android, android-arm64, Release, PublishAot=true. Both sides built clean from the same tree.

before after delta
.so 25,229,320 25,029,728 −199,592 (−0.79%)
.so deflated in APK 9,829,143 9,761,291 −67,852 (−0.69%)
APK 14,903,624 14,833,992 −69,632 (−0.47%)

Commits

  1. Stop linking libc++ and libunwind — the targets change.
  2. Update stale libc++ linker comments.
  3. Stop shipping libc++ archives in the runtime packs — see below.

Notes

  • This PR depends on [Native] Restore indexed GC temporary peers #12509: before it, the GC bridge used a std::unordered_map, which drags in __next_prime and __libcpp_verbose_abort from libc++ internals. The rest of this stack is already rebased on top of it, so nothing further is needed.
  • This only affects NativeAOT. The Mono and CoreCLR runtimes still link libc++, and NativeRuntimeComponents.cs (the unified-runtime archive list) is deliberately untouched.
  • libunwind.a drops out with zero undefined symbols — the NativeAOT runtime never referenced it.

Testing

Built, installed and launched a default MAUI app on an API 36 arm64 emulator. Cold start with no crashes.

llvm-nm --undefined-only on the resulting libnaot-android.release-static-release.a reports no operator new/operator delete, __cxa_*, _Unwind_* or __libcpp_* references. The only remaining std:: symbols are string_view appearing in mangled names, which is header-only and carries no runtime dependency.

Not covered: only the arm64 + workload-linker path was exercised locally. The NDK linker path and the x64/arm ABIs rely on CI.


Also: stop shipping the archives in the runtime packs

Previously a separate PR stacked directly on this one; folded in here because "stop linking it" and "stop shipping it" are the same change to the reader, and reviewing them apart means reading the same targets twice.

The NativeAOT runtime packs still shipped libc++_static.a, libc++abi.a and libunwind.a even though, after the change above, nothing links them any more.

Why this needs a new item kind

_AndroidNdkRedistributable (in build-tools/scripts/Ndk.targets) tagged NDK files with just two kinds:

  • Systemlibc.so, libdl.so, liblog.so, libm.so, libz.so — shipped to every runtime.
  • Toolchaincrtbegin_so.o, crtend_so.o, libc++_static.a, libc++abi.a, libclang_rt.builtins-*.a, libunwind.a — shipped to CoreCLR and NativeAOT, since both do native linking.

NativeAOT still needs crtbegin_so.o, crtend_so.o and libclang_rt.builtins-*.a, so the Toolchain group cannot just be dropped for NativeAOT.

This adds a third kind, CplusPlus, for the three C++ archives, and ships it only for CoreCLR. Both packaging sites are updated:

  • src/native/native.targets — the local bin/<Config>/lib/packs layout.
  • build-tools/create-packs/Microsoft.Android.Runtime.proj — the shipped NuGet packs.

Size

Per ABI, removed from the NativeAOT runtime pack:

Archive Size
libc++_static.a 15,182,348
libc++abi.a 3,125,348
libunwind.a 91,152
Total 18,398,848

Across android-arm, android-arm64 and android-x64 that is roughly 55 MB of pack content. This does not change application size — that is the linker change above — but it shrinks what users restore.

Testing

Deleted each pack directory and regenerated it via _CopyToPackDirs, rather than checking a pack that could still contain stale files.

NativeAOT (android-arm64) — the three archives are gone, and everything NativeAOT links is still present:

crtbegin_so.o  crtend_so.o  libc.so  libclang_rt.builtins-aarch64-android.a
libdl.so  liblog.so  libm.so  libz.so
libnaot-android.debug-static-debug.a  libnaot-android.debug.so
libnaot-android.release-static-release.a  libnaot-android.release.so
libxa-java-interop-release.a

CoreCLR (android-arm64) — all three are still shipped:

crtbegin_so.o  crtend_so.o  libarchive-dso-stub.so  libc.so
libc++_static.a  libc++abi.a  libclang_rt.builtins-aarch64-android.a
libdl.so  liblog.so  libm.so  libunwind.a  libz.so

Mono is unaffected — it only ever received the System kind.

Copilot AI lite review requested due to automatic review settings August 26, 2026 13:03
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from 6c0530e to 75d714c Compare August 26, 2026 13:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR advances the NativeAOT size/startup work in #12139 by removing libc++/libunwind from the NativeAOT link, while also refactoring several native runtime paths to avoid local-string helpers and reduce incidental C++ runtime dependencies. It also updates test baselines for resulting native library/package size changes.

Changes:

  • Update NativeAOT MSBuild targets to stop linking libc++_static.a, libc++abi.a, and libunwind.a, and to stop removing libstdc++compat.a.
  • Refactor multiple native runtime components (Mono + CoreCLR hosts) to use explicit bounded buffers / snprintf / heap fallback helpers instead of local-string types.
  • Update .apkdesc test resource baselines to reflect new libmonodroid.so and package sizes.
Show a summary per file
File Description
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc Update expected APK contents/sizes after native size change.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc Update expected APK contents/sizes after native size change.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.R8.apkdesc Update expected APK contents/sizes after native size change.
src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc Update expected APK contents/sizes after native size change.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets Stop linking libc++/libunwind for NativeAOT and keep libstdc++compat.a.
src/native/mono/runtime-base/util.hh Add joined-path formatting/join helpers and shared helpers include.
src/native/mono/runtime-base/android-system.hh Add string_view system property overload returning into caller buffer.
src/native/mono/runtime-base/android-system.cc Implement new buffer-based system property retrieval fallback path.
src/native/mono/monodroid/monodroid-glue.cc Replace local-string timing “more info” assembly strings with bounded formatting + heap fallback.
src/native/common/runtime-base/timing-internal.cc Replace property parsing and timing log path composition to use char buffers + join helper.
src/native/common/include/runtime-base/timing-internal.hh Replace dynamic_local_string formatting with snprintf-based message building and add_more_info overloads.
src/native/clr/runtime-base/util.cc Replace dynamic_local_string directory creation with manual mutable buffer + heap fallback.
src/native/clr/runtime-base/logger.cc Switch various path and category parsing to buffer-based helpers and string_view.
src/native/clr/runtime-base/android-system.cc Replace local-string path building with join helpers; refactor DSO path formatting to explicit buffers.
src/native/clr/runtime-base/android-system-shared.cc Change system property retrieval to direct buffer writes; remove small-buffer heap shim in __system_property_get wrapper.
src/native/clr/include/runtime-base/util.hh Add join_paths / format_dso_name helpers; remove path-combine concepts/local-string overloads.
src/native/clr/include/runtime-base/logger.hh Update set_category signature to accept std::string_view.
src/native/clr/include/runtime-base/android-system.hh Introduce buffer-based path formatting helpers; refactor primary override dir formatting.
src/native/clr/include/host/pinvoke-override-impl.hh Build lib*.so pinvoke names via new DSO-name formatter with heap fallback.
src/native/clr/include/host/os-bridge.hh Add missing include for shared utilities used by the header.
src/native/clr/include/host/host-environment.hh Refactor XDG path building to use buffer join logic (and environment setup changes).
src/native/clr/include/host/assembly-store.hh Remove dependency on local-string header.
src/native/clr/host/typemap.cc Build managed type names via bounded formatting + heap fallback.
src/native/clr/host/host.cc Avoid temporary local-string allocations for logging/timing type names.
src/native/clr/host/bridge-processing.cc Add missing includes needed after refactors.
src/native/clr/host/assembly-store.cc Replace local-string timing messages with bounded snprintf messages.

Review details

  • Files reviewed: 6/6 changed files
  • Comments generated: 0
  • Review effort level: Lite

@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from 75d714c to 8021d9f Compare August 26, 2026 13:15
@simonrozsival simonrozsival added the drop-libcpp Work to remove the libc++ dependency from Android NativeAOT label Aug 26, 2026
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from dd62525 to 4126bc1 Compare August 26, 2026 14:39
@simonrozsival

Copy link
Copy Markdown
Member Author

Linker evidence for keeping libstdc++compat.a

The reasoning in this PR is now backed by the linker's own extraction trace, rather than by argument. Replaying the app link with ld.lld --why-extract produces this chain:

libbootstrapperdll.o
  --[RhRegisterOSModule]-->        libRuntime.WorkstationGC.a(Runtime.PrivateLibunwind.o)
  --[operator new(size_t, nothrow_t const&)]-->  libstdc++compat.a(stdcppshim.cpp.o)

Two conclusions follow directly:

  1. The nothrow-new demand originates in dotnet/runtime, not in our code. It comes from the runtime's own bundled libunwind (Runtime.PrivateLibunwind.o), which the ILC SDK already ships libstdc++compat.a to satisfy. Our targets were unconditionally removing that archive with the comment "This library conflicts with static libc++" — true only while libc++ was linked. Keeping it is the entire fix; no C++ runtime shims of our own are needed.

  2. Dropping the NDK's libunwind.a is safe. The runtime carries its own unwinder and never depended on the NDK copy. libRuntime.WorkstationGC.a defines __unw_iterate_dwarf_unwind_cache, UnwindHelpers::StepFrame and UnwindHelpers::GetUnwindProcInfo, and walks DWARF at runtime via dl_iterate_phdr. Stack walking is unaffected.

Symbol-level verification

strings -a on the published .so (llvm-nm is useless here — the shipped .so is stripped, so symbol counts are 0/0 either way):

probe before after
libc++abi 2 0
std::bad_alloc 1 0
terminate_handler 2 0
_Unwind_Resume 2 0

Size impact

Default MAUI app, android-arm64, Release, PublishAot:

before after delta
.so 25,229,320 25,029,728 −199,592 (−0.79%)
.so deflated 9,829,143 9,761,291 −67,852 (−0.69%)
APK 14,903,624 14,833,992 −69,632 (−0.47%)

Why there is no CMake change in this PR

src/native/**/CMakeLists.txt only produces a static archive (libnaot-android.release-static-release.a) — ar bundling, no linking, so there is nothing there to stop linking. The application link is driven entirely by Microsoft.Android.Sdk.NativeAOT.targets, which is what this PR changes.

@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from 4126bc1 to 4dab0f8 Compare August 27, 2026 06:25
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from 4dab0f8 to 5ac0b54 Compare August 27, 2026 10:21
@jonathanpeppers
jonathanpeppers force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from 5ac0b54 to 31a6570 Compare August 27, 2026 14:25
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from 4bc321d to 7f9f586 Compare August 27, 2026 16:09
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch 2 times, most recently from ea894f2 to d292114 Compare August 28, 2026 07:54
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from d292114 to 935ec9d Compare August 28, 2026 08:46
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from 935ec9d to 0b54728 Compare August 28, 2026 08:55
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from 0b54728 to 495d51f Compare August 28, 2026 09:51
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from 495d51f to 051dee2 Compare August 28, 2026 10:29
simonrozsival and others added 3 commits August 28, 2026 14:02
NativeAOT applications no longer need the C++ standard library.  The host
sources compiled into `libnaot-android` do not use any libc++ facility, and
the five C++ allocation-ABI symbols the NativeAOT runtime itself references
(`operator delete`, `operator delete[]`, the two `nothrow` `operator new`
overloads and `std::nothrow`) are already supplied by `libstdc++compat.a`,
which ships in the ILC SDK.

We used to remove `libstdc++compat.a` from the link because it conflicts
with static libc++.  With libc++ gone there is no conflict, so keep it and
drop `libc++_static.a`, `libc++abi.a` and `libunwind.a` instead.

Measured on a default MAUI app (arm64, Release, `PublishAot`):

| | before | after | delta |
|---|---|---|---|
| `.so` | 25,229,320 | 25,029,728 | -199,592 (-0.79%) |
| `.so` deflated in APK | 9,829,143 | 9,761,291 | -67,852 (-0.69%) |
| APK | 14,903,624 | 14,833,992 | -69,632 (-0.47%) |

Contributes to #12139.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
`LinkStandardCPlusPlusLibrary` is no longer about avoiding interference with
static libc++ -- it now keeps the compiler driver from adding a library we
do not link at all.  Also fix the `clang++` note to mention `-nostdlib++`,
which is the flag that suppresses the C++ standard library, rather than
`-nostdlib`, which also drops the CRT.

Note that no explicit `-nostdlib++` is needed today: the final `.so` is
linked by `LinkNativeAotSharedLibrary` via `ld.lld` directly, with fully
explicit inputs and `AllowUndefinedSymbols = false`, so no compiler driver
is in a position to add libc++ implicitly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
Now that NativeAOT applications no longer link libc++, the NativeAOT
runtime packs still carry `libc++_static.a`, `libc++abi.a` and `libunwind.a`
as dead weight.

The NDK redistributables were split into just `System` and `Toolchain`, and
the `Toolchain` group was shipped to both CoreCLR and NativeAOT.  NativeAOT
still needs `crtbegin_so.o`, `crtend_so.o` and `libclang_rt.builtins-*.a`
from that group, so the group cannot simply be dropped.

Introduce a third `CplusPlus` kind for the three C++ archives and ship it
only for CoreCLR, which still links libc++.

Per ABI this removes 18,398,848 bytes from the NativeAOT runtime pack:

| Archive | Size |
| --- | ---: |
| `libc++_static.a` | 15,182,348 |
| `libc++abi.a` | 3,125,348 |
| `libunwind.a` | 91,152 |

Across the three shipped ABIs (`android-arm`, `android-arm64`, `android-x64`)
that is roughly 55 MB.

Contributes to #12139.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/nativeaot-remove-libcpp branch from 051dee2 to 1f93d26 Compare August 28, 2026 12:06
Base automatically changed from dev/simonrozsival/simplify-create-directory to dev/simonrozsival/simplify-dso-name August 28, 2026 12:42
@simonrozsival simonrozsival changed the title [NativeAOT] Stop linking libc++ and libunwind [NativeAOT] Stop linking and shipping libc++ and libunwind Aug 28, 2026
simonrozsival added a commit that referenced this pull request Aug 28, 2026
NativeAOT already had zero references to the C++ standard library
after #12523, so switching `nativeaot-default-common` to
`ANDROID_STL=none` costs nothing and turns any future libc++ use into
a link error instead of silently pulling the archive back in.

Verified on `android-arm64` Release with a clean configure: all three
NativeAOT artifacts -- `libnaot-android.release.so`,
`libnaot-android.release-static-release.a` and
`libxa-java-interop-release.a` -- are byte-for-byte identical to the
`c++_static` build.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drop-libcpp Work to remove the libc++ dependency from Android NativeAOT

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[NativeAOT] Stop linking Android NDK libunwind into applications [NativeAOT] Remove libc++ from libNativeAOT.so

2 participants