Skip to content

[native] Remove std::format from the CoreCLR host - #12534

Merged
simonrozsival merged 6 commits into
mainfrom
dev/simonrozsival/clr-remove-std-format
Sep 3, 2026
Merged

[native] Remove std::format from the CoreCLR host#12534
simonrozsival merged 6 commits into
mainfrom
dev/simonrozsival/clr-remove-std-format

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented Aug 27, 2026

Copy link
Copy Markdown
Member

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, doubleandlong 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:

beforeafter
undefined libc++ refs14276 (−46%)
objects with the std::format fingerprint40

Azure DevOps build #1572420 measured the resulting arm64 Release CoreCLR APK savings:

artifactbeforeaftersaving
libmonodroid.so (uncompressed APK entry)1,094,848 B564,736 B530,112 B (48.4%)
Simple APK (R8 on/off)7,034,299 B6,854,075 B180,224 B (2.56%)
XForms APK18,468,429 B18,284,109 B184,320 B (1.00%)
XForms APK (R8)16,317,971 B16,133,651 B184,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. 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:

objectbeforeafter
host.cc.o4127
assembly-store.cc.o3520
typemap.cc.o225
bridge-processing.cc.o222

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:

lanestd::format#include <format>
clr/00
common/ (shared)00
nativeaot/00
mono/452

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.

CopilotAI 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 continues the CoreCLR-host libc++ reduction work by removing std::format usage from CoreCLR-reachable native host code paths and switching logging/abort call sites to the existing printf-annotated log_*f / Helpers::abort_applicationf APIs. This reduces libc++ symbol pull-in while also making format/argument mismatches compiler-checked.

Changes:

  • Replaced std::format-style logging with log_debugf / log_infof / log_warnf / log_errorf across CoreCLR host/runtime-base code and shared native headers.
  • Added Helpers::abort_applicationf definition to the Mono shared lane and migrated CoreCLR abort call sites to the abort_applicationf overload.
  • Adjusted a few debug-only type/name helpers and log strings to avoid std::string_view/std::format dependencies in CoreCLR code paths.
Show a summary per file
FileDescription
src/native/mono/shared/helpers.ccAdds Helpers::abort_applicationf implementation for the Mono lane.
src/native/common/include/runtime-base/timing-internal.hhSwitches warning logs to log_warnf (printf-style).
src/native/common/include/runtime-base/system-loadlibrary-wrapper.hhConverts debug logs to log_debugf.
src/native/common/include/runtime-base/mainthread-dso-loader.hhRemoves <format> usage and converts logs/abort to printf-style formatting.
src/native/common/include/runtime-base/dso-loader.hhConverts loader tracing/error logs to printf-style formatting.
src/native/clr/runtime-base/android-system.ccConverts environment/system-property diagnostics to printf-style logs.
src/native/clr/pinvoke-override/precompiled.ccRemoves <format> usage and converts abort/log statements to abort_applicationf/log_debugf.
src/native/clr/include/runtime-base/monodroid-dl.hhConverts DSO cache and symbol-lookup logs to printf-style formatting.
src/native/clr/include/runtime-base/android-system.hhConverts debug log to log_debugf.
src/native/clr/include/host/typemap.hhReplaces debug label std::string_view constants with const char* and updates debug signatures.
src/native/clr/include/host/pinvoke-override-impl.hhConverts p/invoke override diagnostics to printf-style logging.
src/native/clr/host/typemap.ccConverts typemap debug/release tracing to printf-style logging and updates helper signatures.
src/native/clr/host/host.ccConverts CoreCLR host logging/abort paths to printf-style formatting and adds <cinttypes> usage.
src/native/clr/host/fastdev-assemblies.ccConverts FastDev logs and aborts to printf-style formatting.
src/native/clr/host/bridge-processing.ccConverts GC-related logs to log_*f formatting (including NativeAOT-shared TU).
src/native/clr/host/assembly-store.ccRemoves std::format usage (including hex formatting) and converts cache/store logging and aborts to printf-style formatting.

Review details

  • Files reviewed: 16/16 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment threadsrc/native/common/include/runtime-base/dso-loader.hh Outdated
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/clr-remove-std-format branch from b4e9cd8 to 01a4b88CompareAugust 27, 2026 07:16
@simonrozsival
simonrozsival changed the base branch from main to dev/simonrozsival/nativeaot-drop-libcpp-packsAugust 27, 2026 07:16
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/clr-remove-std-format branch from ef156f8 to dfe614cCompareAugust 27, 2026 09:41
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/clr-remove-std-format branch from 0aa1be2 to 05f8061CompareAugust 27, 2026 10:21
@jonathanpeppers
jonathanpeppersforce-pushed the dev/simonrozsival/clr-remove-std-format branch from 05f8061 to 0d6ad82CompareAugust 27, 2026 14:25
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/clr-remove-std-format branch 2 times, most recently from 03be5c7 to f48725dCompareAugust 27, 2026 16:09
@simonrozsivalsimonrozsival added the drop-libcpp Work to remove the libc++ dependency from Android NativeAOT label Aug 27, 2026
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/clr-remove-std-format branch 2 times, most recently from 8daa480 to 828dfe6CompareAugust 28, 2026 07:54
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/clr-remove-std-format branch from 828dfe6 to c6a73e6CompareAugust 28, 2026 08:47
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/clr-remove-std-format branch from c6a73e6 to 5bae941CompareAugust 28, 2026 08:55
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/clr-remove-std-format branch from 5bae941 to 71c8a79CompareAugust 28, 2026 09:51
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/clr-remove-std-format branch 2 times, most recently from 018724f to e3baa71CompareAugust 28, 2026 12:06
Base automatically changed from dev/simonrozsival/nativeaot-drop-libcpp-packs to dev/simonrozsival/nativeaot-remove-libcppAugust 28, 2026 12:42
@jonathanpeppers
jonathanpeppersforce-pushed the dev/simonrozsival/clr-remove-std-format branch from 7e2707d to 7b1b9a4CompareAugust 31, 2026 13:39
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/clr-remove-std-format branch 2 times, most recently from 9fc677b to 1e82b81CompareSeptember 2, 2026 08:02
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/review

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Android PR Reviewer completed successfully!

Generated by Android PR Reviewer for #12534

@github-actionsgithub-actionsBot 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.

⚠️Needs Changes — 0 errors, 1 warning.

The printf-style migration removes the CoreCLR std::format dependency, keeps format arguments type-checked, handles bounded string views correctly, and the updated APK descriptors agree with the measured size reduction. One hot type-map path loses the old macros’ lazy argument evaluation and now performs GUID-to-text conversion even when assembly logging is disabled; please restore a category guard or equivalent lazy wrapper.

CI is still in progress with no failures reported so far.

Generated by Android PR Reviewer for #12534 · gpt56 · 280.8 AIC · ⌖ 19.3 AIC · ⊞ 25.7K
Comment /review to run again

Comment threadsrc/native/clr/host/typemap.cc Outdated
@simonrozsivalsimonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Sep 2, 2026
Base automatically changed from dev/simonrozsival/nativeaot-remove-libcpp to mainSeptember 2, 2026 13:20
simonrozsivaland others added 6 commits September 2, 2026 15:20
Part of the "drop libc++" effort (#12533). `std::format` is by far the
biggest single contributor of libc++ symbols in the native host: each
translation unit that formats a value pulls in `std::to_chars` for
`float`/`double`/`long double`, `std::locale`, `std::numpunct` and
`std::use_facet`.
This converts every `std::format`-based logging call site in 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`.
Unlike the `std::format` macros, those are annotated with
`__attribute__((format(printf, ...)))`, so the compiler now type-checks
every format specifier against its argument.
`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`.
Also fixes a latent bug: `clr/host/bridge-processing.cc` is compiled into
both the CoreCLR and the NativeAOT hosts, but NativeAOT's `log_types.hh`
is a stub, so those calls fell through to the printf-style macros in
`java-interop-logger.h` and logged a literal `{}` instead of the value.
Measured on the arm64 Release CoreCLR archive
(`libnet-android.release-static-release.a`), undefined libc++ symbols drop
from 144 to 78 (-46%), and the `std::format` fingerprint is gone from
every object file.
Verified by building the CoreCLR, MonoVM and NativeAOT lanes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
- Retry the main-thread DSO loader pipe write on EINTR and treat any
incomplete write as a failure, rather than only checking for -1.
- Drop the duplicated word in the "Trying to load loading shared JNI
library" log message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
With every CoreCLR-lane call site converted to the printf-style `log_*f`
functions, the `std::format`-based macros and templates in
`clr/include/shared/log_types.hh` have no users left. They generated no
code (the templates were never instantiated), but they kept `<format>`
and the whole `std::format` API reachable from every CoreCLR translation
unit. The file is now identical to the NativeAOT stub.
The `std::string_view` overload of `log_write` goes away as well. It was
duplicated in the CoreCLR and MonoVM copies of `log_types.hh` and existed
only so that call sites using a string literal wouldn't have to write
`.data ()`. It had just four users, all in `timing-internal.cc`: three
pass a literal and now rely on the plain `const char*` overload, and the
fourth passes a `std::string_view` produced by `FastTiming::dump ()` and
now uses `log_writef ()` with `%.*s`.
That last one also removes a fragile invariant: the overload called
`.data ()` and so required a NUL-terminated string, but `dump ()` builds
its views from a buffer and an explicit length. They happen to be
NUL-terminated today, and nothing enforced it. `%.*s` honours the length.
`std::format` is now entirely absent from the `clr/`, `common/` and
`nativeaot/` trees; only MonoVM still uses it.
Verified by building the CoreCLR, MonoVM and NativeAOT lanes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
The `log_time` helper in `FastTiming::dump ()` formatted into a fixed
256-byte buffer and silently clamped the length when the message didn't
fit, so an oversized line would have been quietly cut short.
`snprintf` already reports how much room the message needs, so use it:
format into a stack buffer, and on overflow allocate a heap buffer of
exactly the required size and format again. This matches the existing
`format_message`/`build_message` pair used for the individual timing
events, including freeing the buffer only when it differs from the
stack one.
The stack buffer is now `Constants::MAX_LOGCAT_MESSAGE_LENGTH`, the same
size `dump ()` already uses for each event message, so the heap path is
not expected to be taken in practice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Guard the converted printf-style debug calls before constructing MonoGuidString so disabled assembly logging retains the previous lazy argument evaluation.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/clr-remove-std-format branch from 43b8893 to 8627170CompareSeptember 2, 2026 13:20
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@simonrozsival
simonrozsival merged commit 9f9c972 into mainSep 3, 2026
44 checks passed
@simonrozsival
simonrozsival deleted the dev/simonrozsival/clr-remove-std-format branch September 3, 2026 05:58
simonrozsival added a commit that referenced this pull request Sep 3, 2026
…12541)
Part of #12533 (drop `libc++` from the CoreCLR host). Stacked on top of #12534.
`std::mutex` is a thin wrapper over `pthread_mutex_t`, but using it pulls `<mutex>` and out-of-line libc++ symbols into the native host. This PR stores `pthread_mutex_t` directly and provides a small CoreCLR RAII guard so locked scopes retain automatic unlock behavior without libc++.
### What
* Replace CoreCLR's `std::mutex` instances with `pthread_mutex_t`, initialized using `PTHREAD_MUTEX_INITIALIZER`.
* Add `xamarin::android::lock_guard` under the CoreCLR include tree. It takes `pthread_mutex_t&` and calls `pthread_mutex_lock` / `pthread_mutex_unlock`.
* Update CoreCLR's `StartupAwareLock` to take `pthread_mutex_t&` directly.
* Convert shared `Timing` to direct pthread calls and keep it explicitly non-copyable and non-movable.
* Leave `src/native/mono` unchanged; MonoVM retains its existing `mutex` and templated `lock_guard` implementation in `mono/shared/cppcompat.hh`.
`PTHREAD_MUTEX_INITIALIZER` keeps static instances constant-initialized, so this adds no thread-safe initialization guards. The CoreCLR guard is header-only and compiles down to direct pthread calls.
### Effect
This removes the last `#include <mutex>` in the repository:
| | files including `<mutex>` | `std::mutex` / `std::lock_guard` uses |
|---|---:|---:|
| before | 6 | 13 |
| after | **0** | **0** |
It also drops eight undefined libc++ references:
| symbol | before | after |
|---|---:|---:|
| `std::__ndk1::mutex::lock()` | 3 | 0 |
| `std::__ndk1::mutex::unlock()` | 3 | 0 |
| `std::__ndk1::mutex::~mutex()` | 2 | 0 |
| | refs | `__cxa_guard_*` |
|---|---:|---:|
| #12534 (base) | 55 | 10 |
| this PR | **47** | 10 |
The link-time `libc++` requirement only disappears when every cause reaches zero, so this is one of several prerequisites rather than a self-sufficient win. The remaining causes (`operator new`/`delete[]`, `__cxa_guard_*`, `std::string`, `__libcpp_verbose_abort`) are tracked in #12533.
### Verification
* CoreCLR, MonoVM and NativeAOT build clean.
* `fastdev-assemblies.cc` is `#if defined(DEBUG)` and was additionally compiled with `-DDEBUG`.
* The CoreCLR guard compiled with the runtime's no-C++-exceptions settings references only `pthread_mutex_lock` and `pthread_mutex_unlock`; it introduces no C++ exception-runtime or initialization-guard symbols.
* Real `libc++` references: **CoreCLR 55 → 47, NativeAOT 0**. The eight removed references are exactly the mutex members listed above.
* `__cxa_guard_*` undefined references remain at **10**, confirming that the static mutexes remain constant-initialized.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drop-libcppWork to remove the libc++ dependency from Android NativeAOTready-to-reviewThis PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival@jonathanpeppers