Uh oh!
There was an error while loading. Please reload this page.
[NativeAOT] Stop shipping libc++ archives in the runtime packs - #12524
Conversation
There was a problem hiding this comment.
Pull request overview
This PR reduces the size of the shipped Android NativeAOT runtime packs by stopping the inclusion of libc++-related NDK archives (libc++_static.a, libc++abi.a, libunwind.a) that are no longer linked by NativeAOT. It does this by introducing a new NDK redistributable “Kind” (CplusPlus) and only packaging those assets for the CoreCLR runtime flavor.
Changes:
- Introduce a new
_AndroidNdkRedistributable.KindvalueCplusPlusfor the three C++ archives. - Update local pack staging (
src/native/native.targets) to includeCplusPlusassets only for CoreCLR. - Update shipped NuGet runtime pack creation (
Microsoft.Android.Runtime.proj) to includeCplusPlusassets only for CoreCLR.
Show a summary per file
| File | Description |
|---|---|
| src/native/native.targets | Adds CoreCLR-only inclusion of CplusPlus NDK archives when copying assets into the local runtime pack directory layout. |
| build-tools/scripts/Ndk.targets | Re-tags libc++/libunwind NDK archives from Toolchain to the new CplusPlus kind. |
| build-tools/create-packs/Microsoft.Android.Runtime.proj | Adds CoreCLR-only inclusion of CplusPlus NDK archives when creating the shipped runtime pack NuGets. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
Uh oh!
There was an error while loading. Please reload this page.
d165234 to
6cd95f9Compare6cd95f9 to
880e1deCompare880e1de to
98acccdCompare98acccd to
1401b7fCompare1401b7f to
4acae6cCompare4acae6c to
c81efceComparec81efce to
fdd347dComparefdd347d to
7a5a5c7Compare7a5a5c7 to
2872174Compare2872174 to
92b7f75Compare92b7f75 to
15895a5Compare15895a5 to
08b4765CompareNow 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
08b4765 to
a970e21CompareUh oh!
There was an error while loading. Please reload this page.
simonrozsival
commented
Aug 28, 2026
Consolidated into #12523 to reduce the depth of the #12546 stack. No code changed: the commits from this PR are now part of #12523 unmodified, and the resulting tree is byte-identical. This PR sat directly on top of #12523 and touched the same files, so reviewing them together is easier than reviewing the same file across two intermediate states. |
Part of #12533 (the CoreCLR follow-up to #12139). Stacked on top of #12524. ### Why `std::format` is by far the biggest single contributor of libc++ symbols in the native host. Every translation unit that formats *any* value pulls in `std::to_chars` for `float`, `double` **and** `long double`, plus `std::locale`, `std::numpunct` and `std::use_facet` — about 15 symbols per object file, whether or not the code ever formats a floating point number. ### What Converts every `std::format`-based logging call site reachable from the CoreCLR lane to the printf-style `log_debugf` / `log_infof` / `log_warnf` / `log_errorf` functions that already exist in `common/include/shared/log_functions.hh`. This is not just a mechanical swap — unlike the `std::format` macros, these are annotated with `__attribute__((format(printf, ...)))`, so the compiler now **type-checks every format specifier against its argument**. `-Wformat` / `-Werror=format-security` are already enabled, and the build is clean. `Helpers::abort_application (CAT, std::format (…))` call sites move to the previously unused `Helpers::abort_applicationf` overload. That overload was only *defined* in the CoreCLR lane, so an identical definition is added to `mono/shared/helpers.cc`. Several of the converted files live in `common/` rather than `clr/`. Those are header-inlined into CoreCLR objects (`dso-loader.hh`, `mainthread-dso-loader.hh`, `monodroid-dl.hh`, …), so leaving them alone would have left the `std::format` payload in `host.cc.o` regardless. `log_*f` is defined in *both* the CoreCLR and MonoVM lanes, so converting them is safe for Mono too — verified by building it. ### Bonus: fixes a latent NativeAOT logging bug `clr/host/bridge-processing.cc` is compiled into **both** the CoreCLR and the NativeAOT hosts. NativeAOT's `log_types.hh` is a 5-line stub, so those calls fell through to the printf-style macros in `java-interop-logger.h` — which have no `format(printf)` attribute, so nothing warned. In NativeAOT builds they printed a literal `{}` instead of the value. Both sites are now correct in both lanes. ### Results Measured on the arm64 Release CoreCLR archive (`libnet-android.release-static-release.a`), counting undefined `std::__ndk1::*` / `operator new` / `operator delete` / `__cxa_*` symbols. Baseline is this PR's base, #12524: | | before | after | |---|---:|---:| | **undefined libc++ refs** | **142** | **76** (−46%) | | objects with the `std::format` fingerprint | 4 | **0** | Azure DevOps build [#1572420](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1572420) measured the resulting arm64 Release CoreCLR APK savings: | artifact | before | after | saving | |---|---:|---:|---:| | `libmonodroid.so` (uncompressed APK entry) | 1,094,848 B | 564,736 B | **530,112 B (48.4%)** | | Simple APK (R8 on/off) | 7,034,299 B | 6,854,075 B | **180,224 B (2.56%)** | | XForms APK | 18,468,429 B | 18,284,109 B | **184,320 B (1.00%)** | | XForms APK (R8) | 16,317,971 B | 16,133,651 B | **184,320 B (1.13%)** | The unexpectedly large `libmonodroid.so` decrease was independently checked against the CI logs. Build #1572420's `apkdiff` output read the actual signed APK and reported the 564,736-byte entry; the 1,094,848-byte baseline was generated by earlier CI and passed the immediately preceding base validation in build [#1572418](https://dev.azure.com/dnceng-public/public/_build/results?buildId=1572418). The PR-only native patch is unchanged after rebasing. The size reduction is plausible because `libc++_static.a` is linked as a normal archive under `--gc-sections`: removing the last `std::format` references prevents the linker from pulling in its locale and floating-point formatting closure. That code compressed well, which is why the signed APK decreases by 180–184 KB rather than the full 530 KB uncompressed ELF reduction. Per-object, before → after: | object | before | after | |---|---:|---:| | `host.cc.o` | 41 | 27 | | `assembly-store.cc.o` | 35 | 20 | | `typemap.cc.o` | 22 | 5 | | `bridge-processing.cc.o` | 22 | 2 | The `std::format` fingerprint (co-occurrence of `to_chars<float/double/long double>` with `locale` / `numpunct` / `use_facet`) is now **absent from every object file** in the archive. ### Deleting the machinery With the last call site gone, the `std::format` macros and templates in `clr/include/shared/log_types.hh` have no users left, so this PR deletes them too. That file is now identical to the existing NativeAOT stub. This generates no code change on its own — the templates were never instantiated, which is why the counts above already show the `std::format` fingerprint gone — but it means `std::format` can no longer be reintroduced into the CoreCLR host by accident. The `std::string_view` overload of `log_write` goes with it. It was duplicated in the CoreCLR and MonoVM copies of `log_types.hh` and existed only so call sites passing a string literal wouldn't have to write `.data ()`. It had four users, all in `timing-internal.cc`: three pass a literal and now bind to the plain `const char*` overload, and the fourth passes a view produced by `FastTiming::dump ()` and now uses `log_writef ()` with `%.*s`. That last one also retires a fragile invariant. The overload called `.data ()`, so it required a NUL-terminated string — but `dump ()` builds its views from a buffer plus an explicit length. They are all NUL-terminated today and nothing enforced it. `%.*s` honours the length instead. `std::format` is now completely absent from the `clr/`, `common/` and `nativeaot/` trees: | lane | `std::format` | `#include <format>` | |---|---:|---:| | `clr/` | 0 | 0 | | `common/` (shared) | 0 | 0 | | `nativeaot/` | 0 | 0 | | `mono/` | 45 | 2 | ### Verification Built all three runtime lanes locally for `arm64-v8a` Release — **CoreCLR, MonoVM and NativeAOT** — with zero errors and zero new warnings. Building MonoVM caught a real link error (the missing `abort_applicationf` definition) that a CoreCLR-only build would have missed. ### Not in this PR - Converting the remaining `mono/`-only call sites, and deleting the `std::format` machinery from `mono/shared/log_types.hh`. - Removing the remaining `std::string` / `std::function` / `std::mutex` usage from `host.cc` and `assembly-store.cc` (the 76 remaining refs). - Dropping the `CplusPlusArchive` entries from `NativeRuntimeComponents.cs` — the last step, once the archives are genuinely unreferenced.
Contributes to #12139. Builds on #12523, which stopped linking libc++ into NativeAOT applications.
The NativeAOT runtime packs still ship
libc++_static.a,libc++abi.aandlibunwind.aeven though nothing links them any more.Why this needs a new item kind
_AndroidNdkRedistributable(inbuild-tools/scripts/Ndk.targets) tagged NDK files with just two kinds:System—libc.so,libdl.so,liblog.so,libm.so,libz.so— shipped to every runtime.Toolchain—crtbegin_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.oandlibclang_rt.builtins-*.a, so theToolchaingroup 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 localbin/<Config>/lib/packslayout.build-tools/create-packs/Microsoft.Android.Runtime.proj— the shipped NuGet packs.Size
Per ABI, removed from the NativeAOT runtime pack:
libc++_static.alibc++abi.alibunwind.aAcross
android-arm,android-arm64andandroid-x64that is roughly 55 MB of pack content. This does not change application size — that was #12523 — 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:CoreCLR (
android-arm64) — all three are still shipped:Mono is unaffected — it only ever received the
Systemkind.