Skip to content

[native] Replace std::function with function pointers - #12548

Merged
simonrozsival merged 4 commits into
dev/simonrozsival/clr-timing-free-listfrom
dev/simonrozsival/clr-replace-std-function
Aug 28, 2026
Merged

[native] Replace std::function with function pointers#12548
simonrozsival merged 4 commits into
dev/simonrozsival/clr-timing-free-listfrom
dev/simonrozsival/clr-replace-std-function

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Aug 27, 2026

Copy link
Copy Markdown
Member

Part of #12533. Stacked on top of #12547.

std::function is a type-erasing wrapper: it has to be able to store, copy and destroy an arbitrary callable, and it pulls <functional> into every translation unit that sees the declaration. Neither of the two remaining uses in the CoreCLR host needs any of that.

FastTiming::dump

The line writer was taken by value:

void dump (size_t entries, bool indent, std::function<void(std::string_view const&)> line_writer) noexcept;

Of its two callers, dump_to_logcat passes a captureless lambda and dump_to_file captures a single FILE*. A plain function pointer plus an opaque context covers both:

using LineWriter = void (*) (FILE *output, std::string_view const& line);

void dump (size_t entries, bool indent, LineWriter line_writer, FILE *output) noexcept;

The context is typed as FILE* rather than void*, since the only caller that needs one writes to a file — that avoids a cast in the writer. dump_to_file passes its FILE* through instead of capturing it, and dump stays out of line — no template, so no code duplication per callback type.

The two writers are plain functions in an anonymous namespace rather than captureless lambdas. A lambda converted to a function pointer goes through a compiler generated static invoker, so using functions directly removes a level of indirection.

AssemblyStore::configure_from_payload

static void configure_from_payload (const void *payload_start, const std::function<std::string()>& get_full_store_path) noexcept;

The callback existed only to produce a store path for diagnostics, but the single caller was:

AssemblyStore::configure_from_payload (payload, [store_path]() -> std::string { return std::string { store_path }; });

That wraps a const char * in a std::string purely so that all three use sites can call .c_str () on it again. One of those three is the success-path log_debugf at the end of the function, so the callback runs on every startup and this allocated a string every time.

It now takes the const char * directly and uses the existing optional_string () helper, which also makes it null-safe. The stale comment describing the callback was updated — it claimed the path was only used for invalid payloads, which was not true.

Results

before after
undefined libc++ refs 59 59
libnet-android.release.so 546,904 B 539,912 B

The undefined reference count is unchanged: both uses were fully inlined by the optimizer at -O2, so std::function's machinery was emitted into the objects rather than left as undefined references. What goes away is that generated machinery — 6,992 bytes come off the shared library overall (mostly .text and .bss), plus one std::string allocation per startup.

Verification

  • CoreCLR and MonoVM both build clean (only the pre-existing format_managed_type_name warning).
  • Reference counts and sizes measured with llvm-nm --undefined-only / llvm-size over the same build tree, before and after.
  • <functional> is no longer included by timing-internal.hh or assembly-store.hh. The only remaining std::function references in src/native are in src/native/mono/, which this stack does not touch.

On the unchanged reference count

Worth recording, since it is easy to misread as "no progress": llvm-nm --undefined-only lists each undefined symbol once per object file, so this metric counts (object, symbol) pairs rather than call sites. host.cc.o still has 24 relocations against operator delete and 8 against ~basic_string from its other std::string uses, so dropping one use cannot remove the symbol from its undefined list. Measured per object across this change:

object undefined symbol set relocations .text
host.cc.o identical 60 → 56 30,550 → 30,026
assembly-store.cc.o identical 84 → 83 24,424 → 24,106

The count only moves when the last use of a symbol in a given file goes away, so it behaves as a per-file cliff rather than a gradual measure.

The lambdas remaining inside dump are called directly instead of being converted to function pointers, so the optimizer already inlines them; replacing them with named functions measured 2 bytes larger, so they were left as they are.

Copilot AI lite review requested due to automatic review settings August 27, 2026 20:11

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.

Copilot review overview

Review tier: Lite
Findings: 3 Low severity

New issues introduced by this change (3)
Severity Finding
Low severity src/​native/​common/​include/​runtime-base/​timing-internal.hh — 💡 Documentation — The new comment reads a bit awkwardly (“thread state through”). Consider…
Low severity src/​native/​common/​runtime-base/​timing-internal.cc — 💡 Maintainability — Consider declaring line_writer as LineWriter instead of auto so it’s…
Low severity src/​native/​clr/​include/​host/​assembly-store.hh — 💡 Documentation — Since the new API accepts a raw pointer and the implementation is explicitly…
What changed in this PR

This PR reduces C++ standard library surface area in the CoreCLR host by replacing the remaining std::function callback parameters with lower-overhead alternatives (function pointer + void* context, and const char*), helping the broader effort to remove libc++ from the CoreCLR host and reducing startup allocations / binary size.

Changes:

  • Reworked FastTiming::dump to use a function pointer callback (LineWriter) plus an opaque context pointer instead of std::function.
  • Updated AssemblyStore::configure_from_payload to take const char *store_path directly, eliminating a per-startup std::string allocation and simplifying diagnostics.
  • Removed now-unneeded <functional> includes from the affected headers.
File Description
src/​native/​common/​runtime-base/​timing-internal.cc Switches FastTiming::dump callback invocation to (context, line); updates logcat/file dump call sites accordingly.
src/​native/​common/​include/​runtime-base/​timing-internal.hh Removes <functional>, introduces LineWriter typedef, updates dump signature.
src/​native/​clr/​include/​host/​assembly-store.hh Removes <functional>, updates configure_from_payload signature and comment.
src/​native/​clr/​host/​host.cc Updates the single call site to pass store_path directly.
src/​native/​clr/​host/​assembly-store.cc Replaces callback usage with optional_string (store_path) for null-safe diagnostics and debug logging.

Comment thread src/native/common/include/runtime-base/timing-internal.hh Outdated
Comment thread src/native/common/runtime-base/timing-internal.cc Outdated
Comment thread src/native/clr/include/host/assembly-store.hh Outdated
@simonrozsival simonrozsival added the drop-libcpp Work to remove the libc++ dependency from Android NativeAOT label Aug 27, 2026
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/clr-replace-std-function branch from 1f91b00 to a35febf Compare August 27, 2026 21:42
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/clr-replace-std-function branch from eac3f03 to 84c4781 Compare August 28, 2026 07:14
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/clr-replace-std-function branch from 84c4781 to bc86ae1 Compare August 28, 2026 07:54
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/clr-replace-std-function branch from bc86ae1 to 3388358 Compare August 28, 2026 08:47
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/clr-replace-std-function branch from 3388358 to 352372d Compare August 28, 2026 08:56
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/clr-replace-std-function branch from 352372d to 77419ba Compare August 28, 2026 09:51
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/clr-replace-std-function branch from 77419ba to 8df7b68 Compare August 28, 2026 10:29
simonrozsival and others added 4 commits August 28, 2026 14:02
`std::function` is a type-erasing wrapper which needs to store, copy and
destroy an arbitrary callable, and it pulls `<functional>` into every
translation unit that sees the declaration.  Neither of the two uses in the
CoreCLR host needs any of that.

`FastTiming::dump` took its line writer as `std::function<void(std::string_view const&)>`
by value.  Of its two callers one passes a captureless lambda and the other
captures a single `FILE*`, so a plain function pointer plus an opaque
`void *context` covers both:

    using LineWriter = void (*) (void *context, std::string_view const& line);

`AssemblyStore::configure_from_payload` took a `const std::function<std::string()>&`
used only to produce a path for diagnostics.  Its only caller wrapped a
`const char *` in a `std::string` just so that the callee could call
`c_str ()` on it again, and the callback is invoked unconditionally in the
success path, so this allocated a string on every startup.  It now takes the
`const char *` directly.

This does not change the number of undefined libc++ references, since both
uses were fully inlined by the optimizer, but it removes the generated
machinery: `libnet-android.release.so` shrinks by 6,976 bytes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
Both `dump` callers either write to a file or ignore the context entirely, so
there is no need for the context to be `void*`.  Typing it as `FILE*` removes
the `static_cast` in the file line writer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
The two line writers were captureless lambdas converted to function pointers
at the call site.  That conversion goes through a compiler generated static
invoker, so making them plain functions in an anonymous namespace removes a
level of indirection: `libnet-android.release.so` shrinks by a further 56
bytes.

The remaining lambdas inside `dump` are called directly rather than converted
to function pointers, so the optimizer already inlines them completely -
replacing those measured 2 bytes *larger*, so they are left alone.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
Addresses review feedback: `configure_from_payload()` takes a raw `const char*`
and every use of it goes through `optional_string ()`, so the header comment now
says explicitly that passing `nullptr` is allowed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0a35a0db-502d-48c0-8468-e73b5dd0ab2e
@simonrozsival
simonrozsival force-pushed the dev/simonrozsival/clr-replace-std-function branch from 8df7b68 to 278ac99 Compare August 28, 2026 12:06
Base automatically changed from dev/simonrozsival/timing-open-sequences to dev/simonrozsival/clr-timing-free-list August 28, 2026 12:42
@simonrozsival
simonrozsival merged commit 278ac99 into main Aug 28, 2026
7 of 44 checks passed
@simonrozsival
simonrozsival deleted the dev/simonrozsival/clr-replace-std-function branch August 28, 2026 12:42
@simonrozsival

Copy link
Copy Markdown
Member Author

Consolidated into #12545 to reduce the depth of the #12546 stack.

No code changed: the commits from this PR are now part of #12545 unmodified, and the resulting tree is byte-identical. This PR sat directly on top of #12545 and touched the same files, so reviewing them together is easier than reviewing the same file across two intermediate states.

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.

2 participants