Drop libc++ from Android NativeAOT linking - #11311

Closed
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp
Closed

Drop libc++ from Android NativeAOT linking#11311
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

This removes the Android NativeAOT app link-time dependency on libc++.

The branch now:

  • removes the explicit NativeAOT libc++_static.a and libc++abi.a link inputs;
  • removes libc++ packaging from the Android native runtime component list;
  • keeps the final NativeAOT app link on the direct ld.lld/NativeLinker path;
  • reduces NativeAOT-reachable host code that pulled in C++ runtime/STL symbols;
  • adds NativeAOT-local C++ allocation/nothrow shims for the remaining runtime-pack allocation references;
  • shares GC bridge processing between CoreCLR and NativeAOT without tsl::robin_map or other libc++-requiring containers;
  • adds printf-style native logging helpers so call sites can avoid repeated snprintf boilerplate and skip formatting when logging is disabled.

Context

This is related to #9926 and the NDK r29 NativeAOT linking work.

The relevant background is that Android NativeAOT should avoid depending on libc++ instead of working around duplicate libunwind symbols or switching to shared libc++. Related references:

GC bridge / Java peer shape history

The GC bridge sharing in this PR exposed one remaining runtime-specific difference: classic Mono/CoreCLR Java peers use mono.android.* and monodroidAddReference() / monodroidClearReferences(), while NativeAOT trimmable proxy objects use net.dot.jni.GCUserPeerable and jiAddManagedReference() / jiClearManagedReferences().

Brief history:

So the current difference appears to be historical layering rather than a fundamental GC bridge requirement: Mono/CoreCLR kept the older Android-compatible monodroid* Java peer API, while NativeAOT trimmable support was built around new generated net.dot.jni.* proxy sources that avoid the legacy Java.Interop native-registration shape. Follow-up issue: #11526 tracks unifying these Java peer reference APIs, likely by moving the trimmable path toward a single net.dot.* shape while NativeAOT is still experimental.

Size impact

Latest measurements on this branch use samples/NativeAOT/NativeAOT.csproj built in Release with _AndroidTypeMapImplementation=trimmable. APK sizes are for the signed APK.

ArtifactParent libc++ baselineNo-libc++ buildDifference
arm64 APK1,575,849 B1,382,336 B-193,513 B (-12.28%)
x64 APK1,639,459 B1,439,677 B-199,782 B (-12.19%)
arm64 libNativeAOT.so3,481,880 B2,943,232 B-538,648 B (-15.47%)
x64 libNativeAOT.so3,404,896 B2,866,728 B-538,168 B (-15.81%)

libNativeAOT.so is the per-ABI native shared library packaged in the APK, for example lib/arm64-v8a/libNativeAOT.so. It is not the whole Android app package; the APK also contains manifest, resources, Java stubs/classes, signatures, and packaging assets.

Validation

Latest local validation:

  • git diff --check
  • make prepare CONFIGURATION=Debug
  • make all CONFIGURATION=Debug progressed through native-mono, native-nativeaot, and native-clr; the remaining failure was a stale test restore asset issue unrelated to native code.
  • ./dotnet-local.sh build src/native/native-nativeaot.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-clr.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-mono.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-arm64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-x64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Debug -t:Install -p:RuntimeIdentifier=android-arm64
  • launched samples/NativeAOT on an arm64 emulator and confirmed Application.OnCreate() and MainActivity.OnCreate() ran without fatal logcat entries.

Previous branch validation:

  • dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj --no-restore — 457 passed.
  • rebuilt src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj;
  • rebuilt NativeAOT runtime archives for android-arm64 and android-x64;
  • built samples/NativeAOT/NativeAOT.csproj with _AndroidTypeMapImplementation=trimmable for android-arm64 and android-x64;
  • verified generated NativeAOT link response files contain no libc++/libc++abi inputs;
  • verified produced APKs contain no libc++ entries;
  • verified llvm-nm -u reports no undefined C++ runtime-looking symbols in the final libNativeAOT.so outputs;
  • smoke-tested the arm64 APK on an arm64 emulator: libNativeAOT.so loaded without libc++, MainApplication and MainActivity native callbacks ran, MainActivity.OnCreate() logged, and the process remained alive.

Notes

The x64 APK builds and packages without libc++, but local x64 runtime validation still needs an x64 emulator host because Android emulator x86_64 system images are not accepted on Apple Silicon/aarch64 hosts.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

Emulator validation update

I tested the signed NativeAOT + trimmable typemap sample APK on an arm64 emulator after rebuilding/overlaying the local validation packs.

What works:

  • The arm64 APK installs and launches on emulator-5554.
  • lib/arm64-v8a/libNativeAOT.so loads successfully with no libc++ packaged in the APK.
  • Startup reaches managed code: MainApplication and MainActivity native callbacks run, MainActivity.OnCreate() logs, and the process remains alive.

Important caveats:

  • This arm64 smoke currently requires trimmable NativeAOT startup fixes plus two validation-only workarounds:
    • pointing ILC TypeMapEntryAssembly at the app typemap (_NativeAOT.TypeMap) instead of the root _Microsoft.Android.TypeMaps, because scanning _Mono.Android.TypeMap currently fails on Android.Text.IInputType being present in the ref assembly but absent from the runtime assembly;
    • skipping UncaughtExceptionMarshaler setup for trimmable NativeAOT to avoid the IUncaughtExceptionHandler/JavaProxyThrowable startup path.
  • I do not consider those two workarounds shippable as-is; they need separate root-cause fixes before this can be more than a feasibility proof.
  • x64 builds and packages without libc++ (-nostdlib++ is in the link response and no libc++ entries are in the APK), but I could not run the x64 APK locally: the Android emulator rejects x86_64 system images on this Apple Silicon host (Avd's CPU Architecture 'x86_64' is not supported by the QEMU2 emulator on aarch64 host). x64 runtime behavior still needs validation on an x64 emulator host.

@simonrozsivalsimonrozsival changed the title [WIP] Explore dropping libc++ from NativeAOT linking[WIP] Drop libc++ from Android NativeAOT linkingMay 12, 2026
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 23, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved by merging origin/main into this branch and fixing all reported conflicts in commit 6e8b48a.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved locally by merging origin/main and fixing the conflict hunks in commit 0199a84. Push from the agent is currently blocked by branch protection (GH013: Cannot update this protected ref), so this commit is ready but not yet on the remote branch.

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Done in 1c6205f. I merged latest origin/main into this branch and resolved all merge conflicts in the affected typemap generator/targets/tests files.

simonrozsivaland others added 5 commits May 27, 2026 17:35
Remove the explicit NativeAOT final-link dependency on libc++/libc++abi and keep the Android NativeAOT link guarded with -nostdlib++.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Centralize gref log message formatting in OSBridge so call sites can pass printf-style arguments directly and formatting is skipped when gref logging is disabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep gref file guards at the call sites, allow literal single-argument gref log calls, and centralize printf-style log formatting helpers for CLR native logging.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/nativeaot-drop-libcpp branch from 1c6205f to 6a4feeaCompareMay 27, 2026 16:12
@simonrozsivalsimonrozsival changed the title [WIP] Drop libc++ from Android NativeAOT linkingDrop libc++ from Android NativeAOT linkingMay 27, 2026
@simonrozsival
simonrozsival marked this pull request as ready for review May 27, 2026 16:20
CopilotAI review requested due to automatic review settings May 27, 2026 16:20

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 (draft/WIP) explores removing the Android NativeAOT link-time dependency on libc++/libc++abi by adjusting MSBuild NativeAOT linking inputs and runtime component packaging, and by refactoring native runtime code paths to avoid pulling in C++ standard library symbols (including introducing minimal C++ allocation/nothrow shims).

Changes:

  • Remove explicit libc++/libc++abi static link inputs and runtime component entries for NativeAOT Android builds.
  • Add NativeAOT-local C++ runtime shims (operator new/delete + std::nothrow) and refactor multiple native components to avoid std::format/other STL usage.
  • Refactor CLR/native logging and GC bridge processing (including callback plumbing for bridge processing and temporary peer map implementation changes).

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.csDrops libc++/libc++abi from the known runtime archive list.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targetsRemoves libc++/libc++abi from NativeAOT link inputs; updates linker/stdlib commentary.
src/native/nativeaot/include/host/host.hhAdds NativeAOT host header shim include.
src/native/nativeaot/include/host/bridge-processing.hhAdapts NativeAOT bridge-processing to callback-based shared implementation.
src/native/nativeaot/host/internal-pinvoke-stubs.ccSimplifies abort path for unimplemented pinvokes to avoid extra std deps.
src/native/nativeaot/host/host.ccRemoves std::format usage from NativeAOT host logging path.
src/native/nativeaot/host/cxx-shims.ccAdds minimal C++ allocation / nothrow shims to avoid libc++ dependency.
src/native/nativeaot/host/CMakeLists.txtAdds cxx-shims.cc to NativeAOT host build.
src/native/nativeaot/host/bridge-processing.ccRemoves std::format usage and wires callbacks for NativeAOT bridge processing.
src/native/common/include/shared/helpers.hhAdds Helpers::abort_applicationf formatted abort helper.
src/native/common/include/shared/cpp-util.hhRemoves C++ ranges/string allocations from diagnostics helpers and logging.
src/native/common/include/runtime-base/timing.hhReplaces std::format timing log formatting with snprintf.
src/native/common/include/runtime-base/timing-internal.hhReplaces std::format timing/internal warnings with snprintf-based logging.
src/native/common/include/runtime-base/strings.hhReplaces some std-container usage and updates formatting/logging to avoid STL pulls.
src/native/common/include/runtime-base/jni-wrappers.hhReplaces new[]/delete[] usage with malloc + placement-new where needed.
src/native/clr/shared/log_functions.ccIntroduces vprintf-style log helpers (log_writev, log_*_fmt).
src/native/clr/shared/helpers.ccAdds implementation of Helpers::abort_applicationf; switches some fatal logging to printf-style.
src/native/clr/runtime-base/util.ccUpdates logging calls to new printf-style logging helpers.
src/native/clr/runtime-base/logger.ccRefactors gref/lref log path storage away from std::string; updates open/log messages.
src/native/clr/runtime-base/android-system-shared.ccReplaces some new[]/delete[] allocations and formatting with malloc/snprintf and fmt logging.
src/native/clr/include/shared/log_types.hhAdds printf-style logging APIs and adjusts macro behavior for XA_HOST_NATIVEAOT builds.
src/native/clr/include/runtime-base/util.hhSwitches to formatted abort/log helpers; adds mmap failure formatted aborts and info logging.
src/native/clr/include/runtime-base/android-system.hhChanges override-dir storage away from std::string for NativeAOT compatibility; API now returns const char*.
src/native/clr/include/host/os-bridge.hhAdds formatted gref logging overloads and refactors internal logging helpers signatures.
src/native/clr/include/host/host-environment.hhUpdates debug/warn logging calls to avoid std::format and skip work when disabled.
src/native/clr/include/host/gc-bridge.hhReplaces std::thread/semaphore with pthread/sem_t for compatibility and reduced STL usage.
src/native/clr/include/host/bridge-processing.hhRemoves now-unneeded CoreCLR no-op overrides after shared callback refactor.
src/native/clr/include/host/bridge-processing-shared.hhIntroduces callback plumbing and switches temporary peer map to robin_map.
src/native/clr/host/os-bridge.ccRefactors stack-trace/gref logging to avoid std::format and centralize formatted logging.
src/native/clr/host/internal-pinvokes-shared.ccUpdates managed->native log forwarding to respect category enablement and use log_write.
src/native/clr/host/host-shared.ccReplaces std::format/string_view literal usage in error logging with printf-style.
src/native/clr/host/gc-bridge.ccMigrates to sem_wait/atomic builtins and updates logging formatting.
src/native/clr/host/fastdev-assemblies.ccUpdates override-dir handling to match AndroidSystem API change to const char*.
src/native/clr/host/bridge-processing.ccImplements callback-based bridge processing shared logic and temporary peer lifecycle handling.
Comments suppressed due to low confidence (1)

src/native/clr/host/os-bridge.cc:203

  • OSBridge::log_it() logs the main line to logcat unconditionally via log_write(...), even when logcat_enabled is false. This makes gref/lref logging emit to logcat even in the “file-only” configuration, and the logcat_enabled parameter only affects stack traces. Consider only calling log_write when logcat_enabled is true (and keep file writes independent), so behavior matches Logger::{gref,lref}_to_logcat().
void OSBridge::log_it (LogCategories category, const char *line, FILE *to, const char *const from, bool logcat_enabled) noexcept
{
log_write (category, LogLevel::Info, line);
// We skip logcat here when logging to file is enabled because _write_stack_trace will output to logcat as well, if enabled
if (to == nullptr) {
if (logcat_enabled) {
_write_stack_trace (nullptr, from, category);
}

Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh Outdated
simonrozsivaland others added 5 commits May 27, 2026 20:58
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the missing standard and logging declarations needed by the native runtime headers when they are compiled directly by the Android runtime ninja builds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid ambiguous gref log overload resolution, insert temporary GC bridge peers into robin_map without mutating through the iterator proxy, and include robin_map headers in the NativeAOT host build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the robin_map-backed temporary peer lookup for the CLR host, but use a simple indexed JNI handle table for the NativeAOT host so app-linked static runtimes do not pull in libc++/c++abi symbols.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid allocating a temporary peer slot for every GC bridge component in the NativeAOT host. The NativeAOT path now stores only the temporary peers it creates, while the CLR host continues to use robin_map.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsivaland others added 7 commits May 28, 2026 07:15
Remove the CoreCLR-only robin_map implementation so bridge processing uses the same sparse temporary peer list for both CoreCLR and NativeAOT. Leave a note about a possible future lookup optimization using the SCC Count field.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Encapsulate temporary peer storage in a dedicated RAII type and encode temporary peer indexes in the SCC count while the bridge owns the GC cross-reference arguments. Share the implementation across CoreCLR and NativeAOT, keeping only the NativeAOT GCUserPeerable callbacks runtime-specific.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert native runtime logging macros to use printf-style helpers across CLR, Mono, and NativeAOT. This removes the NativeAOT-only preformatted logging branch while preserving category gating for debug and info logs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid calling GetMethodID with a null jclass when initializing the temporary peer map. This preserves the intended abort message when the runtime field lookup fails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explain that these shims intentionally cover only the no-libc++ allocation symbols needed by the NativeAOT runtime pack while native code is built without C++ exceptions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop libc++ from NativeAOT linking reduces libUnnamedProject.so by
~625KB. Refresh the BuildReleaseArm64 size-regression reference
descriptions from the latest CI build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 9, 2026 21:54
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in 8dcf139 by merging the latest origin/main into this branch and fixing the remaining conflicts in the CoreCLR pinvoke override files.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 26, 2026 21:43
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in d50cd69 and follow-up 921fef6. I merged the latest origin/main, fixed the remaining conflicts in the GC bridge and apkdesc baselines, and updated the external/xamarin-android-tools/external/Java.Interop submodule pointers from the merged mainline state.

@simonrozsivalsimonrozsival added the drop-libcpp Work to remove the libc++ dependency from Android NativeAOT label Jul 16, 2026
simonrozsival added a commit that referenced this pull request Jul 17, 2026
Use the negative-index TemporaryPeerMap approach from #11311 so CoreCLR and NativeAOT no longer need robin-map for GC bridge processing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa
simonrozsival added a commit that referenced this pull request Jul 20, 2026
## Summary
Replace the shared CoreCLR/NativeAOT GC bridge's temporary-peer `std::unordered_map` with an allocation-backed `TemporaryPeerMap`.
The design follows the approach previously developed in #11311: an empty strongly connected component temporarily carries an encoded peer-array index in `StronglyConnectedComponent.Count`, so the bridge does not need a general-purpose C++ hash table during its scoped cross-reference pass.
Split from #12142. The two PRs are independent and can merge in either order. Part of #12139.
## Background
During GC bridge processing, each strongly connected component (SCC) must behave like one Java object:
- `Count == 1`: the existing Java peer represents the SCC directly;
- `Count > 1`: the bridge adds circular references so all peers remain alive or are collected together;
- `Count == 0`: there is no Java peer, so the bridge creates a temporary `mono.android.GCUserPeer` solely to represent that SCC while cross-SCC references are established.
The previous implementation stored those temporary peers in `std::unordered_map<size_t, jobject>`, keyed by SCC index. The required key set and capacity are already known before processing begins, and lookup is only needed within one short scope, making a hash table unnecessary.
## Implementation
Files:
- `src/native/clr/include/host/bridge-processing-shared.hh`
- `src/native/clr/host/bridge-processing.cc`
### `TemporaryPeerMap` lifetime
1. The constructor scans all SCCs, rejects pre-existing marker values, and counts exactly how many temporary peers are required.
2. If none are required, it performs no allocation.
3. Otherwise it reserves JNI local-reference capacity for all temporary peers plus slack, then allocates one zero-initialized `jobject` array with `calloc`.
4. `add()` creates the temporary `GCUserPeer`, stores it in the next array slot, and writes the encoded slot index into the SCC's `Count` field.
5. Cross-reference target selection detects the encoded marker and retrieves the peer directly from the array.
6. At the end of the scoped cross-reference pass, the destructor deletes every temporary JNI local reference, resets every marked SCC to `Count == 0`, frees the array, and clears its bookkeeping.
7. Normal weak-global-reference processing starts only after the destructor has restored the original SCC shape.
### Index encoding
`Count` is unsigned, so the temporary index is stored as `~index`, which has the same bit pattern as `-(index + 1)`:
- index zero remains representable;
- the high bit acts as the temporary-peer marker;
- encoding rejects indexes that already use the marker bit;
- decoding verifies the marker and bounds-checks the resulting array index;
- the constructor verifies that runtime-provided SCC counts do not already use the reserved marker space.
### JNI initialization and safety
- cache the `mono.android.GCUserPeer` class and constructor during runtime initialization;
- preserve the existing cached `mono.android.IGCUserPeer` method IDs used for reference callbacks;
- reserve local-reference capacity before creating a potentially large temporary-peer set;
- clear and log an `EnsureLocalCapacity` failure consistently with the previous implementation;
- fail fast on allocation failure, peer-construction failure, invalid markers, capacity overruns, missing peers, and out-of-range indexes;
- preserve existing fail-fast handling for Java exceptions raised by `monodroidAddReference` or `monodroidClearReferences`;
- explicitly delete copy and move construction/assignment so the owning array and JNI local references cannot be shallow-copied.
## Behavior preserved
- temporary peers remain alive until every cross-SCC reference has been added;
- temporary local references are released before the Java GC is triggered;
- zero-, one-, and multi-peer SCC handling remains unchanged;
- cross-reference source/destination selection and `refs_added` bookkeeping remain unchanged;
- the runtime receives its SCC array back with all temporary markers removed;
- CoreCLR and NativeAOT continue to use the same shared bridge implementation and host-specific peer callback hooks.
## Scope and non-goals
- This PR changes only temporary-peer storage; it does not change the GC bridge graph algorithm or collection policy.
- It does not change Java peer APIs, reference callback names, or GC trigger behavior.
- It does not introduce robin-map or another replacement hash table.
- The unrelated logging/path/source-location ownership cleanup remains in #12142.
## Expected impact
- remove `std::unordered_map` from shared CoreCLR/NativeAOT temporary-peer processing;
- remove the associated `std::__ndk1::__next_prime` and hash-table allocation/code roots;
- replace per-node hash-table bookkeeping with one exact-size array allocation;
- make temporary JNI reference ownership and SCC marker restoration explicit through RAII;
- preserve GC bridge semantics while reducing C++ standard-library reachability.
## Validation
The implementation is the isolated GC bridge change previously carried in #12142, plus explicit non-copyable/non-movable ownership semantics for `TemporaryPeerMap`.
- `git diff --check` passed;
- the ownership hardening is declaration-only and introduces no runtime code;
- CI is validating head `d4315727e`.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

drop-libcppWork to remove the libc++ dependency from Android NativeAOT

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Drop libc++ from Android NativeAOT linking - #11311

Closed
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp
Closed

Drop libc++ from Android NativeAOT linking#11311
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

This removes the Android NativeAOT app link-time dependency on libc++.

The branch now:

  • removes the explicit NativeAOT libc++_static.a and libc++abi.a link inputs;
  • removes libc++ packaging from the Android native runtime component list;
  • keeps the final NativeAOT app link on the direct ld.lld/NativeLinker path;
  • reduces NativeAOT-reachable host code that pulled in C++ runtime/STL symbols;
  • adds NativeAOT-local C++ allocation/nothrow shims for the remaining runtime-pack allocation references;
  • shares GC bridge processing between CoreCLR and NativeAOT without tsl::robin_map or other libc++-requiring containers;
  • adds printf-style native logging helpers so call sites can avoid repeated snprintf boilerplate and skip formatting when logging is disabled.

Context

This is related to #9926 and the NDK r29 NativeAOT linking work.

The relevant background is that Android NativeAOT should avoid depending on libc++ instead of working around duplicate libunwind symbols or switching to shared libc++. Related references:

GC bridge / Java peer shape history

The GC bridge sharing in this PR exposed one remaining runtime-specific difference: classic Mono/CoreCLR Java peers use mono.android.* and monodroidAddReference() / monodroidClearReferences(), while NativeAOT trimmable proxy objects use net.dot.jni.GCUserPeerable and jiAddManagedReference() / jiClearManagedReferences().

Brief history:

So the current difference appears to be historical layering rather than a fundamental GC bridge requirement: Mono/CoreCLR kept the older Android-compatible monodroid* Java peer API, while NativeAOT trimmable support was built around new generated net.dot.jni.* proxy sources that avoid the legacy Java.Interop native-registration shape. Follow-up issue: #11526 tracks unifying these Java peer reference APIs, likely by moving the trimmable path toward a single net.dot.* shape while NativeAOT is still experimental.

Size impact

Latest measurements on this branch use samples/NativeAOT/NativeAOT.csproj built in Release with _AndroidTypeMapImplementation=trimmable. APK sizes are for the signed APK.

ArtifactParent libc++ baselineNo-libc++ buildDifference
arm64 APK1,575,849 B1,382,336 B-193,513 B (-12.28%)
x64 APK1,639,459 B1,439,677 B-199,782 B (-12.19%)
arm64 libNativeAOT.so3,481,880 B2,943,232 B-538,648 B (-15.47%)
x64 libNativeAOT.so3,404,896 B2,866,728 B-538,168 B (-15.81%)

libNativeAOT.so is the per-ABI native shared library packaged in the APK, for example lib/arm64-v8a/libNativeAOT.so. It is not the whole Android app package; the APK also contains manifest, resources, Java stubs/classes, signatures, and packaging assets.

Validation

Latest local validation:

  • git diff --check
  • make prepare CONFIGURATION=Debug
  • make all CONFIGURATION=Debug progressed through native-mono, native-nativeaot, and native-clr; the remaining failure was a stale test restore asset issue unrelated to native code.
  • ./dotnet-local.sh build src/native/native-nativeaot.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-clr.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-mono.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-arm64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-x64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Debug -t:Install -p:RuntimeIdentifier=android-arm64
  • launched samples/NativeAOT on an arm64 emulator and confirmed Application.OnCreate() and MainActivity.OnCreate() ran without fatal logcat entries.

Previous branch validation:

  • dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj --no-restore — 457 passed.
  • rebuilt src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj;
  • rebuilt NativeAOT runtime archives for android-arm64 and android-x64;
  • built samples/NativeAOT/NativeAOT.csproj with _AndroidTypeMapImplementation=trimmable for android-arm64 and android-x64;
  • verified generated NativeAOT link response files contain no libc++/libc++abi inputs;
  • verified produced APKs contain no libc++ entries;
  • verified llvm-nm -u reports no undefined C++ runtime-looking symbols in the final libNativeAOT.so outputs;
  • smoke-tested the arm64 APK on an arm64 emulator: libNativeAOT.so loaded without libc++, MainApplication and MainActivity native callbacks ran, MainActivity.OnCreate() logged, and the process remained alive.

Notes

The x64 APK builds and packages without libc++, but local x64 runtime validation still needs an x64 emulator host because Android emulator x86_64 system images are not accepted on Apple Silicon/aarch64 hosts.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

Emulator validation update

I tested the signed NativeAOT + trimmable typemap sample APK on an arm64 emulator after rebuilding/overlaying the local validation packs.

What works:

  • The arm64 APK installs and launches on emulator-5554.
  • lib/arm64-v8a/libNativeAOT.so loads successfully with no libc++ packaged in the APK.
  • Startup reaches managed code: MainApplication and MainActivity native callbacks run, MainActivity.OnCreate() logs, and the process remains alive.

Important caveats:

  • This arm64 smoke currently requires trimmable NativeAOT startup fixes plus two validation-only workarounds:
    • pointing ILC TypeMapEntryAssembly at the app typemap (_NativeAOT.TypeMap) instead of the root _Microsoft.Android.TypeMaps, because scanning _Mono.Android.TypeMap currently fails on Android.Text.IInputType being present in the ref assembly but absent from the runtime assembly;
    • skipping UncaughtExceptionMarshaler setup for trimmable NativeAOT to avoid the IUncaughtExceptionHandler/JavaProxyThrowable startup path.
  • I do not consider those two workarounds shippable as-is; they need separate root-cause fixes before this can be more than a feasibility proof.
  • x64 builds and packages without libc++ (-nostdlib++ is in the link response and no libc++ entries are in the APK), but I could not run the x64 APK locally: the Android emulator rejects x86_64 system images on this Apple Silicon host (Avd's CPU Architecture 'x86_64' is not supported by the QEMU2 emulator on aarch64 host). x64 runtime behavior still needs validation on an x64 emulator host.

@simonrozsivalsimonrozsival changed the title [WIP] Explore dropping libc++ from NativeAOT linking[WIP] Drop libc++ from Android NativeAOT linkingMay 12, 2026
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 23, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved by merging origin/main into this branch and fixing all reported conflicts in commit 6e8b48a.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved locally by merging origin/main and fixing the conflict hunks in commit 0199a84. Push from the agent is currently blocked by branch protection (GH013: Cannot update this protected ref), so this commit is ready but not yet on the remote branch.

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Done in 1c6205f. I merged latest origin/main into this branch and resolved all merge conflicts in the affected typemap generator/targets/tests files.

simonrozsivaland others added 5 commits May 27, 2026 17:35
Remove the explicit NativeAOT final-link dependency on libc++/libc++abi and keep the Android NativeAOT link guarded with -nostdlib++.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Centralize gref log message formatting in OSBridge so call sites can pass printf-style arguments directly and formatting is skipped when gref logging is disabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep gref file guards at the call sites, allow literal single-argument gref log calls, and centralize printf-style log formatting helpers for CLR native logging.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/nativeaot-drop-libcpp branch from 1c6205f to 6a4feeaCompareMay 27, 2026 16:12
@simonrozsivalsimonrozsival changed the title [WIP] Drop libc++ from Android NativeAOT linkingDrop libc++ from Android NativeAOT linkingMay 27, 2026
@simonrozsival
simonrozsival marked this pull request as ready for review May 27, 2026 16:20
CopilotAI review requested due to automatic review settings May 27, 2026 16:20

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 (draft/WIP) explores removing the Android NativeAOT link-time dependency on libc++/libc++abi by adjusting MSBuild NativeAOT linking inputs and runtime component packaging, and by refactoring native runtime code paths to avoid pulling in C++ standard library symbols (including introducing minimal C++ allocation/nothrow shims).

Changes:

  • Remove explicit libc++/libc++abi static link inputs and runtime component entries for NativeAOT Android builds.
  • Add NativeAOT-local C++ runtime shims (operator new/delete + std::nothrow) and refactor multiple native components to avoid std::format/other STL usage.
  • Refactor CLR/native logging and GC bridge processing (including callback plumbing for bridge processing and temporary peer map implementation changes).

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.csDrops libc++/libc++abi from the known runtime archive list.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targetsRemoves libc++/libc++abi from NativeAOT link inputs; updates linker/stdlib commentary.
src/native/nativeaot/include/host/host.hhAdds NativeAOT host header shim include.
src/native/nativeaot/include/host/bridge-processing.hhAdapts NativeAOT bridge-processing to callback-based shared implementation.
src/native/nativeaot/host/internal-pinvoke-stubs.ccSimplifies abort path for unimplemented pinvokes to avoid extra std deps.
src/native/nativeaot/host/host.ccRemoves std::format usage from NativeAOT host logging path.
src/native/nativeaot/host/cxx-shims.ccAdds minimal C++ allocation / nothrow shims to avoid libc++ dependency.
src/native/nativeaot/host/CMakeLists.txtAdds cxx-shims.cc to NativeAOT host build.
src/native/nativeaot/host/bridge-processing.ccRemoves std::format usage and wires callbacks for NativeAOT bridge processing.
src/native/common/include/shared/helpers.hhAdds Helpers::abort_applicationf formatted abort helper.
src/native/common/include/shared/cpp-util.hhRemoves C++ ranges/string allocations from diagnostics helpers and logging.
src/native/common/include/runtime-base/timing.hhReplaces std::format timing log formatting with snprintf.
src/native/common/include/runtime-base/timing-internal.hhReplaces std::format timing/internal warnings with snprintf-based logging.
src/native/common/include/runtime-base/strings.hhReplaces some std-container usage and updates formatting/logging to avoid STL pulls.
src/native/common/include/runtime-base/jni-wrappers.hhReplaces new[]/delete[] usage with malloc + placement-new where needed.
src/native/clr/shared/log_functions.ccIntroduces vprintf-style log helpers (log_writev, log_*_fmt).
src/native/clr/shared/helpers.ccAdds implementation of Helpers::abort_applicationf; switches some fatal logging to printf-style.
src/native/clr/runtime-base/util.ccUpdates logging calls to new printf-style logging helpers.
src/native/clr/runtime-base/logger.ccRefactors gref/lref log path storage away from std::string; updates open/log messages.
src/native/clr/runtime-base/android-system-shared.ccReplaces some new[]/delete[] allocations and formatting with malloc/snprintf and fmt logging.
src/native/clr/include/shared/log_types.hhAdds printf-style logging APIs and adjusts macro behavior for XA_HOST_NATIVEAOT builds.
src/native/clr/include/runtime-base/util.hhSwitches to formatted abort/log helpers; adds mmap failure formatted aborts and info logging.
src/native/clr/include/runtime-base/android-system.hhChanges override-dir storage away from std::string for NativeAOT compatibility; API now returns const char*.
src/native/clr/include/host/os-bridge.hhAdds formatted gref logging overloads and refactors internal logging helpers signatures.
src/native/clr/include/host/host-environment.hhUpdates debug/warn logging calls to avoid std::format and skip work when disabled.
src/native/clr/include/host/gc-bridge.hhReplaces std::thread/semaphore with pthread/sem_t for compatibility and reduced STL usage.
src/native/clr/include/host/bridge-processing.hhRemoves now-unneeded CoreCLR no-op overrides after shared callback refactor.
src/native/clr/include/host/bridge-processing-shared.hhIntroduces callback plumbing and switches temporary peer map to robin_map.
src/native/clr/host/os-bridge.ccRefactors stack-trace/gref logging to avoid std::format and centralize formatted logging.
src/native/clr/host/internal-pinvokes-shared.ccUpdates managed->native log forwarding to respect category enablement and use log_write.
src/native/clr/host/host-shared.ccReplaces std::format/string_view literal usage in error logging with printf-style.
src/native/clr/host/gc-bridge.ccMigrates to sem_wait/atomic builtins and updates logging formatting.
src/native/clr/host/fastdev-assemblies.ccUpdates override-dir handling to match AndroidSystem API change to const char*.
src/native/clr/host/bridge-processing.ccImplements callback-based bridge processing shared logic and temporary peer lifecycle handling.
Comments suppressed due to low confidence (1)

src/native/clr/host/os-bridge.cc:203

  • OSBridge::log_it() logs the main line to logcat unconditionally via log_write(...), even when logcat_enabled is false. This makes gref/lref logging emit to logcat even in the “file-only” configuration, and the logcat_enabled parameter only affects stack traces. Consider only calling log_write when logcat_enabled is true (and keep file writes independent), so behavior matches Logger::{gref,lref}_to_logcat().
void OSBridge::log_it (LogCategories category, const char *line, FILE *to, const char *const from, bool logcat_enabled) noexcept
{
log_write (category, LogLevel::Info, line);
// We skip logcat here when logging to file is enabled because _write_stack_trace will output to logcat as well, if enabled
if (to == nullptr) {
if (logcat_enabled) {
_write_stack_trace (nullptr, from, category);
}

Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh Outdated
simonrozsivaland others added 5 commits May 27, 2026 20:58
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the missing standard and logging declarations needed by the native runtime headers when they are compiled directly by the Android runtime ninja builds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid ambiguous gref log overload resolution, insert temporary GC bridge peers into robin_map without mutating through the iterator proxy, and include robin_map headers in the NativeAOT host build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the robin_map-backed temporary peer lookup for the CLR host, but use a simple indexed JNI handle table for the NativeAOT host so app-linked static runtimes do not pull in libc++/c++abi symbols.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid allocating a temporary peer slot for every GC bridge component in the NativeAOT host. The NativeAOT path now stores only the temporary peers it creates, while the CLR host continues to use robin_map.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsivaland others added 7 commits May 28, 2026 07:15
Remove the CoreCLR-only robin_map implementation so bridge processing uses the same sparse temporary peer list for both CoreCLR and NativeAOT. Leave a note about a possible future lookup optimization using the SCC Count field.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Encapsulate temporary peer storage in a dedicated RAII type and encode temporary peer indexes in the SCC count while the bridge owns the GC cross-reference arguments. Share the implementation across CoreCLR and NativeAOT, keeping only the NativeAOT GCUserPeerable callbacks runtime-specific.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert native runtime logging macros to use printf-style helpers across CLR, Mono, and NativeAOT. This removes the NativeAOT-only preformatted logging branch while preserving category gating for debug and info logs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid calling GetMethodID with a null jclass when initializing the temporary peer map. This preserves the intended abort message when the runtime field lookup fails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explain that these shims intentionally cover only the no-libc++ allocation symbols needed by the NativeAOT runtime pack while native code is built without C++ exceptions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop libc++ from NativeAOT linking reduces libUnnamedProject.so by
~625KB. Refresh the BuildReleaseArm64 size-regression reference
descriptions from the latest CI build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 9, 2026 21:54
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in 8dcf139 by merging the latest origin/main into this branch and fixing the remaining conflicts in the CoreCLR pinvoke override files.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 26, 2026 21:43
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in d50cd69 and follow-up 921fef6. I merged the latest origin/main, fixed the remaining conflicts in the GC bridge and apkdesc baselines, and updated the external/xamarin-android-tools/external/Java.Interop submodule pointers from the merged mainline state.

@simonrozsivalsimonrozsival added the drop-libcpp Work to remove the libc++ dependency from Android NativeAOT label Jul 16, 2026
simonrozsival added a commit that referenced this pull request Jul 17, 2026
Use the negative-index TemporaryPeerMap approach from #11311 so CoreCLR and NativeAOT no longer need robin-map for GC bridge processing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa
simonrozsival added a commit that referenced this pull request Jul 20, 2026
## Summary
Replace the shared CoreCLR/NativeAOT GC bridge's temporary-peer `std::unordered_map` with an allocation-backed `TemporaryPeerMap`.
The design follows the approach previously developed in #11311: an empty strongly connected component temporarily carries an encoded peer-array index in `StronglyConnectedComponent.Count`, so the bridge does not need a general-purpose C++ hash table during its scoped cross-reference pass.
Split from #12142. The two PRs are independent and can merge in either order. Part of #12139.
## Background
During GC bridge processing, each strongly connected component (SCC) must behave like one Java object:
- `Count == 1`: the existing Java peer represents the SCC directly;
- `Count > 1`: the bridge adds circular references so all peers remain alive or are collected together;
- `Count == 0`: there is no Java peer, so the bridge creates a temporary `mono.android.GCUserPeer` solely to represent that SCC while cross-SCC references are established.
The previous implementation stored those temporary peers in `std::unordered_map<size_t, jobject>`, keyed by SCC index. The required key set and capacity are already known before processing begins, and lookup is only needed within one short scope, making a hash table unnecessary.
## Implementation
Files:
- `src/native/clr/include/host/bridge-processing-shared.hh`
- `src/native/clr/host/bridge-processing.cc`
### `TemporaryPeerMap` lifetime
1. The constructor scans all SCCs, rejects pre-existing marker values, and counts exactly how many temporary peers are required.
2. If none are required, it performs no allocation.
3. Otherwise it reserves JNI local-reference capacity for all temporary peers plus slack, then allocates one zero-initialized `jobject` array with `calloc`.
4. `add()` creates the temporary `GCUserPeer`, stores it in the next array slot, and writes the encoded slot index into the SCC's `Count` field.
5. Cross-reference target selection detects the encoded marker and retrieves the peer directly from the array.
6. At the end of the scoped cross-reference pass, the destructor deletes every temporary JNI local reference, resets every marked SCC to `Count == 0`, frees the array, and clears its bookkeeping.
7. Normal weak-global-reference processing starts only after the destructor has restored the original SCC shape.
### Index encoding
`Count` is unsigned, so the temporary index is stored as `~index`, which has the same bit pattern as `-(index + 1)`:
- index zero remains representable;
- the high bit acts as the temporary-peer marker;
- encoding rejects indexes that already use the marker bit;
- decoding verifies the marker and bounds-checks the resulting array index;
- the constructor verifies that runtime-provided SCC counts do not already use the reserved marker space.
### JNI initialization and safety
- cache the `mono.android.GCUserPeer` class and constructor during runtime initialization;
- preserve the existing cached `mono.android.IGCUserPeer` method IDs used for reference callbacks;
- reserve local-reference capacity before creating a potentially large temporary-peer set;
- clear and log an `EnsureLocalCapacity` failure consistently with the previous implementation;
- fail fast on allocation failure, peer-construction failure, invalid markers, capacity overruns, missing peers, and out-of-range indexes;
- preserve existing fail-fast handling for Java exceptions raised by `monodroidAddReference` or `monodroidClearReferences`;
- explicitly delete copy and move construction/assignment so the owning array and JNI local references cannot be shallow-copied.
## Behavior preserved
- temporary peers remain alive until every cross-SCC reference has been added;
- temporary local references are released before the Java GC is triggered;
- zero-, one-, and multi-peer SCC handling remains unchanged;
- cross-reference source/destination selection and `refs_added` bookkeeping remain unchanged;
- the runtime receives its SCC array back with all temporary markers removed;
- CoreCLR and NativeAOT continue to use the same shared bridge implementation and host-specific peer callback hooks.
## Scope and non-goals
- This PR changes only temporary-peer storage; it does not change the GC bridge graph algorithm or collection policy.
- It does not change Java peer APIs, reference callback names, or GC trigger behavior.
- It does not introduce robin-map or another replacement hash table.
- The unrelated logging/path/source-location ownership cleanup remains in #12142.
## Expected impact
- remove `std::unordered_map` from shared CoreCLR/NativeAOT temporary-peer processing;
- remove the associated `std::__ndk1::__next_prime` and hash-table allocation/code roots;
- replace per-node hash-table bookkeeping with one exact-size array allocation;
- make temporary JNI reference ownership and SCC marker restoration explicit through RAII;
- preserve GC bridge semantics while reducing C++ standard-library reachability.
## Validation
The implementation is the isolated GC bridge change previously carried in #12142, plus explicit non-copyable/non-movable ownership semantics for `TemporaryPeerMap`.
- `git diff --check` passed;
- the ownership hardening is declaration-only and introduces no runtime code;
- CI is validating head `d4315727e`.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

drop-libcppWork to remove the libc++ dependency from Android NativeAOT

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Drop libc++ from Android NativeAOT linking - #11311

Closed
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp
Closed

Drop libc++ from Android NativeAOT linking#11311
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

This removes the Android NativeAOT app link-time dependency on libc++.

The branch now:

  • removes the explicit NativeAOT libc++_static.a and libc++abi.a link inputs;
  • removes libc++ packaging from the Android native runtime component list;
  • keeps the final NativeAOT app link on the direct ld.lld/NativeLinker path;
  • reduces NativeAOT-reachable host code that pulled in C++ runtime/STL symbols;
  • adds NativeAOT-local C++ allocation/nothrow shims for the remaining runtime-pack allocation references;
  • shares GC bridge processing between CoreCLR and NativeAOT without tsl::robin_map or other libc++-requiring containers;
  • adds printf-style native logging helpers so call sites can avoid repeated snprintf boilerplate and skip formatting when logging is disabled.

Context

This is related to #9926 and the NDK r29 NativeAOT linking work.

The relevant background is that Android NativeAOT should avoid depending on libc++ instead of working around duplicate libunwind symbols or switching to shared libc++. Related references:

GC bridge / Java peer shape history

The GC bridge sharing in this PR exposed one remaining runtime-specific difference: classic Mono/CoreCLR Java peers use mono.android.* and monodroidAddReference() / monodroidClearReferences(), while NativeAOT trimmable proxy objects use net.dot.jni.GCUserPeerable and jiAddManagedReference() / jiClearManagedReferences().

Brief history:

So the current difference appears to be historical layering rather than a fundamental GC bridge requirement: Mono/CoreCLR kept the older Android-compatible monodroid* Java peer API, while NativeAOT trimmable support was built around new generated net.dot.jni.* proxy sources that avoid the legacy Java.Interop native-registration shape. Follow-up issue: #11526 tracks unifying these Java peer reference APIs, likely by moving the trimmable path toward a single net.dot.* shape while NativeAOT is still experimental.

Size impact

Latest measurements on this branch use samples/NativeAOT/NativeAOT.csproj built in Release with _AndroidTypeMapImplementation=trimmable. APK sizes are for the signed APK.

ArtifactParent libc++ baselineNo-libc++ buildDifference
arm64 APK1,575,849 B1,382,336 B-193,513 B (-12.28%)
x64 APK1,639,459 B1,439,677 B-199,782 B (-12.19%)
arm64 libNativeAOT.so3,481,880 B2,943,232 B-538,648 B (-15.47%)
x64 libNativeAOT.so3,404,896 B2,866,728 B-538,168 B (-15.81%)

libNativeAOT.so is the per-ABI native shared library packaged in the APK, for example lib/arm64-v8a/libNativeAOT.so. It is not the whole Android app package; the APK also contains manifest, resources, Java stubs/classes, signatures, and packaging assets.

Validation

Latest local validation:

  • git diff --check
  • make prepare CONFIGURATION=Debug
  • make all CONFIGURATION=Debug progressed through native-mono, native-nativeaot, and native-clr; the remaining failure was a stale test restore asset issue unrelated to native code.
  • ./dotnet-local.sh build src/native/native-nativeaot.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-clr.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-mono.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-arm64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-x64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Debug -t:Install -p:RuntimeIdentifier=android-arm64
  • launched samples/NativeAOT on an arm64 emulator and confirmed Application.OnCreate() and MainActivity.OnCreate() ran without fatal logcat entries.

Previous branch validation:

  • dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj --no-restore — 457 passed.
  • rebuilt src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj;
  • rebuilt NativeAOT runtime archives for android-arm64 and android-x64;
  • built samples/NativeAOT/NativeAOT.csproj with _AndroidTypeMapImplementation=trimmable for android-arm64 and android-x64;
  • verified generated NativeAOT link response files contain no libc++/libc++abi inputs;
  • verified produced APKs contain no libc++ entries;
  • verified llvm-nm -u reports no undefined C++ runtime-looking symbols in the final libNativeAOT.so outputs;
  • smoke-tested the arm64 APK on an arm64 emulator: libNativeAOT.so loaded without libc++, MainApplication and MainActivity native callbacks ran, MainActivity.OnCreate() logged, and the process remained alive.

Notes

The x64 APK builds and packages without libc++, but local x64 runtime validation still needs an x64 emulator host because Android emulator x86_64 system images are not accepted on Apple Silicon/aarch64 hosts.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

Emulator validation update

I tested the signed NativeAOT + trimmable typemap sample APK on an arm64 emulator after rebuilding/overlaying the local validation packs.

What works:

  • The arm64 APK installs and launches on emulator-5554.
  • lib/arm64-v8a/libNativeAOT.so loads successfully with no libc++ packaged in the APK.
  • Startup reaches managed code: MainApplication and MainActivity native callbacks run, MainActivity.OnCreate() logs, and the process remains alive.

Important caveats:

  • This arm64 smoke currently requires trimmable NativeAOT startup fixes plus two validation-only workarounds:
    • pointing ILC TypeMapEntryAssembly at the app typemap (_NativeAOT.TypeMap) instead of the root _Microsoft.Android.TypeMaps, because scanning _Mono.Android.TypeMap currently fails on Android.Text.IInputType being present in the ref assembly but absent from the runtime assembly;
    • skipping UncaughtExceptionMarshaler setup for trimmable NativeAOT to avoid the IUncaughtExceptionHandler/JavaProxyThrowable startup path.
  • I do not consider those two workarounds shippable as-is; they need separate root-cause fixes before this can be more than a feasibility proof.
  • x64 builds and packages without libc++ (-nostdlib++ is in the link response and no libc++ entries are in the APK), but I could not run the x64 APK locally: the Android emulator rejects x86_64 system images on this Apple Silicon host (Avd's CPU Architecture 'x86_64' is not supported by the QEMU2 emulator on aarch64 host). x64 runtime behavior still needs validation on an x64 emulator host.

@simonrozsivalsimonrozsival changed the title [WIP] Explore dropping libc++ from NativeAOT linking[WIP] Drop libc++ from Android NativeAOT linkingMay 12, 2026
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 23, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved by merging origin/main into this branch and fixing all reported conflicts in commit 6e8b48a.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved locally by merging origin/main and fixing the conflict hunks in commit 0199a84. Push from the agent is currently blocked by branch protection (GH013: Cannot update this protected ref), so this commit is ready but not yet on the remote branch.

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Done in 1c6205f. I merged latest origin/main into this branch and resolved all merge conflicts in the affected typemap generator/targets/tests files.

simonrozsivaland others added 5 commits May 27, 2026 17:35
Remove the explicit NativeAOT final-link dependency on libc++/libc++abi and keep the Android NativeAOT link guarded with -nostdlib++.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Centralize gref log message formatting in OSBridge so call sites can pass printf-style arguments directly and formatting is skipped when gref logging is disabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep gref file guards at the call sites, allow literal single-argument gref log calls, and centralize printf-style log formatting helpers for CLR native logging.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/nativeaot-drop-libcpp branch from 1c6205f to 6a4feeaCompareMay 27, 2026 16:12
@simonrozsivalsimonrozsival changed the title [WIP] Drop libc++ from Android NativeAOT linkingDrop libc++ from Android NativeAOT linkingMay 27, 2026
@simonrozsival
simonrozsival marked this pull request as ready for review May 27, 2026 16:20
CopilotAI review requested due to automatic review settings May 27, 2026 16:20

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 (draft/WIP) explores removing the Android NativeAOT link-time dependency on libc++/libc++abi by adjusting MSBuild NativeAOT linking inputs and runtime component packaging, and by refactoring native runtime code paths to avoid pulling in C++ standard library symbols (including introducing minimal C++ allocation/nothrow shims).

Changes:

  • Remove explicit libc++/libc++abi static link inputs and runtime component entries for NativeAOT Android builds.
  • Add NativeAOT-local C++ runtime shims (operator new/delete + std::nothrow) and refactor multiple native components to avoid std::format/other STL usage.
  • Refactor CLR/native logging and GC bridge processing (including callback plumbing for bridge processing and temporary peer map implementation changes).

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.csDrops libc++/libc++abi from the known runtime archive list.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targetsRemoves libc++/libc++abi from NativeAOT link inputs; updates linker/stdlib commentary.
src/native/nativeaot/include/host/host.hhAdds NativeAOT host header shim include.
src/native/nativeaot/include/host/bridge-processing.hhAdapts NativeAOT bridge-processing to callback-based shared implementation.
src/native/nativeaot/host/internal-pinvoke-stubs.ccSimplifies abort path for unimplemented pinvokes to avoid extra std deps.
src/native/nativeaot/host/host.ccRemoves std::format usage from NativeAOT host logging path.
src/native/nativeaot/host/cxx-shims.ccAdds minimal C++ allocation / nothrow shims to avoid libc++ dependency.
src/native/nativeaot/host/CMakeLists.txtAdds cxx-shims.cc to NativeAOT host build.
src/native/nativeaot/host/bridge-processing.ccRemoves std::format usage and wires callbacks for NativeAOT bridge processing.
src/native/common/include/shared/helpers.hhAdds Helpers::abort_applicationf formatted abort helper.
src/native/common/include/shared/cpp-util.hhRemoves C++ ranges/string allocations from diagnostics helpers and logging.
src/native/common/include/runtime-base/timing.hhReplaces std::format timing log formatting with snprintf.
src/native/common/include/runtime-base/timing-internal.hhReplaces std::format timing/internal warnings with snprintf-based logging.
src/native/common/include/runtime-base/strings.hhReplaces some std-container usage and updates formatting/logging to avoid STL pulls.
src/native/common/include/runtime-base/jni-wrappers.hhReplaces new[]/delete[] usage with malloc + placement-new where needed.
src/native/clr/shared/log_functions.ccIntroduces vprintf-style log helpers (log_writev, log_*_fmt).
src/native/clr/shared/helpers.ccAdds implementation of Helpers::abort_applicationf; switches some fatal logging to printf-style.
src/native/clr/runtime-base/util.ccUpdates logging calls to new printf-style logging helpers.
src/native/clr/runtime-base/logger.ccRefactors gref/lref log path storage away from std::string; updates open/log messages.
src/native/clr/runtime-base/android-system-shared.ccReplaces some new[]/delete[] allocations and formatting with malloc/snprintf and fmt logging.
src/native/clr/include/shared/log_types.hhAdds printf-style logging APIs and adjusts macro behavior for XA_HOST_NATIVEAOT builds.
src/native/clr/include/runtime-base/util.hhSwitches to formatted abort/log helpers; adds mmap failure formatted aborts and info logging.
src/native/clr/include/runtime-base/android-system.hhChanges override-dir storage away from std::string for NativeAOT compatibility; API now returns const char*.
src/native/clr/include/host/os-bridge.hhAdds formatted gref logging overloads and refactors internal logging helpers signatures.
src/native/clr/include/host/host-environment.hhUpdates debug/warn logging calls to avoid std::format and skip work when disabled.
src/native/clr/include/host/gc-bridge.hhReplaces std::thread/semaphore with pthread/sem_t for compatibility and reduced STL usage.
src/native/clr/include/host/bridge-processing.hhRemoves now-unneeded CoreCLR no-op overrides after shared callback refactor.
src/native/clr/include/host/bridge-processing-shared.hhIntroduces callback plumbing and switches temporary peer map to robin_map.
src/native/clr/host/os-bridge.ccRefactors stack-trace/gref logging to avoid std::format and centralize formatted logging.
src/native/clr/host/internal-pinvokes-shared.ccUpdates managed->native log forwarding to respect category enablement and use log_write.
src/native/clr/host/host-shared.ccReplaces std::format/string_view literal usage in error logging with printf-style.
src/native/clr/host/gc-bridge.ccMigrates to sem_wait/atomic builtins and updates logging formatting.
src/native/clr/host/fastdev-assemblies.ccUpdates override-dir handling to match AndroidSystem API change to const char*.
src/native/clr/host/bridge-processing.ccImplements callback-based bridge processing shared logic and temporary peer lifecycle handling.
Comments suppressed due to low confidence (1)

src/native/clr/host/os-bridge.cc:203

  • OSBridge::log_it() logs the main line to logcat unconditionally via log_write(...), even when logcat_enabled is false. This makes gref/lref logging emit to logcat even in the “file-only” configuration, and the logcat_enabled parameter only affects stack traces. Consider only calling log_write when logcat_enabled is true (and keep file writes independent), so behavior matches Logger::{gref,lref}_to_logcat().
void OSBridge::log_it (LogCategories category, const char *line, FILE *to, const char *const from, bool logcat_enabled) noexcept
{
log_write (category, LogLevel::Info, line);
// We skip logcat here when logging to file is enabled because _write_stack_trace will output to logcat as well, if enabled
if (to == nullptr) {
if (logcat_enabled) {
_write_stack_trace (nullptr, from, category);
}

Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh Outdated
simonrozsivaland others added 5 commits May 27, 2026 20:58
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the missing standard and logging declarations needed by the native runtime headers when they are compiled directly by the Android runtime ninja builds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid ambiguous gref log overload resolution, insert temporary GC bridge peers into robin_map without mutating through the iterator proxy, and include robin_map headers in the NativeAOT host build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the robin_map-backed temporary peer lookup for the CLR host, but use a simple indexed JNI handle table for the NativeAOT host so app-linked static runtimes do not pull in libc++/c++abi symbols.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid allocating a temporary peer slot for every GC bridge component in the NativeAOT host. The NativeAOT path now stores only the temporary peers it creates, while the CLR host continues to use robin_map.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsivaland others added 7 commits May 28, 2026 07:15
Remove the CoreCLR-only robin_map implementation so bridge processing uses the same sparse temporary peer list for both CoreCLR and NativeAOT. Leave a note about a possible future lookup optimization using the SCC Count field.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Encapsulate temporary peer storage in a dedicated RAII type and encode temporary peer indexes in the SCC count while the bridge owns the GC cross-reference arguments. Share the implementation across CoreCLR and NativeAOT, keeping only the NativeAOT GCUserPeerable callbacks runtime-specific.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert native runtime logging macros to use printf-style helpers across CLR, Mono, and NativeAOT. This removes the NativeAOT-only preformatted logging branch while preserving category gating for debug and info logs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid calling GetMethodID with a null jclass when initializing the temporary peer map. This preserves the intended abort message when the runtime field lookup fails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explain that these shims intentionally cover only the no-libc++ allocation symbols needed by the NativeAOT runtime pack while native code is built without C++ exceptions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop libc++ from NativeAOT linking reduces libUnnamedProject.so by
~625KB. Refresh the BuildReleaseArm64 size-regression reference
descriptions from the latest CI build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 9, 2026 21:54
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in 8dcf139 by merging the latest origin/main into this branch and fixing the remaining conflicts in the CoreCLR pinvoke override files.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 26, 2026 21:43
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in d50cd69 and follow-up 921fef6. I merged the latest origin/main, fixed the remaining conflicts in the GC bridge and apkdesc baselines, and updated the external/xamarin-android-tools/external/Java.Interop submodule pointers from the merged mainline state.

@simonrozsivalsimonrozsival added the drop-libcpp Work to remove the libc++ dependency from Android NativeAOT label Jul 16, 2026
simonrozsival added a commit that referenced this pull request Jul 17, 2026
Use the negative-index TemporaryPeerMap approach from #11311 so CoreCLR and NativeAOT no longer need robin-map for GC bridge processing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa
simonrozsival added a commit that referenced this pull request Jul 20, 2026
## Summary
Replace the shared CoreCLR/NativeAOT GC bridge's temporary-peer `std::unordered_map` with an allocation-backed `TemporaryPeerMap`.
The design follows the approach previously developed in #11311: an empty strongly connected component temporarily carries an encoded peer-array index in `StronglyConnectedComponent.Count`, so the bridge does not need a general-purpose C++ hash table during its scoped cross-reference pass.
Split from #12142. The two PRs are independent and can merge in either order. Part of #12139.
## Background
During GC bridge processing, each strongly connected component (SCC) must behave like one Java object:
- `Count == 1`: the existing Java peer represents the SCC directly;
- `Count > 1`: the bridge adds circular references so all peers remain alive or are collected together;
- `Count == 0`: there is no Java peer, so the bridge creates a temporary `mono.android.GCUserPeer` solely to represent that SCC while cross-SCC references are established.
The previous implementation stored those temporary peers in `std::unordered_map<size_t, jobject>`, keyed by SCC index. The required key set and capacity are already known before processing begins, and lookup is only needed within one short scope, making a hash table unnecessary.
## Implementation
Files:
- `src/native/clr/include/host/bridge-processing-shared.hh`
- `src/native/clr/host/bridge-processing.cc`
### `TemporaryPeerMap` lifetime
1. The constructor scans all SCCs, rejects pre-existing marker values, and counts exactly how many temporary peers are required.
2. If none are required, it performs no allocation.
3. Otherwise it reserves JNI local-reference capacity for all temporary peers plus slack, then allocates one zero-initialized `jobject` array with `calloc`.
4. `add()` creates the temporary `GCUserPeer`, stores it in the next array slot, and writes the encoded slot index into the SCC's `Count` field.
5. Cross-reference target selection detects the encoded marker and retrieves the peer directly from the array.
6. At the end of the scoped cross-reference pass, the destructor deletes every temporary JNI local reference, resets every marked SCC to `Count == 0`, frees the array, and clears its bookkeeping.
7. Normal weak-global-reference processing starts only after the destructor has restored the original SCC shape.
### Index encoding
`Count` is unsigned, so the temporary index is stored as `~index`, which has the same bit pattern as `-(index + 1)`:
- index zero remains representable;
- the high bit acts as the temporary-peer marker;
- encoding rejects indexes that already use the marker bit;
- decoding verifies the marker and bounds-checks the resulting array index;
- the constructor verifies that runtime-provided SCC counts do not already use the reserved marker space.
### JNI initialization and safety
- cache the `mono.android.GCUserPeer` class and constructor during runtime initialization;
- preserve the existing cached `mono.android.IGCUserPeer` method IDs used for reference callbacks;
- reserve local-reference capacity before creating a potentially large temporary-peer set;
- clear and log an `EnsureLocalCapacity` failure consistently with the previous implementation;
- fail fast on allocation failure, peer-construction failure, invalid markers, capacity overruns, missing peers, and out-of-range indexes;
- preserve existing fail-fast handling for Java exceptions raised by `monodroidAddReference` or `monodroidClearReferences`;
- explicitly delete copy and move construction/assignment so the owning array and JNI local references cannot be shallow-copied.
## Behavior preserved
- temporary peers remain alive until every cross-SCC reference has been added;
- temporary local references are released before the Java GC is triggered;
- zero-, one-, and multi-peer SCC handling remains unchanged;
- cross-reference source/destination selection and `refs_added` bookkeeping remain unchanged;
- the runtime receives its SCC array back with all temporary markers removed;
- CoreCLR and NativeAOT continue to use the same shared bridge implementation and host-specific peer callback hooks.
## Scope and non-goals
- This PR changes only temporary-peer storage; it does not change the GC bridge graph algorithm or collection policy.
- It does not change Java peer APIs, reference callback names, or GC trigger behavior.
- It does not introduce robin-map or another replacement hash table.
- The unrelated logging/path/source-location ownership cleanup remains in #12142.
## Expected impact
- remove `std::unordered_map` from shared CoreCLR/NativeAOT temporary-peer processing;
- remove the associated `std::__ndk1::__next_prime` and hash-table allocation/code roots;
- replace per-node hash-table bookkeeping with one exact-size array allocation;
- make temporary JNI reference ownership and SCC marker restoration explicit through RAII;
- preserve GC bridge semantics while reducing C++ standard-library reachability.
## Validation
The implementation is the isolated GC bridge change previously carried in #12142, plus explicit non-copyable/non-movable ownership semantics for `TemporaryPeerMap`.
- `git diff --check` passed;
- the ownership hardening is declaration-only and introduces no runtime code;
- CI is validating head `d4315727e`.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

drop-libcppWork to remove the libc++ dependency from Android NativeAOT

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Drop libc++ from Android NativeAOT linking - #11311

Closed
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp
Closed

Drop libc++ from Android NativeAOT linking#11311
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

This removes the Android NativeAOT app link-time dependency on libc++.

The branch now:

  • removes the explicit NativeAOT libc++_static.a and libc++abi.a link inputs;
  • removes libc++ packaging from the Android native runtime component list;
  • keeps the final NativeAOT app link on the direct ld.lld/NativeLinker path;
  • reduces NativeAOT-reachable host code that pulled in C++ runtime/STL symbols;
  • adds NativeAOT-local C++ allocation/nothrow shims for the remaining runtime-pack allocation references;
  • shares GC bridge processing between CoreCLR and NativeAOT without tsl::robin_map or other libc++-requiring containers;
  • adds printf-style native logging helpers so call sites can avoid repeated snprintf boilerplate and skip formatting when logging is disabled.

Context

This is related to #9926 and the NDK r29 NativeAOT linking work.

The relevant background is that Android NativeAOT should avoid depending on libc++ instead of working around duplicate libunwind symbols or switching to shared libc++. Related references:

GC bridge / Java peer shape history

The GC bridge sharing in this PR exposed one remaining runtime-specific difference: classic Mono/CoreCLR Java peers use mono.android.* and monodroidAddReference() / monodroidClearReferences(), while NativeAOT trimmable proxy objects use net.dot.jni.GCUserPeerable and jiAddManagedReference() / jiClearManagedReferences().

Brief history:

So the current difference appears to be historical layering rather than a fundamental GC bridge requirement: Mono/CoreCLR kept the older Android-compatible monodroid* Java peer API, while NativeAOT trimmable support was built around new generated net.dot.jni.* proxy sources that avoid the legacy Java.Interop native-registration shape. Follow-up issue: #11526 tracks unifying these Java peer reference APIs, likely by moving the trimmable path toward a single net.dot.* shape while NativeAOT is still experimental.

Size impact

Latest measurements on this branch use samples/NativeAOT/NativeAOT.csproj built in Release with _AndroidTypeMapImplementation=trimmable. APK sizes are for the signed APK.

ArtifactParent libc++ baselineNo-libc++ buildDifference
arm64 APK1,575,849 B1,382,336 B-193,513 B (-12.28%)
x64 APK1,639,459 B1,439,677 B-199,782 B (-12.19%)
arm64 libNativeAOT.so3,481,880 B2,943,232 B-538,648 B (-15.47%)
x64 libNativeAOT.so3,404,896 B2,866,728 B-538,168 B (-15.81%)

libNativeAOT.so is the per-ABI native shared library packaged in the APK, for example lib/arm64-v8a/libNativeAOT.so. It is not the whole Android app package; the APK also contains manifest, resources, Java stubs/classes, signatures, and packaging assets.

Validation

Latest local validation:

  • git diff --check
  • make prepare CONFIGURATION=Debug
  • make all CONFIGURATION=Debug progressed through native-mono, native-nativeaot, and native-clr; the remaining failure was a stale test restore asset issue unrelated to native code.
  • ./dotnet-local.sh build src/native/native-nativeaot.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-clr.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-mono.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-arm64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-x64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Debug -t:Install -p:RuntimeIdentifier=android-arm64
  • launched samples/NativeAOT on an arm64 emulator and confirmed Application.OnCreate() and MainActivity.OnCreate() ran without fatal logcat entries.

Previous branch validation:

  • dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj --no-restore — 457 passed.
  • rebuilt src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj;
  • rebuilt NativeAOT runtime archives for android-arm64 and android-x64;
  • built samples/NativeAOT/NativeAOT.csproj with _AndroidTypeMapImplementation=trimmable for android-arm64 and android-x64;
  • verified generated NativeAOT link response files contain no libc++/libc++abi inputs;
  • verified produced APKs contain no libc++ entries;
  • verified llvm-nm -u reports no undefined C++ runtime-looking symbols in the final libNativeAOT.so outputs;
  • smoke-tested the arm64 APK on an arm64 emulator: libNativeAOT.so loaded without libc++, MainApplication and MainActivity native callbacks ran, MainActivity.OnCreate() logged, and the process remained alive.

Notes

The x64 APK builds and packages without libc++, but local x64 runtime validation still needs an x64 emulator host because Android emulator x86_64 system images are not accepted on Apple Silicon/aarch64 hosts.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

Emulator validation update

I tested the signed NativeAOT + trimmable typemap sample APK on an arm64 emulator after rebuilding/overlaying the local validation packs.

What works:

  • The arm64 APK installs and launches on emulator-5554.
  • lib/arm64-v8a/libNativeAOT.so loads successfully with no libc++ packaged in the APK.
  • Startup reaches managed code: MainApplication and MainActivity native callbacks run, MainActivity.OnCreate() logs, and the process remains alive.

Important caveats:

  • This arm64 smoke currently requires trimmable NativeAOT startup fixes plus two validation-only workarounds:
    • pointing ILC TypeMapEntryAssembly at the app typemap (_NativeAOT.TypeMap) instead of the root _Microsoft.Android.TypeMaps, because scanning _Mono.Android.TypeMap currently fails on Android.Text.IInputType being present in the ref assembly but absent from the runtime assembly;
    • skipping UncaughtExceptionMarshaler setup for trimmable NativeAOT to avoid the IUncaughtExceptionHandler/JavaProxyThrowable startup path.
  • I do not consider those two workarounds shippable as-is; they need separate root-cause fixes before this can be more than a feasibility proof.
  • x64 builds and packages without libc++ (-nostdlib++ is in the link response and no libc++ entries are in the APK), but I could not run the x64 APK locally: the Android emulator rejects x86_64 system images on this Apple Silicon host (Avd's CPU Architecture 'x86_64' is not supported by the QEMU2 emulator on aarch64 host). x64 runtime behavior still needs validation on an x64 emulator host.

@simonrozsivalsimonrozsival changed the title [WIP] Explore dropping libc++ from NativeAOT linking[WIP] Drop libc++ from Android NativeAOT linkingMay 12, 2026
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 23, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved by merging origin/main into this branch and fixing all reported conflicts in commit 6e8b48a.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved locally by merging origin/main and fixing the conflict hunks in commit 0199a84. Push from the agent is currently blocked by branch protection (GH013: Cannot update this protected ref), so this commit is ready but not yet on the remote branch.

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Done in 1c6205f. I merged latest origin/main into this branch and resolved all merge conflicts in the affected typemap generator/targets/tests files.

simonrozsivaland others added 5 commits May 27, 2026 17:35
Remove the explicit NativeAOT final-link dependency on libc++/libc++abi and keep the Android NativeAOT link guarded with -nostdlib++.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Centralize gref log message formatting in OSBridge so call sites can pass printf-style arguments directly and formatting is skipped when gref logging is disabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep gref file guards at the call sites, allow literal single-argument gref log calls, and centralize printf-style log formatting helpers for CLR native logging.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/nativeaot-drop-libcpp branch from 1c6205f to 6a4feeaCompareMay 27, 2026 16:12
@simonrozsivalsimonrozsival changed the title [WIP] Drop libc++ from Android NativeAOT linkingDrop libc++ from Android NativeAOT linkingMay 27, 2026
@simonrozsival
simonrozsival marked this pull request as ready for review May 27, 2026 16:20
CopilotAI review requested due to automatic review settings May 27, 2026 16:20

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 (draft/WIP) explores removing the Android NativeAOT link-time dependency on libc++/libc++abi by adjusting MSBuild NativeAOT linking inputs and runtime component packaging, and by refactoring native runtime code paths to avoid pulling in C++ standard library symbols (including introducing minimal C++ allocation/nothrow shims).

Changes:

  • Remove explicit libc++/libc++abi static link inputs and runtime component entries for NativeAOT Android builds.
  • Add NativeAOT-local C++ runtime shims (operator new/delete + std::nothrow) and refactor multiple native components to avoid std::format/other STL usage.
  • Refactor CLR/native logging and GC bridge processing (including callback plumbing for bridge processing and temporary peer map implementation changes).

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.csDrops libc++/libc++abi from the known runtime archive list.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targetsRemoves libc++/libc++abi from NativeAOT link inputs; updates linker/stdlib commentary.
src/native/nativeaot/include/host/host.hhAdds NativeAOT host header shim include.
src/native/nativeaot/include/host/bridge-processing.hhAdapts NativeAOT bridge-processing to callback-based shared implementation.
src/native/nativeaot/host/internal-pinvoke-stubs.ccSimplifies abort path for unimplemented pinvokes to avoid extra std deps.
src/native/nativeaot/host/host.ccRemoves std::format usage from NativeAOT host logging path.
src/native/nativeaot/host/cxx-shims.ccAdds minimal C++ allocation / nothrow shims to avoid libc++ dependency.
src/native/nativeaot/host/CMakeLists.txtAdds cxx-shims.cc to NativeAOT host build.
src/native/nativeaot/host/bridge-processing.ccRemoves std::format usage and wires callbacks for NativeAOT bridge processing.
src/native/common/include/shared/helpers.hhAdds Helpers::abort_applicationf formatted abort helper.
src/native/common/include/shared/cpp-util.hhRemoves C++ ranges/string allocations from diagnostics helpers and logging.
src/native/common/include/runtime-base/timing.hhReplaces std::format timing log formatting with snprintf.
src/native/common/include/runtime-base/timing-internal.hhReplaces std::format timing/internal warnings with snprintf-based logging.
src/native/common/include/runtime-base/strings.hhReplaces some std-container usage and updates formatting/logging to avoid STL pulls.
src/native/common/include/runtime-base/jni-wrappers.hhReplaces new[]/delete[] usage with malloc + placement-new where needed.
src/native/clr/shared/log_functions.ccIntroduces vprintf-style log helpers (log_writev, log_*_fmt).
src/native/clr/shared/helpers.ccAdds implementation of Helpers::abort_applicationf; switches some fatal logging to printf-style.
src/native/clr/runtime-base/util.ccUpdates logging calls to new printf-style logging helpers.
src/native/clr/runtime-base/logger.ccRefactors gref/lref log path storage away from std::string; updates open/log messages.
src/native/clr/runtime-base/android-system-shared.ccReplaces some new[]/delete[] allocations and formatting with malloc/snprintf and fmt logging.
src/native/clr/include/shared/log_types.hhAdds printf-style logging APIs and adjusts macro behavior for XA_HOST_NATIVEAOT builds.
src/native/clr/include/runtime-base/util.hhSwitches to formatted abort/log helpers; adds mmap failure formatted aborts and info logging.
src/native/clr/include/runtime-base/android-system.hhChanges override-dir storage away from std::string for NativeAOT compatibility; API now returns const char*.
src/native/clr/include/host/os-bridge.hhAdds formatted gref logging overloads and refactors internal logging helpers signatures.
src/native/clr/include/host/host-environment.hhUpdates debug/warn logging calls to avoid std::format and skip work when disabled.
src/native/clr/include/host/gc-bridge.hhReplaces std::thread/semaphore with pthread/sem_t for compatibility and reduced STL usage.
src/native/clr/include/host/bridge-processing.hhRemoves now-unneeded CoreCLR no-op overrides after shared callback refactor.
src/native/clr/include/host/bridge-processing-shared.hhIntroduces callback plumbing and switches temporary peer map to robin_map.
src/native/clr/host/os-bridge.ccRefactors stack-trace/gref logging to avoid std::format and centralize formatted logging.
src/native/clr/host/internal-pinvokes-shared.ccUpdates managed->native log forwarding to respect category enablement and use log_write.
src/native/clr/host/host-shared.ccReplaces std::format/string_view literal usage in error logging with printf-style.
src/native/clr/host/gc-bridge.ccMigrates to sem_wait/atomic builtins and updates logging formatting.
src/native/clr/host/fastdev-assemblies.ccUpdates override-dir handling to match AndroidSystem API change to const char*.
src/native/clr/host/bridge-processing.ccImplements callback-based bridge processing shared logic and temporary peer lifecycle handling.
Comments suppressed due to low confidence (1)

src/native/clr/host/os-bridge.cc:203

  • OSBridge::log_it() logs the main line to logcat unconditionally via log_write(...), even when logcat_enabled is false. This makes gref/lref logging emit to logcat even in the “file-only” configuration, and the logcat_enabled parameter only affects stack traces. Consider only calling log_write when logcat_enabled is true (and keep file writes independent), so behavior matches Logger::{gref,lref}_to_logcat().
void OSBridge::log_it (LogCategories category, const char *line, FILE *to, const char *const from, bool logcat_enabled) noexcept
{
log_write (category, LogLevel::Info, line);
// We skip logcat here when logging to file is enabled because _write_stack_trace will output to logcat as well, if enabled
if (to == nullptr) {
if (logcat_enabled) {
_write_stack_trace (nullptr, from, category);
}

Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh Outdated
simonrozsivaland others added 5 commits May 27, 2026 20:58
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the missing standard and logging declarations needed by the native runtime headers when they are compiled directly by the Android runtime ninja builds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid ambiguous gref log overload resolution, insert temporary GC bridge peers into robin_map without mutating through the iterator proxy, and include robin_map headers in the NativeAOT host build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the robin_map-backed temporary peer lookup for the CLR host, but use a simple indexed JNI handle table for the NativeAOT host so app-linked static runtimes do not pull in libc++/c++abi symbols.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid allocating a temporary peer slot for every GC bridge component in the NativeAOT host. The NativeAOT path now stores only the temporary peers it creates, while the CLR host continues to use robin_map.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsivaland others added 7 commits May 28, 2026 07:15
Remove the CoreCLR-only robin_map implementation so bridge processing uses the same sparse temporary peer list for both CoreCLR and NativeAOT. Leave a note about a possible future lookup optimization using the SCC Count field.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Encapsulate temporary peer storage in a dedicated RAII type and encode temporary peer indexes in the SCC count while the bridge owns the GC cross-reference arguments. Share the implementation across CoreCLR and NativeAOT, keeping only the NativeAOT GCUserPeerable callbacks runtime-specific.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert native runtime logging macros to use printf-style helpers across CLR, Mono, and NativeAOT. This removes the NativeAOT-only preformatted logging branch while preserving category gating for debug and info logs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid calling GetMethodID with a null jclass when initializing the temporary peer map. This preserves the intended abort message when the runtime field lookup fails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explain that these shims intentionally cover only the no-libc++ allocation symbols needed by the NativeAOT runtime pack while native code is built without C++ exceptions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop libc++ from NativeAOT linking reduces libUnnamedProject.so by
~625KB. Refresh the BuildReleaseArm64 size-regression reference
descriptions from the latest CI build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 9, 2026 21:54
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in 8dcf139 by merging the latest origin/main into this branch and fixing the remaining conflicts in the CoreCLR pinvoke override files.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 26, 2026 21:43
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in d50cd69 and follow-up 921fef6. I merged the latest origin/main, fixed the remaining conflicts in the GC bridge and apkdesc baselines, and updated the external/xamarin-android-tools/external/Java.Interop submodule pointers from the merged mainline state.

@simonrozsivalsimonrozsival added the drop-libcpp Work to remove the libc++ dependency from Android NativeAOT label Jul 16, 2026
simonrozsival added a commit that referenced this pull request Jul 17, 2026
Use the negative-index TemporaryPeerMap approach from #11311 so CoreCLR and NativeAOT no longer need robin-map for GC bridge processing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa
simonrozsival added a commit that referenced this pull request Jul 20, 2026
## Summary
Replace the shared CoreCLR/NativeAOT GC bridge's temporary-peer `std::unordered_map` with an allocation-backed `TemporaryPeerMap`.
The design follows the approach previously developed in #11311: an empty strongly connected component temporarily carries an encoded peer-array index in `StronglyConnectedComponent.Count`, so the bridge does not need a general-purpose C++ hash table during its scoped cross-reference pass.
Split from #12142. The two PRs are independent and can merge in either order. Part of #12139.
## Background
During GC bridge processing, each strongly connected component (SCC) must behave like one Java object:
- `Count == 1`: the existing Java peer represents the SCC directly;
- `Count > 1`: the bridge adds circular references so all peers remain alive or are collected together;
- `Count == 0`: there is no Java peer, so the bridge creates a temporary `mono.android.GCUserPeer` solely to represent that SCC while cross-SCC references are established.
The previous implementation stored those temporary peers in `std::unordered_map<size_t, jobject>`, keyed by SCC index. The required key set and capacity are already known before processing begins, and lookup is only needed within one short scope, making a hash table unnecessary.
## Implementation
Files:
- `src/native/clr/include/host/bridge-processing-shared.hh`
- `src/native/clr/host/bridge-processing.cc`
### `TemporaryPeerMap` lifetime
1. The constructor scans all SCCs, rejects pre-existing marker values, and counts exactly how many temporary peers are required.
2. If none are required, it performs no allocation.
3. Otherwise it reserves JNI local-reference capacity for all temporary peers plus slack, then allocates one zero-initialized `jobject` array with `calloc`.
4. `add()` creates the temporary `GCUserPeer`, stores it in the next array slot, and writes the encoded slot index into the SCC's `Count` field.
5. Cross-reference target selection detects the encoded marker and retrieves the peer directly from the array.
6. At the end of the scoped cross-reference pass, the destructor deletes every temporary JNI local reference, resets every marked SCC to `Count == 0`, frees the array, and clears its bookkeeping.
7. Normal weak-global-reference processing starts only after the destructor has restored the original SCC shape.
### Index encoding
`Count` is unsigned, so the temporary index is stored as `~index`, which has the same bit pattern as `-(index + 1)`:
- index zero remains representable;
- the high bit acts as the temporary-peer marker;
- encoding rejects indexes that already use the marker bit;
- decoding verifies the marker and bounds-checks the resulting array index;
- the constructor verifies that runtime-provided SCC counts do not already use the reserved marker space.
### JNI initialization and safety
- cache the `mono.android.GCUserPeer` class and constructor during runtime initialization;
- preserve the existing cached `mono.android.IGCUserPeer` method IDs used for reference callbacks;
- reserve local-reference capacity before creating a potentially large temporary-peer set;
- clear and log an `EnsureLocalCapacity` failure consistently with the previous implementation;
- fail fast on allocation failure, peer-construction failure, invalid markers, capacity overruns, missing peers, and out-of-range indexes;
- preserve existing fail-fast handling for Java exceptions raised by `monodroidAddReference` or `monodroidClearReferences`;
- explicitly delete copy and move construction/assignment so the owning array and JNI local references cannot be shallow-copied.
## Behavior preserved
- temporary peers remain alive until every cross-SCC reference has been added;
- temporary local references are released before the Java GC is triggered;
- zero-, one-, and multi-peer SCC handling remains unchanged;
- cross-reference source/destination selection and `refs_added` bookkeeping remain unchanged;
- the runtime receives its SCC array back with all temporary markers removed;
- CoreCLR and NativeAOT continue to use the same shared bridge implementation and host-specific peer callback hooks.
## Scope and non-goals
- This PR changes only temporary-peer storage; it does not change the GC bridge graph algorithm or collection policy.
- It does not change Java peer APIs, reference callback names, or GC trigger behavior.
- It does not introduce robin-map or another replacement hash table.
- The unrelated logging/path/source-location ownership cleanup remains in #12142.
## Expected impact
- remove `std::unordered_map` from shared CoreCLR/NativeAOT temporary-peer processing;
- remove the associated `std::__ndk1::__next_prime` and hash-table allocation/code roots;
- replace per-node hash-table bookkeeping with one exact-size array allocation;
- make temporary JNI reference ownership and SCC marker restoration explicit through RAII;
- preserve GC bridge semantics while reducing C++ standard-library reachability.
## Validation
The implementation is the isolated GC bridge change previously carried in #12142, plus explicit non-copyable/non-movable ownership semantics for `TemporaryPeerMap`.
- `git diff --check` passed;
- the ownership hardening is declaration-only and introduces no runtime code;
- CI is validating head `d4315727e`.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

drop-libcppWork to remove the libc++ dependency from Android NativeAOT

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Drop libc++ from Android NativeAOT linking - #11311

Closed
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp
Closed

Drop libc++ from Android NativeAOT linking#11311
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

This removes the Android NativeAOT app link-time dependency on libc++.

The branch now:

  • removes the explicit NativeAOT libc++_static.a and libc++abi.a link inputs;
  • removes libc++ packaging from the Android native runtime component list;
  • keeps the final NativeAOT app link on the direct ld.lld/NativeLinker path;
  • reduces NativeAOT-reachable host code that pulled in C++ runtime/STL symbols;
  • adds NativeAOT-local C++ allocation/nothrow shims for the remaining runtime-pack allocation references;
  • shares GC bridge processing between CoreCLR and NativeAOT without tsl::robin_map or other libc++-requiring containers;
  • adds printf-style native logging helpers so call sites can avoid repeated snprintf boilerplate and skip formatting when logging is disabled.

Context

This is related to #9926 and the NDK r29 NativeAOT linking work.

The relevant background is that Android NativeAOT should avoid depending on libc++ instead of working around duplicate libunwind symbols or switching to shared libc++. Related references:

GC bridge / Java peer shape history

The GC bridge sharing in this PR exposed one remaining runtime-specific difference: classic Mono/CoreCLR Java peers use mono.android.* and monodroidAddReference() / monodroidClearReferences(), while NativeAOT trimmable proxy objects use net.dot.jni.GCUserPeerable and jiAddManagedReference() / jiClearManagedReferences().

Brief history:

So the current difference appears to be historical layering rather than a fundamental GC bridge requirement: Mono/CoreCLR kept the older Android-compatible monodroid* Java peer API, while NativeAOT trimmable support was built around new generated net.dot.jni.* proxy sources that avoid the legacy Java.Interop native-registration shape. Follow-up issue: #11526 tracks unifying these Java peer reference APIs, likely by moving the trimmable path toward a single net.dot.* shape while NativeAOT is still experimental.

Size impact

Latest measurements on this branch use samples/NativeAOT/NativeAOT.csproj built in Release with _AndroidTypeMapImplementation=trimmable. APK sizes are for the signed APK.

ArtifactParent libc++ baselineNo-libc++ buildDifference
arm64 APK1,575,849 B1,382,336 B-193,513 B (-12.28%)
x64 APK1,639,459 B1,439,677 B-199,782 B (-12.19%)
arm64 libNativeAOT.so3,481,880 B2,943,232 B-538,648 B (-15.47%)
x64 libNativeAOT.so3,404,896 B2,866,728 B-538,168 B (-15.81%)

libNativeAOT.so is the per-ABI native shared library packaged in the APK, for example lib/arm64-v8a/libNativeAOT.so. It is not the whole Android app package; the APK also contains manifest, resources, Java stubs/classes, signatures, and packaging assets.

Validation

Latest local validation:

  • git diff --check
  • make prepare CONFIGURATION=Debug
  • make all CONFIGURATION=Debug progressed through native-mono, native-nativeaot, and native-clr; the remaining failure was a stale test restore asset issue unrelated to native code.
  • ./dotnet-local.sh build src/native/native-nativeaot.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-clr.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-mono.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-arm64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-x64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Debug -t:Install -p:RuntimeIdentifier=android-arm64
  • launched samples/NativeAOT on an arm64 emulator and confirmed Application.OnCreate() and MainActivity.OnCreate() ran without fatal logcat entries.

Previous branch validation:

  • dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj --no-restore — 457 passed.
  • rebuilt src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj;
  • rebuilt NativeAOT runtime archives for android-arm64 and android-x64;
  • built samples/NativeAOT/NativeAOT.csproj with _AndroidTypeMapImplementation=trimmable for android-arm64 and android-x64;
  • verified generated NativeAOT link response files contain no libc++/libc++abi inputs;
  • verified produced APKs contain no libc++ entries;
  • verified llvm-nm -u reports no undefined C++ runtime-looking symbols in the final libNativeAOT.so outputs;
  • smoke-tested the arm64 APK on an arm64 emulator: libNativeAOT.so loaded without libc++, MainApplication and MainActivity native callbacks ran, MainActivity.OnCreate() logged, and the process remained alive.

Notes

The x64 APK builds and packages without libc++, but local x64 runtime validation still needs an x64 emulator host because Android emulator x86_64 system images are not accepted on Apple Silicon/aarch64 hosts.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

Emulator validation update

I tested the signed NativeAOT + trimmable typemap sample APK on an arm64 emulator after rebuilding/overlaying the local validation packs.

What works:

  • The arm64 APK installs and launches on emulator-5554.
  • lib/arm64-v8a/libNativeAOT.so loads successfully with no libc++ packaged in the APK.
  • Startup reaches managed code: MainApplication and MainActivity native callbacks run, MainActivity.OnCreate() logs, and the process remains alive.

Important caveats:

  • This arm64 smoke currently requires trimmable NativeAOT startup fixes plus two validation-only workarounds:
    • pointing ILC TypeMapEntryAssembly at the app typemap (_NativeAOT.TypeMap) instead of the root _Microsoft.Android.TypeMaps, because scanning _Mono.Android.TypeMap currently fails on Android.Text.IInputType being present in the ref assembly but absent from the runtime assembly;
    • skipping UncaughtExceptionMarshaler setup for trimmable NativeAOT to avoid the IUncaughtExceptionHandler/JavaProxyThrowable startup path.
  • I do not consider those two workarounds shippable as-is; they need separate root-cause fixes before this can be more than a feasibility proof.
  • x64 builds and packages without libc++ (-nostdlib++ is in the link response and no libc++ entries are in the APK), but I could not run the x64 APK locally: the Android emulator rejects x86_64 system images on this Apple Silicon host (Avd's CPU Architecture 'x86_64' is not supported by the QEMU2 emulator on aarch64 host). x64 runtime behavior still needs validation on an x64 emulator host.

@simonrozsivalsimonrozsival changed the title [WIP] Explore dropping libc++ from NativeAOT linking[WIP] Drop libc++ from Android NativeAOT linkingMay 12, 2026
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 23, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved by merging origin/main into this branch and fixing all reported conflicts in commit 6e8b48a.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved locally by merging origin/main and fixing the conflict hunks in commit 0199a84. Push from the agent is currently blocked by branch protection (GH013: Cannot update this protected ref), so this commit is ready but not yet on the remote branch.

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Done in 1c6205f. I merged latest origin/main into this branch and resolved all merge conflicts in the affected typemap generator/targets/tests files.

simonrozsivaland others added 5 commits May 27, 2026 17:35
Remove the explicit NativeAOT final-link dependency on libc++/libc++abi and keep the Android NativeAOT link guarded with -nostdlib++.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Centralize gref log message formatting in OSBridge so call sites can pass printf-style arguments directly and formatting is skipped when gref logging is disabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep gref file guards at the call sites, allow literal single-argument gref log calls, and centralize printf-style log formatting helpers for CLR native logging.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/nativeaot-drop-libcpp branch from 1c6205f to 6a4feeaCompareMay 27, 2026 16:12
@simonrozsivalsimonrozsival changed the title [WIP] Drop libc++ from Android NativeAOT linkingDrop libc++ from Android NativeAOT linkingMay 27, 2026
@simonrozsival
simonrozsival marked this pull request as ready for review May 27, 2026 16:20
CopilotAI review requested due to automatic review settings May 27, 2026 16:20

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 (draft/WIP) explores removing the Android NativeAOT link-time dependency on libc++/libc++abi by adjusting MSBuild NativeAOT linking inputs and runtime component packaging, and by refactoring native runtime code paths to avoid pulling in C++ standard library symbols (including introducing minimal C++ allocation/nothrow shims).

Changes:

  • Remove explicit libc++/libc++abi static link inputs and runtime component entries for NativeAOT Android builds.
  • Add NativeAOT-local C++ runtime shims (operator new/delete + std::nothrow) and refactor multiple native components to avoid std::format/other STL usage.
  • Refactor CLR/native logging and GC bridge processing (including callback plumbing for bridge processing and temporary peer map implementation changes).

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.csDrops libc++/libc++abi from the known runtime archive list.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targetsRemoves libc++/libc++abi from NativeAOT link inputs; updates linker/stdlib commentary.
src/native/nativeaot/include/host/host.hhAdds NativeAOT host header shim include.
src/native/nativeaot/include/host/bridge-processing.hhAdapts NativeAOT bridge-processing to callback-based shared implementation.
src/native/nativeaot/host/internal-pinvoke-stubs.ccSimplifies abort path for unimplemented pinvokes to avoid extra std deps.
src/native/nativeaot/host/host.ccRemoves std::format usage from NativeAOT host logging path.
src/native/nativeaot/host/cxx-shims.ccAdds minimal C++ allocation / nothrow shims to avoid libc++ dependency.
src/native/nativeaot/host/CMakeLists.txtAdds cxx-shims.cc to NativeAOT host build.
src/native/nativeaot/host/bridge-processing.ccRemoves std::format usage and wires callbacks for NativeAOT bridge processing.
src/native/common/include/shared/helpers.hhAdds Helpers::abort_applicationf formatted abort helper.
src/native/common/include/shared/cpp-util.hhRemoves C++ ranges/string allocations from diagnostics helpers and logging.
src/native/common/include/runtime-base/timing.hhReplaces std::format timing log formatting with snprintf.
src/native/common/include/runtime-base/timing-internal.hhReplaces std::format timing/internal warnings with snprintf-based logging.
src/native/common/include/runtime-base/strings.hhReplaces some std-container usage and updates formatting/logging to avoid STL pulls.
src/native/common/include/runtime-base/jni-wrappers.hhReplaces new[]/delete[] usage with malloc + placement-new where needed.
src/native/clr/shared/log_functions.ccIntroduces vprintf-style log helpers (log_writev, log_*_fmt).
src/native/clr/shared/helpers.ccAdds implementation of Helpers::abort_applicationf; switches some fatal logging to printf-style.
src/native/clr/runtime-base/util.ccUpdates logging calls to new printf-style logging helpers.
src/native/clr/runtime-base/logger.ccRefactors gref/lref log path storage away from std::string; updates open/log messages.
src/native/clr/runtime-base/android-system-shared.ccReplaces some new[]/delete[] allocations and formatting with malloc/snprintf and fmt logging.
src/native/clr/include/shared/log_types.hhAdds printf-style logging APIs and adjusts macro behavior for XA_HOST_NATIVEAOT builds.
src/native/clr/include/runtime-base/util.hhSwitches to formatted abort/log helpers; adds mmap failure formatted aborts and info logging.
src/native/clr/include/runtime-base/android-system.hhChanges override-dir storage away from std::string for NativeAOT compatibility; API now returns const char*.
src/native/clr/include/host/os-bridge.hhAdds formatted gref logging overloads and refactors internal logging helpers signatures.
src/native/clr/include/host/host-environment.hhUpdates debug/warn logging calls to avoid std::format and skip work when disabled.
src/native/clr/include/host/gc-bridge.hhReplaces std::thread/semaphore with pthread/sem_t for compatibility and reduced STL usage.
src/native/clr/include/host/bridge-processing.hhRemoves now-unneeded CoreCLR no-op overrides after shared callback refactor.
src/native/clr/include/host/bridge-processing-shared.hhIntroduces callback plumbing and switches temporary peer map to robin_map.
src/native/clr/host/os-bridge.ccRefactors stack-trace/gref logging to avoid std::format and centralize formatted logging.
src/native/clr/host/internal-pinvokes-shared.ccUpdates managed->native log forwarding to respect category enablement and use log_write.
src/native/clr/host/host-shared.ccReplaces std::format/string_view literal usage in error logging with printf-style.
src/native/clr/host/gc-bridge.ccMigrates to sem_wait/atomic builtins and updates logging formatting.
src/native/clr/host/fastdev-assemblies.ccUpdates override-dir handling to match AndroidSystem API change to const char*.
src/native/clr/host/bridge-processing.ccImplements callback-based bridge processing shared logic and temporary peer lifecycle handling.
Comments suppressed due to low confidence (1)

src/native/clr/host/os-bridge.cc:203

  • OSBridge::log_it() logs the main line to logcat unconditionally via log_write(...), even when logcat_enabled is false. This makes gref/lref logging emit to logcat even in the “file-only” configuration, and the logcat_enabled parameter only affects stack traces. Consider only calling log_write when logcat_enabled is true (and keep file writes independent), so behavior matches Logger::{gref,lref}_to_logcat().
void OSBridge::log_it (LogCategories category, const char *line, FILE *to, const char *const from, bool logcat_enabled) noexcept
{
log_write (category, LogLevel::Info, line);
// We skip logcat here when logging to file is enabled because _write_stack_trace will output to logcat as well, if enabled
if (to == nullptr) {
if (logcat_enabled) {
_write_stack_trace (nullptr, from, category);
}

Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh Outdated
simonrozsivaland others added 5 commits May 27, 2026 20:58
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the missing standard and logging declarations needed by the native runtime headers when they are compiled directly by the Android runtime ninja builds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid ambiguous gref log overload resolution, insert temporary GC bridge peers into robin_map without mutating through the iterator proxy, and include robin_map headers in the NativeAOT host build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the robin_map-backed temporary peer lookup for the CLR host, but use a simple indexed JNI handle table for the NativeAOT host so app-linked static runtimes do not pull in libc++/c++abi symbols.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid allocating a temporary peer slot for every GC bridge component in the NativeAOT host. The NativeAOT path now stores only the temporary peers it creates, while the CLR host continues to use robin_map.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsivaland others added 7 commits May 28, 2026 07:15
Remove the CoreCLR-only robin_map implementation so bridge processing uses the same sparse temporary peer list for both CoreCLR and NativeAOT. Leave a note about a possible future lookup optimization using the SCC Count field.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Encapsulate temporary peer storage in a dedicated RAII type and encode temporary peer indexes in the SCC count while the bridge owns the GC cross-reference arguments. Share the implementation across CoreCLR and NativeAOT, keeping only the NativeAOT GCUserPeerable callbacks runtime-specific.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert native runtime logging macros to use printf-style helpers across CLR, Mono, and NativeAOT. This removes the NativeAOT-only preformatted logging branch while preserving category gating for debug and info logs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid calling GetMethodID with a null jclass when initializing the temporary peer map. This preserves the intended abort message when the runtime field lookup fails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explain that these shims intentionally cover only the no-libc++ allocation symbols needed by the NativeAOT runtime pack while native code is built without C++ exceptions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop libc++ from NativeAOT linking reduces libUnnamedProject.so by
~625KB. Refresh the BuildReleaseArm64 size-regression reference
descriptions from the latest CI build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 9, 2026 21:54
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in 8dcf139 by merging the latest origin/main into this branch and fixing the remaining conflicts in the CoreCLR pinvoke override files.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 26, 2026 21:43
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in d50cd69 and follow-up 921fef6. I merged the latest origin/main, fixed the remaining conflicts in the GC bridge and apkdesc baselines, and updated the external/xamarin-android-tools/external/Java.Interop submodule pointers from the merged mainline state.

@simonrozsivalsimonrozsival added the drop-libcpp Work to remove the libc++ dependency from Android NativeAOT label Jul 16, 2026
simonrozsival added a commit that referenced this pull request Jul 17, 2026
Use the negative-index TemporaryPeerMap approach from #11311 so CoreCLR and NativeAOT no longer need robin-map for GC bridge processing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa
simonrozsival added a commit that referenced this pull request Jul 20, 2026
## Summary
Replace the shared CoreCLR/NativeAOT GC bridge's temporary-peer `std::unordered_map` with an allocation-backed `TemporaryPeerMap`.
The design follows the approach previously developed in #11311: an empty strongly connected component temporarily carries an encoded peer-array index in `StronglyConnectedComponent.Count`, so the bridge does not need a general-purpose C++ hash table during its scoped cross-reference pass.
Split from #12142. The two PRs are independent and can merge in either order. Part of #12139.
## Background
During GC bridge processing, each strongly connected component (SCC) must behave like one Java object:
- `Count == 1`: the existing Java peer represents the SCC directly;
- `Count > 1`: the bridge adds circular references so all peers remain alive or are collected together;
- `Count == 0`: there is no Java peer, so the bridge creates a temporary `mono.android.GCUserPeer` solely to represent that SCC while cross-SCC references are established.
The previous implementation stored those temporary peers in `std::unordered_map<size_t, jobject>`, keyed by SCC index. The required key set and capacity are already known before processing begins, and lookup is only needed within one short scope, making a hash table unnecessary.
## Implementation
Files:
- `src/native/clr/include/host/bridge-processing-shared.hh`
- `src/native/clr/host/bridge-processing.cc`
### `TemporaryPeerMap` lifetime
1. The constructor scans all SCCs, rejects pre-existing marker values, and counts exactly how many temporary peers are required.
2. If none are required, it performs no allocation.
3. Otherwise it reserves JNI local-reference capacity for all temporary peers plus slack, then allocates one zero-initialized `jobject` array with `calloc`.
4. `add()` creates the temporary `GCUserPeer`, stores it in the next array slot, and writes the encoded slot index into the SCC's `Count` field.
5. Cross-reference target selection detects the encoded marker and retrieves the peer directly from the array.
6. At the end of the scoped cross-reference pass, the destructor deletes every temporary JNI local reference, resets every marked SCC to `Count == 0`, frees the array, and clears its bookkeeping.
7. Normal weak-global-reference processing starts only after the destructor has restored the original SCC shape.
### Index encoding
`Count` is unsigned, so the temporary index is stored as `~index`, which has the same bit pattern as `-(index + 1)`:
- index zero remains representable;
- the high bit acts as the temporary-peer marker;
- encoding rejects indexes that already use the marker bit;
- decoding verifies the marker and bounds-checks the resulting array index;
- the constructor verifies that runtime-provided SCC counts do not already use the reserved marker space.
### JNI initialization and safety
- cache the `mono.android.GCUserPeer` class and constructor during runtime initialization;
- preserve the existing cached `mono.android.IGCUserPeer` method IDs used for reference callbacks;
- reserve local-reference capacity before creating a potentially large temporary-peer set;
- clear and log an `EnsureLocalCapacity` failure consistently with the previous implementation;
- fail fast on allocation failure, peer-construction failure, invalid markers, capacity overruns, missing peers, and out-of-range indexes;
- preserve existing fail-fast handling for Java exceptions raised by `monodroidAddReference` or `monodroidClearReferences`;
- explicitly delete copy and move construction/assignment so the owning array and JNI local references cannot be shallow-copied.
## Behavior preserved
- temporary peers remain alive until every cross-SCC reference has been added;
- temporary local references are released before the Java GC is triggered;
- zero-, one-, and multi-peer SCC handling remains unchanged;
- cross-reference source/destination selection and `refs_added` bookkeeping remain unchanged;
- the runtime receives its SCC array back with all temporary markers removed;
- CoreCLR and NativeAOT continue to use the same shared bridge implementation and host-specific peer callback hooks.
## Scope and non-goals
- This PR changes only temporary-peer storage; it does not change the GC bridge graph algorithm or collection policy.
- It does not change Java peer APIs, reference callback names, or GC trigger behavior.
- It does not introduce robin-map or another replacement hash table.
- The unrelated logging/path/source-location ownership cleanup remains in #12142.
## Expected impact
- remove `std::unordered_map` from shared CoreCLR/NativeAOT temporary-peer processing;
- remove the associated `std::__ndk1::__next_prime` and hash-table allocation/code roots;
- replace per-node hash-table bookkeeping with one exact-size array allocation;
- make temporary JNI reference ownership and SCC marker restoration explicit through RAII;
- preserve GC bridge semantics while reducing C++ standard-library reachability.
## Validation
The implementation is the isolated GC bridge change previously carried in #12142, plus explicit non-copyable/non-movable ownership semantics for `TemporaryPeerMap`.
- `git diff --check` passed;
- the ownership hardening is declaration-only and introduces no runtime code;
- CI is validating head `d4315727e`.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

drop-libcppWork to remove the libc++ dependency from Android NativeAOT

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Drop libc++ from Android NativeAOT linking - #11311

Closed
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp
Closed

Drop libc++ from Android NativeAOT linking#11311
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

This removes the Android NativeAOT app link-time dependency on libc++.

The branch now:

  • removes the explicit NativeAOT libc++_static.a and libc++abi.a link inputs;
  • removes libc++ packaging from the Android native runtime component list;
  • keeps the final NativeAOT app link on the direct ld.lld/NativeLinker path;
  • reduces NativeAOT-reachable host code that pulled in C++ runtime/STL symbols;
  • adds NativeAOT-local C++ allocation/nothrow shims for the remaining runtime-pack allocation references;
  • shares GC bridge processing between CoreCLR and NativeAOT without tsl::robin_map or other libc++-requiring containers;
  • adds printf-style native logging helpers so call sites can avoid repeated snprintf boilerplate and skip formatting when logging is disabled.

Context

This is related to #9926 and the NDK r29 NativeAOT linking work.

The relevant background is that Android NativeAOT should avoid depending on libc++ instead of working around duplicate libunwind symbols or switching to shared libc++. Related references:

GC bridge / Java peer shape history

The GC bridge sharing in this PR exposed one remaining runtime-specific difference: classic Mono/CoreCLR Java peers use mono.android.* and monodroidAddReference() / monodroidClearReferences(), while NativeAOT trimmable proxy objects use net.dot.jni.GCUserPeerable and jiAddManagedReference() / jiClearManagedReferences().

Brief history:

So the current difference appears to be historical layering rather than a fundamental GC bridge requirement: Mono/CoreCLR kept the older Android-compatible monodroid* Java peer API, while NativeAOT trimmable support was built around new generated net.dot.jni.* proxy sources that avoid the legacy Java.Interop native-registration shape. Follow-up issue: #11526 tracks unifying these Java peer reference APIs, likely by moving the trimmable path toward a single net.dot.* shape while NativeAOT is still experimental.

Size impact

Latest measurements on this branch use samples/NativeAOT/NativeAOT.csproj built in Release with _AndroidTypeMapImplementation=trimmable. APK sizes are for the signed APK.

ArtifactParent libc++ baselineNo-libc++ buildDifference
arm64 APK1,575,849 B1,382,336 B-193,513 B (-12.28%)
x64 APK1,639,459 B1,439,677 B-199,782 B (-12.19%)
arm64 libNativeAOT.so3,481,880 B2,943,232 B-538,648 B (-15.47%)
x64 libNativeAOT.so3,404,896 B2,866,728 B-538,168 B (-15.81%)

libNativeAOT.so is the per-ABI native shared library packaged in the APK, for example lib/arm64-v8a/libNativeAOT.so. It is not the whole Android app package; the APK also contains manifest, resources, Java stubs/classes, signatures, and packaging assets.

Validation

Latest local validation:

  • git diff --check
  • make prepare CONFIGURATION=Debug
  • make all CONFIGURATION=Debug progressed through native-mono, native-nativeaot, and native-clr; the remaining failure was a stale test restore asset issue unrelated to native code.
  • ./dotnet-local.sh build src/native/native-nativeaot.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-clr.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-mono.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-arm64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-x64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Debug -t:Install -p:RuntimeIdentifier=android-arm64
  • launched samples/NativeAOT on an arm64 emulator and confirmed Application.OnCreate() and MainActivity.OnCreate() ran without fatal logcat entries.

Previous branch validation:

  • dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj --no-restore — 457 passed.
  • rebuilt src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj;
  • rebuilt NativeAOT runtime archives for android-arm64 and android-x64;
  • built samples/NativeAOT/NativeAOT.csproj with _AndroidTypeMapImplementation=trimmable for android-arm64 and android-x64;
  • verified generated NativeAOT link response files contain no libc++/libc++abi inputs;
  • verified produced APKs contain no libc++ entries;
  • verified llvm-nm -u reports no undefined C++ runtime-looking symbols in the final libNativeAOT.so outputs;
  • smoke-tested the arm64 APK on an arm64 emulator: libNativeAOT.so loaded without libc++, MainApplication and MainActivity native callbacks ran, MainActivity.OnCreate() logged, and the process remained alive.

Notes

The x64 APK builds and packages without libc++, but local x64 runtime validation still needs an x64 emulator host because Android emulator x86_64 system images are not accepted on Apple Silicon/aarch64 hosts.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

Emulator validation update

I tested the signed NativeAOT + trimmable typemap sample APK on an arm64 emulator after rebuilding/overlaying the local validation packs.

What works:

  • The arm64 APK installs and launches on emulator-5554.
  • lib/arm64-v8a/libNativeAOT.so loads successfully with no libc++ packaged in the APK.
  • Startup reaches managed code: MainApplication and MainActivity native callbacks run, MainActivity.OnCreate() logs, and the process remains alive.

Important caveats:

  • This arm64 smoke currently requires trimmable NativeAOT startup fixes plus two validation-only workarounds:
    • pointing ILC TypeMapEntryAssembly at the app typemap (_NativeAOT.TypeMap) instead of the root _Microsoft.Android.TypeMaps, because scanning _Mono.Android.TypeMap currently fails on Android.Text.IInputType being present in the ref assembly but absent from the runtime assembly;
    • skipping UncaughtExceptionMarshaler setup for trimmable NativeAOT to avoid the IUncaughtExceptionHandler/JavaProxyThrowable startup path.
  • I do not consider those two workarounds shippable as-is; they need separate root-cause fixes before this can be more than a feasibility proof.
  • x64 builds and packages without libc++ (-nostdlib++ is in the link response and no libc++ entries are in the APK), but I could not run the x64 APK locally: the Android emulator rejects x86_64 system images on this Apple Silicon host (Avd's CPU Architecture 'x86_64' is not supported by the QEMU2 emulator on aarch64 host). x64 runtime behavior still needs validation on an x64 emulator host.

@simonrozsivalsimonrozsival changed the title [WIP] Explore dropping libc++ from NativeAOT linking[WIP] Drop libc++ from Android NativeAOT linkingMay 12, 2026
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 23, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved by merging origin/main into this branch and fixing all reported conflicts in commit 6e8b48a.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved locally by merging origin/main and fixing the conflict hunks in commit 0199a84. Push from the agent is currently blocked by branch protection (GH013: Cannot update this protected ref), so this commit is ready but not yet on the remote branch.

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Done in 1c6205f. I merged latest origin/main into this branch and resolved all merge conflicts in the affected typemap generator/targets/tests files.

simonrozsivaland others added 5 commits May 27, 2026 17:35
Remove the explicit NativeAOT final-link dependency on libc++/libc++abi and keep the Android NativeAOT link guarded with -nostdlib++.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Centralize gref log message formatting in OSBridge so call sites can pass printf-style arguments directly and formatting is skipped when gref logging is disabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep gref file guards at the call sites, allow literal single-argument gref log calls, and centralize printf-style log formatting helpers for CLR native logging.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/nativeaot-drop-libcpp branch from 1c6205f to 6a4feeaCompareMay 27, 2026 16:12
@simonrozsivalsimonrozsival changed the title [WIP] Drop libc++ from Android NativeAOT linkingDrop libc++ from Android NativeAOT linkingMay 27, 2026
@simonrozsival
simonrozsival marked this pull request as ready for review May 27, 2026 16:20
CopilotAI review requested due to automatic review settings May 27, 2026 16:20

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 (draft/WIP) explores removing the Android NativeAOT link-time dependency on libc++/libc++abi by adjusting MSBuild NativeAOT linking inputs and runtime component packaging, and by refactoring native runtime code paths to avoid pulling in C++ standard library symbols (including introducing minimal C++ allocation/nothrow shims).

Changes:

  • Remove explicit libc++/libc++abi static link inputs and runtime component entries for NativeAOT Android builds.
  • Add NativeAOT-local C++ runtime shims (operator new/delete + std::nothrow) and refactor multiple native components to avoid std::format/other STL usage.
  • Refactor CLR/native logging and GC bridge processing (including callback plumbing for bridge processing and temporary peer map implementation changes).

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.csDrops libc++/libc++abi from the known runtime archive list.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targetsRemoves libc++/libc++abi from NativeAOT link inputs; updates linker/stdlib commentary.
src/native/nativeaot/include/host/host.hhAdds NativeAOT host header shim include.
src/native/nativeaot/include/host/bridge-processing.hhAdapts NativeAOT bridge-processing to callback-based shared implementation.
src/native/nativeaot/host/internal-pinvoke-stubs.ccSimplifies abort path for unimplemented pinvokes to avoid extra std deps.
src/native/nativeaot/host/host.ccRemoves std::format usage from NativeAOT host logging path.
src/native/nativeaot/host/cxx-shims.ccAdds minimal C++ allocation / nothrow shims to avoid libc++ dependency.
src/native/nativeaot/host/CMakeLists.txtAdds cxx-shims.cc to NativeAOT host build.
src/native/nativeaot/host/bridge-processing.ccRemoves std::format usage and wires callbacks for NativeAOT bridge processing.
src/native/common/include/shared/helpers.hhAdds Helpers::abort_applicationf formatted abort helper.
src/native/common/include/shared/cpp-util.hhRemoves C++ ranges/string allocations from diagnostics helpers and logging.
src/native/common/include/runtime-base/timing.hhReplaces std::format timing log formatting with snprintf.
src/native/common/include/runtime-base/timing-internal.hhReplaces std::format timing/internal warnings with snprintf-based logging.
src/native/common/include/runtime-base/strings.hhReplaces some std-container usage and updates formatting/logging to avoid STL pulls.
src/native/common/include/runtime-base/jni-wrappers.hhReplaces new[]/delete[] usage with malloc + placement-new where needed.
src/native/clr/shared/log_functions.ccIntroduces vprintf-style log helpers (log_writev, log_*_fmt).
src/native/clr/shared/helpers.ccAdds implementation of Helpers::abort_applicationf; switches some fatal logging to printf-style.
src/native/clr/runtime-base/util.ccUpdates logging calls to new printf-style logging helpers.
src/native/clr/runtime-base/logger.ccRefactors gref/lref log path storage away from std::string; updates open/log messages.
src/native/clr/runtime-base/android-system-shared.ccReplaces some new[]/delete[] allocations and formatting with malloc/snprintf and fmt logging.
src/native/clr/include/shared/log_types.hhAdds printf-style logging APIs and adjusts macro behavior for XA_HOST_NATIVEAOT builds.
src/native/clr/include/runtime-base/util.hhSwitches to formatted abort/log helpers; adds mmap failure formatted aborts and info logging.
src/native/clr/include/runtime-base/android-system.hhChanges override-dir storage away from std::string for NativeAOT compatibility; API now returns const char*.
src/native/clr/include/host/os-bridge.hhAdds formatted gref logging overloads and refactors internal logging helpers signatures.
src/native/clr/include/host/host-environment.hhUpdates debug/warn logging calls to avoid std::format and skip work when disabled.
src/native/clr/include/host/gc-bridge.hhReplaces std::thread/semaphore with pthread/sem_t for compatibility and reduced STL usage.
src/native/clr/include/host/bridge-processing.hhRemoves now-unneeded CoreCLR no-op overrides after shared callback refactor.
src/native/clr/include/host/bridge-processing-shared.hhIntroduces callback plumbing and switches temporary peer map to robin_map.
src/native/clr/host/os-bridge.ccRefactors stack-trace/gref logging to avoid std::format and centralize formatted logging.
src/native/clr/host/internal-pinvokes-shared.ccUpdates managed->native log forwarding to respect category enablement and use log_write.
src/native/clr/host/host-shared.ccReplaces std::format/string_view literal usage in error logging with printf-style.
src/native/clr/host/gc-bridge.ccMigrates to sem_wait/atomic builtins and updates logging formatting.
src/native/clr/host/fastdev-assemblies.ccUpdates override-dir handling to match AndroidSystem API change to const char*.
src/native/clr/host/bridge-processing.ccImplements callback-based bridge processing shared logic and temporary peer lifecycle handling.
Comments suppressed due to low confidence (1)

src/native/clr/host/os-bridge.cc:203

  • OSBridge::log_it() logs the main line to logcat unconditionally via log_write(...), even when logcat_enabled is false. This makes gref/lref logging emit to logcat even in the “file-only” configuration, and the logcat_enabled parameter only affects stack traces. Consider only calling log_write when logcat_enabled is true (and keep file writes independent), so behavior matches Logger::{gref,lref}_to_logcat().
void OSBridge::log_it (LogCategories category, const char *line, FILE *to, const char *const from, bool logcat_enabled) noexcept
{
log_write (category, LogLevel::Info, line);
// We skip logcat here when logging to file is enabled because _write_stack_trace will output to logcat as well, if enabled
if (to == nullptr) {
if (logcat_enabled) {
_write_stack_trace (nullptr, from, category);
}

Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh Outdated
simonrozsivaland others added 5 commits May 27, 2026 20:58
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the missing standard and logging declarations needed by the native runtime headers when they are compiled directly by the Android runtime ninja builds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid ambiguous gref log overload resolution, insert temporary GC bridge peers into robin_map without mutating through the iterator proxy, and include robin_map headers in the NativeAOT host build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the robin_map-backed temporary peer lookup for the CLR host, but use a simple indexed JNI handle table for the NativeAOT host so app-linked static runtimes do not pull in libc++/c++abi symbols.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid allocating a temporary peer slot for every GC bridge component in the NativeAOT host. The NativeAOT path now stores only the temporary peers it creates, while the CLR host continues to use robin_map.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsivaland others added 7 commits May 28, 2026 07:15
Remove the CoreCLR-only robin_map implementation so bridge processing uses the same sparse temporary peer list for both CoreCLR and NativeAOT. Leave a note about a possible future lookup optimization using the SCC Count field.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Encapsulate temporary peer storage in a dedicated RAII type and encode temporary peer indexes in the SCC count while the bridge owns the GC cross-reference arguments. Share the implementation across CoreCLR and NativeAOT, keeping only the NativeAOT GCUserPeerable callbacks runtime-specific.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert native runtime logging macros to use printf-style helpers across CLR, Mono, and NativeAOT. This removes the NativeAOT-only preformatted logging branch while preserving category gating for debug and info logs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid calling GetMethodID with a null jclass when initializing the temporary peer map. This preserves the intended abort message when the runtime field lookup fails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explain that these shims intentionally cover only the no-libc++ allocation symbols needed by the NativeAOT runtime pack while native code is built without C++ exceptions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop libc++ from NativeAOT linking reduces libUnnamedProject.so by
~625KB. Refresh the BuildReleaseArm64 size-regression reference
descriptions from the latest CI build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 9, 2026 21:54
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in 8dcf139 by merging the latest origin/main into this branch and fixing the remaining conflicts in the CoreCLR pinvoke override files.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 26, 2026 21:43
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in d50cd69 and follow-up 921fef6. I merged the latest origin/main, fixed the remaining conflicts in the GC bridge and apkdesc baselines, and updated the external/xamarin-android-tools/external/Java.Interop submodule pointers from the merged mainline state.

@simonrozsivalsimonrozsival added the drop-libcpp Work to remove the libc++ dependency from Android NativeAOT label Jul 16, 2026
simonrozsival added a commit that referenced this pull request Jul 17, 2026
Use the negative-index TemporaryPeerMap approach from #11311 so CoreCLR and NativeAOT no longer need robin-map for GC bridge processing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa
simonrozsival added a commit that referenced this pull request Jul 20, 2026
## Summary
Replace the shared CoreCLR/NativeAOT GC bridge's temporary-peer `std::unordered_map` with an allocation-backed `TemporaryPeerMap`.
The design follows the approach previously developed in #11311: an empty strongly connected component temporarily carries an encoded peer-array index in `StronglyConnectedComponent.Count`, so the bridge does not need a general-purpose C++ hash table during its scoped cross-reference pass.
Split from #12142. The two PRs are independent and can merge in either order. Part of #12139.
## Background
During GC bridge processing, each strongly connected component (SCC) must behave like one Java object:
- `Count == 1`: the existing Java peer represents the SCC directly;
- `Count > 1`: the bridge adds circular references so all peers remain alive or are collected together;
- `Count == 0`: there is no Java peer, so the bridge creates a temporary `mono.android.GCUserPeer` solely to represent that SCC while cross-SCC references are established.
The previous implementation stored those temporary peers in `std::unordered_map<size_t, jobject>`, keyed by SCC index. The required key set and capacity are already known before processing begins, and lookup is only needed within one short scope, making a hash table unnecessary.
## Implementation
Files:
- `src/native/clr/include/host/bridge-processing-shared.hh`
- `src/native/clr/host/bridge-processing.cc`
### `TemporaryPeerMap` lifetime
1. The constructor scans all SCCs, rejects pre-existing marker values, and counts exactly how many temporary peers are required.
2. If none are required, it performs no allocation.
3. Otherwise it reserves JNI local-reference capacity for all temporary peers plus slack, then allocates one zero-initialized `jobject` array with `calloc`.
4. `add()` creates the temporary `GCUserPeer`, stores it in the next array slot, and writes the encoded slot index into the SCC's `Count` field.
5. Cross-reference target selection detects the encoded marker and retrieves the peer directly from the array.
6. At the end of the scoped cross-reference pass, the destructor deletes every temporary JNI local reference, resets every marked SCC to `Count == 0`, frees the array, and clears its bookkeeping.
7. Normal weak-global-reference processing starts only after the destructor has restored the original SCC shape.
### Index encoding
`Count` is unsigned, so the temporary index is stored as `~index`, which has the same bit pattern as `-(index + 1)`:
- index zero remains representable;
- the high bit acts as the temporary-peer marker;
- encoding rejects indexes that already use the marker bit;
- decoding verifies the marker and bounds-checks the resulting array index;
- the constructor verifies that runtime-provided SCC counts do not already use the reserved marker space.
### JNI initialization and safety
- cache the `mono.android.GCUserPeer` class and constructor during runtime initialization;
- preserve the existing cached `mono.android.IGCUserPeer` method IDs used for reference callbacks;
- reserve local-reference capacity before creating a potentially large temporary-peer set;
- clear and log an `EnsureLocalCapacity` failure consistently with the previous implementation;
- fail fast on allocation failure, peer-construction failure, invalid markers, capacity overruns, missing peers, and out-of-range indexes;
- preserve existing fail-fast handling for Java exceptions raised by `monodroidAddReference` or `monodroidClearReferences`;
- explicitly delete copy and move construction/assignment so the owning array and JNI local references cannot be shallow-copied.
## Behavior preserved
- temporary peers remain alive until every cross-SCC reference has been added;
- temporary local references are released before the Java GC is triggered;
- zero-, one-, and multi-peer SCC handling remains unchanged;
- cross-reference source/destination selection and `refs_added` bookkeeping remain unchanged;
- the runtime receives its SCC array back with all temporary markers removed;
- CoreCLR and NativeAOT continue to use the same shared bridge implementation and host-specific peer callback hooks.
## Scope and non-goals
- This PR changes only temporary-peer storage; it does not change the GC bridge graph algorithm or collection policy.
- It does not change Java peer APIs, reference callback names, or GC trigger behavior.
- It does not introduce robin-map or another replacement hash table.
- The unrelated logging/path/source-location ownership cleanup remains in #12142.
## Expected impact
- remove `std::unordered_map` from shared CoreCLR/NativeAOT temporary-peer processing;
- remove the associated `std::__ndk1::__next_prime` and hash-table allocation/code roots;
- replace per-node hash-table bookkeeping with one exact-size array allocation;
- make temporary JNI reference ownership and SCC marker restoration explicit through RAII;
- preserve GC bridge semantics while reducing C++ standard-library reachability.
## Validation
The implementation is the isolated GC bridge change previously carried in #12142, plus explicit non-copyable/non-movable ownership semantics for `TemporaryPeerMap`.
- `git diff --check` passed;
- the ownership hardening is declaration-only and introduces no runtime code;
- CI is validating head `d4315727e`.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

drop-libcppWork to remove the libc++ dependency from Android NativeAOT

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Drop libc++ from Android NativeAOT linking - #11311

Closed
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp
Closed

Drop libc++ from Android NativeAOT linking#11311
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

This removes the Android NativeAOT app link-time dependency on libc++.

The branch now:

  • removes the explicit NativeAOT libc++_static.a and libc++abi.a link inputs;
  • removes libc++ packaging from the Android native runtime component list;
  • keeps the final NativeAOT app link on the direct ld.lld/NativeLinker path;
  • reduces NativeAOT-reachable host code that pulled in C++ runtime/STL symbols;
  • adds NativeAOT-local C++ allocation/nothrow shims for the remaining runtime-pack allocation references;
  • shares GC bridge processing between CoreCLR and NativeAOT without tsl::robin_map or other libc++-requiring containers;
  • adds printf-style native logging helpers so call sites can avoid repeated snprintf boilerplate and skip formatting when logging is disabled.

Context

This is related to #9926 and the NDK r29 NativeAOT linking work.

The relevant background is that Android NativeAOT should avoid depending on libc++ instead of working around duplicate libunwind symbols or switching to shared libc++. Related references:

GC bridge / Java peer shape history

The GC bridge sharing in this PR exposed one remaining runtime-specific difference: classic Mono/CoreCLR Java peers use mono.android.* and monodroidAddReference() / monodroidClearReferences(), while NativeAOT trimmable proxy objects use net.dot.jni.GCUserPeerable and jiAddManagedReference() / jiClearManagedReferences().

Brief history:

So the current difference appears to be historical layering rather than a fundamental GC bridge requirement: Mono/CoreCLR kept the older Android-compatible monodroid* Java peer API, while NativeAOT trimmable support was built around new generated net.dot.jni.* proxy sources that avoid the legacy Java.Interop native-registration shape. Follow-up issue: #11526 tracks unifying these Java peer reference APIs, likely by moving the trimmable path toward a single net.dot.* shape while NativeAOT is still experimental.

Size impact

Latest measurements on this branch use samples/NativeAOT/NativeAOT.csproj built in Release with _AndroidTypeMapImplementation=trimmable. APK sizes are for the signed APK.

ArtifactParent libc++ baselineNo-libc++ buildDifference
arm64 APK1,575,849 B1,382,336 B-193,513 B (-12.28%)
x64 APK1,639,459 B1,439,677 B-199,782 B (-12.19%)
arm64 libNativeAOT.so3,481,880 B2,943,232 B-538,648 B (-15.47%)
x64 libNativeAOT.so3,404,896 B2,866,728 B-538,168 B (-15.81%)

libNativeAOT.so is the per-ABI native shared library packaged in the APK, for example lib/arm64-v8a/libNativeAOT.so. It is not the whole Android app package; the APK also contains manifest, resources, Java stubs/classes, signatures, and packaging assets.

Validation

Latest local validation:

  • git diff --check
  • make prepare CONFIGURATION=Debug
  • make all CONFIGURATION=Debug progressed through native-mono, native-nativeaot, and native-clr; the remaining failure was a stale test restore asset issue unrelated to native code.
  • ./dotnet-local.sh build src/native/native-nativeaot.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-clr.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-mono.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-arm64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-x64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Debug -t:Install -p:RuntimeIdentifier=android-arm64
  • launched samples/NativeAOT on an arm64 emulator and confirmed Application.OnCreate() and MainActivity.OnCreate() ran without fatal logcat entries.

Previous branch validation:

  • dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj --no-restore — 457 passed.
  • rebuilt src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj;
  • rebuilt NativeAOT runtime archives for android-arm64 and android-x64;
  • built samples/NativeAOT/NativeAOT.csproj with _AndroidTypeMapImplementation=trimmable for android-arm64 and android-x64;
  • verified generated NativeAOT link response files contain no libc++/libc++abi inputs;
  • verified produced APKs contain no libc++ entries;
  • verified llvm-nm -u reports no undefined C++ runtime-looking symbols in the final libNativeAOT.so outputs;
  • smoke-tested the arm64 APK on an arm64 emulator: libNativeAOT.so loaded without libc++, MainApplication and MainActivity native callbacks ran, MainActivity.OnCreate() logged, and the process remained alive.

Notes

The x64 APK builds and packages without libc++, but local x64 runtime validation still needs an x64 emulator host because Android emulator x86_64 system images are not accepted on Apple Silicon/aarch64 hosts.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

Emulator validation update

I tested the signed NativeAOT + trimmable typemap sample APK on an arm64 emulator after rebuilding/overlaying the local validation packs.

What works:

  • The arm64 APK installs and launches on emulator-5554.
  • lib/arm64-v8a/libNativeAOT.so loads successfully with no libc++ packaged in the APK.
  • Startup reaches managed code: MainApplication and MainActivity native callbacks run, MainActivity.OnCreate() logs, and the process remains alive.

Important caveats:

  • This arm64 smoke currently requires trimmable NativeAOT startup fixes plus two validation-only workarounds:
    • pointing ILC TypeMapEntryAssembly at the app typemap (_NativeAOT.TypeMap) instead of the root _Microsoft.Android.TypeMaps, because scanning _Mono.Android.TypeMap currently fails on Android.Text.IInputType being present in the ref assembly but absent from the runtime assembly;
    • skipping UncaughtExceptionMarshaler setup for trimmable NativeAOT to avoid the IUncaughtExceptionHandler/JavaProxyThrowable startup path.
  • I do not consider those two workarounds shippable as-is; they need separate root-cause fixes before this can be more than a feasibility proof.
  • x64 builds and packages without libc++ (-nostdlib++ is in the link response and no libc++ entries are in the APK), but I could not run the x64 APK locally: the Android emulator rejects x86_64 system images on this Apple Silicon host (Avd's CPU Architecture 'x86_64' is not supported by the QEMU2 emulator on aarch64 host). x64 runtime behavior still needs validation on an x64 emulator host.

@simonrozsivalsimonrozsival changed the title [WIP] Explore dropping libc++ from NativeAOT linking[WIP] Drop libc++ from Android NativeAOT linkingMay 12, 2026
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 23, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved by merging origin/main into this branch and fixing all reported conflicts in commit 6e8b48a.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved locally by merging origin/main and fixing the conflict hunks in commit 0199a84. Push from the agent is currently blocked by branch protection (GH013: Cannot update this protected ref), so this commit is ready but not yet on the remote branch.

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Done in 1c6205f. I merged latest origin/main into this branch and resolved all merge conflicts in the affected typemap generator/targets/tests files.

simonrozsivaland others added 5 commits May 27, 2026 17:35
Remove the explicit NativeAOT final-link dependency on libc++/libc++abi and keep the Android NativeAOT link guarded with -nostdlib++.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Centralize gref log message formatting in OSBridge so call sites can pass printf-style arguments directly and formatting is skipped when gref logging is disabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep gref file guards at the call sites, allow literal single-argument gref log calls, and centralize printf-style log formatting helpers for CLR native logging.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/nativeaot-drop-libcpp branch from 1c6205f to 6a4feeaCompareMay 27, 2026 16:12
@simonrozsivalsimonrozsival changed the title [WIP] Drop libc++ from Android NativeAOT linkingDrop libc++ from Android NativeAOT linkingMay 27, 2026
@simonrozsival
simonrozsival marked this pull request as ready for review May 27, 2026 16:20
CopilotAI review requested due to automatic review settings May 27, 2026 16:20

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 (draft/WIP) explores removing the Android NativeAOT link-time dependency on libc++/libc++abi by adjusting MSBuild NativeAOT linking inputs and runtime component packaging, and by refactoring native runtime code paths to avoid pulling in C++ standard library symbols (including introducing minimal C++ allocation/nothrow shims).

Changes:

  • Remove explicit libc++/libc++abi static link inputs and runtime component entries for NativeAOT Android builds.
  • Add NativeAOT-local C++ runtime shims (operator new/delete + std::nothrow) and refactor multiple native components to avoid std::format/other STL usage.
  • Refactor CLR/native logging and GC bridge processing (including callback plumbing for bridge processing and temporary peer map implementation changes).

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.csDrops libc++/libc++abi from the known runtime archive list.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targetsRemoves libc++/libc++abi from NativeAOT link inputs; updates linker/stdlib commentary.
src/native/nativeaot/include/host/host.hhAdds NativeAOT host header shim include.
src/native/nativeaot/include/host/bridge-processing.hhAdapts NativeAOT bridge-processing to callback-based shared implementation.
src/native/nativeaot/host/internal-pinvoke-stubs.ccSimplifies abort path for unimplemented pinvokes to avoid extra std deps.
src/native/nativeaot/host/host.ccRemoves std::format usage from NativeAOT host logging path.
src/native/nativeaot/host/cxx-shims.ccAdds minimal C++ allocation / nothrow shims to avoid libc++ dependency.
src/native/nativeaot/host/CMakeLists.txtAdds cxx-shims.cc to NativeAOT host build.
src/native/nativeaot/host/bridge-processing.ccRemoves std::format usage and wires callbacks for NativeAOT bridge processing.
src/native/common/include/shared/helpers.hhAdds Helpers::abort_applicationf formatted abort helper.
src/native/common/include/shared/cpp-util.hhRemoves C++ ranges/string allocations from diagnostics helpers and logging.
src/native/common/include/runtime-base/timing.hhReplaces std::format timing log formatting with snprintf.
src/native/common/include/runtime-base/timing-internal.hhReplaces std::format timing/internal warnings with snprintf-based logging.
src/native/common/include/runtime-base/strings.hhReplaces some std-container usage and updates formatting/logging to avoid STL pulls.
src/native/common/include/runtime-base/jni-wrappers.hhReplaces new[]/delete[] usage with malloc + placement-new where needed.
src/native/clr/shared/log_functions.ccIntroduces vprintf-style log helpers (log_writev, log_*_fmt).
src/native/clr/shared/helpers.ccAdds implementation of Helpers::abort_applicationf; switches some fatal logging to printf-style.
src/native/clr/runtime-base/util.ccUpdates logging calls to new printf-style logging helpers.
src/native/clr/runtime-base/logger.ccRefactors gref/lref log path storage away from std::string; updates open/log messages.
src/native/clr/runtime-base/android-system-shared.ccReplaces some new[]/delete[] allocations and formatting with malloc/snprintf and fmt logging.
src/native/clr/include/shared/log_types.hhAdds printf-style logging APIs and adjusts macro behavior for XA_HOST_NATIVEAOT builds.
src/native/clr/include/runtime-base/util.hhSwitches to formatted abort/log helpers; adds mmap failure formatted aborts and info logging.
src/native/clr/include/runtime-base/android-system.hhChanges override-dir storage away from std::string for NativeAOT compatibility; API now returns const char*.
src/native/clr/include/host/os-bridge.hhAdds formatted gref logging overloads and refactors internal logging helpers signatures.
src/native/clr/include/host/host-environment.hhUpdates debug/warn logging calls to avoid std::format and skip work when disabled.
src/native/clr/include/host/gc-bridge.hhReplaces std::thread/semaphore with pthread/sem_t for compatibility and reduced STL usage.
src/native/clr/include/host/bridge-processing.hhRemoves now-unneeded CoreCLR no-op overrides after shared callback refactor.
src/native/clr/include/host/bridge-processing-shared.hhIntroduces callback plumbing and switches temporary peer map to robin_map.
src/native/clr/host/os-bridge.ccRefactors stack-trace/gref logging to avoid std::format and centralize formatted logging.
src/native/clr/host/internal-pinvokes-shared.ccUpdates managed->native log forwarding to respect category enablement and use log_write.
src/native/clr/host/host-shared.ccReplaces std::format/string_view literal usage in error logging with printf-style.
src/native/clr/host/gc-bridge.ccMigrates to sem_wait/atomic builtins and updates logging formatting.
src/native/clr/host/fastdev-assemblies.ccUpdates override-dir handling to match AndroidSystem API change to const char*.
src/native/clr/host/bridge-processing.ccImplements callback-based bridge processing shared logic and temporary peer lifecycle handling.
Comments suppressed due to low confidence (1)

src/native/clr/host/os-bridge.cc:203

  • OSBridge::log_it() logs the main line to logcat unconditionally via log_write(...), even when logcat_enabled is false. This makes gref/lref logging emit to logcat even in the “file-only” configuration, and the logcat_enabled parameter only affects stack traces. Consider only calling log_write when logcat_enabled is true (and keep file writes independent), so behavior matches Logger::{gref,lref}_to_logcat().
void OSBridge::log_it (LogCategories category, const char *line, FILE *to, const char *const from, bool logcat_enabled) noexcept
{
log_write (category, LogLevel::Info, line);
// We skip logcat here when logging to file is enabled because _write_stack_trace will output to logcat as well, if enabled
if (to == nullptr) {
if (logcat_enabled) {
_write_stack_trace (nullptr, from, category);
}

Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh Outdated
simonrozsivaland others added 5 commits May 27, 2026 20:58
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the missing standard and logging declarations needed by the native runtime headers when they are compiled directly by the Android runtime ninja builds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid ambiguous gref log overload resolution, insert temporary GC bridge peers into robin_map without mutating through the iterator proxy, and include robin_map headers in the NativeAOT host build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the robin_map-backed temporary peer lookup for the CLR host, but use a simple indexed JNI handle table for the NativeAOT host so app-linked static runtimes do not pull in libc++/c++abi symbols.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid allocating a temporary peer slot for every GC bridge component in the NativeAOT host. The NativeAOT path now stores only the temporary peers it creates, while the CLR host continues to use robin_map.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsivaland others added 7 commits May 28, 2026 07:15
Remove the CoreCLR-only robin_map implementation so bridge processing uses the same sparse temporary peer list for both CoreCLR and NativeAOT. Leave a note about a possible future lookup optimization using the SCC Count field.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Encapsulate temporary peer storage in a dedicated RAII type and encode temporary peer indexes in the SCC count while the bridge owns the GC cross-reference arguments. Share the implementation across CoreCLR and NativeAOT, keeping only the NativeAOT GCUserPeerable callbacks runtime-specific.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert native runtime logging macros to use printf-style helpers across CLR, Mono, and NativeAOT. This removes the NativeAOT-only preformatted logging branch while preserving category gating for debug and info logs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid calling GetMethodID with a null jclass when initializing the temporary peer map. This preserves the intended abort message when the runtime field lookup fails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explain that these shims intentionally cover only the no-libc++ allocation symbols needed by the NativeAOT runtime pack while native code is built without C++ exceptions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop libc++ from NativeAOT linking reduces libUnnamedProject.so by
~625KB. Refresh the BuildReleaseArm64 size-regression reference
descriptions from the latest CI build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 9, 2026 21:54
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in 8dcf139 by merging the latest origin/main into this branch and fixing the remaining conflicts in the CoreCLR pinvoke override files.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 26, 2026 21:43
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in d50cd69 and follow-up 921fef6. I merged the latest origin/main, fixed the remaining conflicts in the GC bridge and apkdesc baselines, and updated the external/xamarin-android-tools/external/Java.Interop submodule pointers from the merged mainline state.

@simonrozsivalsimonrozsival added the drop-libcpp Work to remove the libc++ dependency from Android NativeAOT label Jul 16, 2026
simonrozsival added a commit that referenced this pull request Jul 17, 2026
Use the negative-index TemporaryPeerMap approach from #11311 so CoreCLR and NativeAOT no longer need robin-map for GC bridge processing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa
simonrozsival added a commit that referenced this pull request Jul 20, 2026
## Summary
Replace the shared CoreCLR/NativeAOT GC bridge's temporary-peer `std::unordered_map` with an allocation-backed `TemporaryPeerMap`.
The design follows the approach previously developed in #11311: an empty strongly connected component temporarily carries an encoded peer-array index in `StronglyConnectedComponent.Count`, so the bridge does not need a general-purpose C++ hash table during its scoped cross-reference pass.
Split from #12142. The two PRs are independent and can merge in either order. Part of #12139.
## Background
During GC bridge processing, each strongly connected component (SCC) must behave like one Java object:
- `Count == 1`: the existing Java peer represents the SCC directly;
- `Count > 1`: the bridge adds circular references so all peers remain alive or are collected together;
- `Count == 0`: there is no Java peer, so the bridge creates a temporary `mono.android.GCUserPeer` solely to represent that SCC while cross-SCC references are established.
The previous implementation stored those temporary peers in `std::unordered_map<size_t, jobject>`, keyed by SCC index. The required key set and capacity are already known before processing begins, and lookup is only needed within one short scope, making a hash table unnecessary.
## Implementation
Files:
- `src/native/clr/include/host/bridge-processing-shared.hh`
- `src/native/clr/host/bridge-processing.cc`
### `TemporaryPeerMap` lifetime
1. The constructor scans all SCCs, rejects pre-existing marker values, and counts exactly how many temporary peers are required.
2. If none are required, it performs no allocation.
3. Otherwise it reserves JNI local-reference capacity for all temporary peers plus slack, then allocates one zero-initialized `jobject` array with `calloc`.
4. `add()` creates the temporary `GCUserPeer`, stores it in the next array slot, and writes the encoded slot index into the SCC's `Count` field.
5. Cross-reference target selection detects the encoded marker and retrieves the peer directly from the array.
6. At the end of the scoped cross-reference pass, the destructor deletes every temporary JNI local reference, resets every marked SCC to `Count == 0`, frees the array, and clears its bookkeeping.
7. Normal weak-global-reference processing starts only after the destructor has restored the original SCC shape.
### Index encoding
`Count` is unsigned, so the temporary index is stored as `~index`, which has the same bit pattern as `-(index + 1)`:
- index zero remains representable;
- the high bit acts as the temporary-peer marker;
- encoding rejects indexes that already use the marker bit;
- decoding verifies the marker and bounds-checks the resulting array index;
- the constructor verifies that runtime-provided SCC counts do not already use the reserved marker space.
### JNI initialization and safety
- cache the `mono.android.GCUserPeer` class and constructor during runtime initialization;
- preserve the existing cached `mono.android.IGCUserPeer` method IDs used for reference callbacks;
- reserve local-reference capacity before creating a potentially large temporary-peer set;
- clear and log an `EnsureLocalCapacity` failure consistently with the previous implementation;
- fail fast on allocation failure, peer-construction failure, invalid markers, capacity overruns, missing peers, and out-of-range indexes;
- preserve existing fail-fast handling for Java exceptions raised by `monodroidAddReference` or `monodroidClearReferences`;
- explicitly delete copy and move construction/assignment so the owning array and JNI local references cannot be shallow-copied.
## Behavior preserved
- temporary peers remain alive until every cross-SCC reference has been added;
- temporary local references are released before the Java GC is triggered;
- zero-, one-, and multi-peer SCC handling remains unchanged;
- cross-reference source/destination selection and `refs_added` bookkeeping remain unchanged;
- the runtime receives its SCC array back with all temporary markers removed;
- CoreCLR and NativeAOT continue to use the same shared bridge implementation and host-specific peer callback hooks.
## Scope and non-goals
- This PR changes only temporary-peer storage; it does not change the GC bridge graph algorithm or collection policy.
- It does not change Java peer APIs, reference callback names, or GC trigger behavior.
- It does not introduce robin-map or another replacement hash table.
- The unrelated logging/path/source-location ownership cleanup remains in #12142.
## Expected impact
- remove `std::unordered_map` from shared CoreCLR/NativeAOT temporary-peer processing;
- remove the associated `std::__ndk1::__next_prime` and hash-table allocation/code roots;
- replace per-node hash-table bookkeeping with one exact-size array allocation;
- make temporary JNI reference ownership and SCC marker restoration explicit through RAII;
- preserve GC bridge semantics while reducing C++ standard-library reachability.
## Validation
The implementation is the isolated GC bridge change previously carried in #12142, plus explicit non-copyable/non-movable ownership semantics for `TemporaryPeerMap`.
- `git diff --check` passed;
- the ownership hardening is declaration-only and introduces no runtime code;
- CI is validating head `d4315727e`.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

drop-libcppWork to remove the libc++ dependency from Android NativeAOT

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Drop libc++ from Android NativeAOT linking - #11311

Closed
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp
Closed

Drop libc++ from Android NativeAOT linking#11311
simonrozsival wants to merge 22 commits into
mainfrom
dev/simonrozsival/nativeaot-drop-libcpp

Conversation

@simonrozsival

@simonrozsivalsimonrozsival commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

This removes the Android NativeAOT app link-time dependency on libc++.

The branch now:

  • removes the explicit NativeAOT libc++_static.a and libc++abi.a link inputs;
  • removes libc++ packaging from the Android native runtime component list;
  • keeps the final NativeAOT app link on the direct ld.lld/NativeLinker path;
  • reduces NativeAOT-reachable host code that pulled in C++ runtime/STL symbols;
  • adds NativeAOT-local C++ allocation/nothrow shims for the remaining runtime-pack allocation references;
  • shares GC bridge processing between CoreCLR and NativeAOT without tsl::robin_map or other libc++-requiring containers;
  • adds printf-style native logging helpers so call sites can avoid repeated snprintf boilerplate and skip formatting when logging is disabled.

Context

This is related to #9926 and the NDK r29 NativeAOT linking work.

The relevant background is that Android NativeAOT should avoid depending on libc++ instead of working around duplicate libunwind symbols or switching to shared libc++. Related references:

GC bridge / Java peer shape history

The GC bridge sharing in this PR exposed one remaining runtime-specific difference: classic Mono/CoreCLR Java peers use mono.android.* and monodroidAddReference() / monodroidClearReferences(), while NativeAOT trimmable proxy objects use net.dot.jni.GCUserPeerable and jiAddManagedReference() / jiClearManagedReferences().

Brief history:

So the current difference appears to be historical layering rather than a fundamental GC bridge requirement: Mono/CoreCLR kept the older Android-compatible monodroid* Java peer API, while NativeAOT trimmable support was built around new generated net.dot.jni.* proxy sources that avoid the legacy Java.Interop native-registration shape. Follow-up issue: #11526 tracks unifying these Java peer reference APIs, likely by moving the trimmable path toward a single net.dot.* shape while NativeAOT is still experimental.

Size impact

Latest measurements on this branch use samples/NativeAOT/NativeAOT.csproj built in Release with _AndroidTypeMapImplementation=trimmable. APK sizes are for the signed APK.

ArtifactParent libc++ baselineNo-libc++ buildDifference
arm64 APK1,575,849 B1,382,336 B-193,513 B (-12.28%)
x64 APK1,639,459 B1,439,677 B-199,782 B (-12.19%)
arm64 libNativeAOT.so3,481,880 B2,943,232 B-538,648 B (-15.47%)
x64 libNativeAOT.so3,404,896 B2,866,728 B-538,168 B (-15.81%)

libNativeAOT.so is the per-ABI native shared library packaged in the APK, for example lib/arm64-v8a/libNativeAOT.so. It is not the whole Android app package; the APK also contains manifest, resources, Java stubs/classes, signatures, and packaging assets.

Validation

Latest local validation:

  • git diff --check
  • make prepare CONFIGURATION=Debug
  • make all CONFIGURATION=Debug progressed through native-mono, native-nativeaot, and native-clr; the remaining failure was a stale test restore asset issue unrelated to native code.
  • ./dotnet-local.sh build src/native/native-nativeaot.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-clr.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build src/native/native-mono.csproj -c Debug --no-restore -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-arm64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Release -p:_AndroidTypeMapImplementation=trimmable -p:RuntimeIdentifier=android-x64 -v minimal
  • ./dotnet-local.sh build samples/NativeAOT/NativeAOT.csproj -c Debug -t:Install -p:RuntimeIdentifier=android-arm64
  • launched samples/NativeAOT on an arm64 emulator and confirmed Application.OnCreate() and MainActivity.OnCreate() ran without fatal logcat entries.

Previous branch validation:

  • dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj --no-restore — 457 passed.
  • rebuilt src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj;
  • rebuilt NativeAOT runtime archives for android-arm64 and android-x64;
  • built samples/NativeAOT/NativeAOT.csproj with _AndroidTypeMapImplementation=trimmable for android-arm64 and android-x64;
  • verified generated NativeAOT link response files contain no libc++/libc++abi inputs;
  • verified produced APKs contain no libc++ entries;
  • verified llvm-nm -u reports no undefined C++ runtime-looking symbols in the final libNativeAOT.so outputs;
  • smoke-tested the arm64 APK on an arm64 emulator: libNativeAOT.so loaded without libc++, MainApplication and MainActivity native callbacks ran, MainActivity.OnCreate() logged, and the process remained alive.

Notes

The x64 APK builds and packages without libc++, but local x64 runtime validation still needs an x64 emulator host because Android emulator x86_64 system images are not accepted on Apple Silicon/aarch64 hosts.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

Emulator validation update

I tested the signed NativeAOT + trimmable typemap sample APK on an arm64 emulator after rebuilding/overlaying the local validation packs.

What works:

  • The arm64 APK installs and launches on emulator-5554.
  • lib/arm64-v8a/libNativeAOT.so loads successfully with no libc++ packaged in the APK.
  • Startup reaches managed code: MainApplication and MainActivity native callbacks run, MainActivity.OnCreate() logs, and the process remains alive.

Important caveats:

  • This arm64 smoke currently requires trimmable NativeAOT startup fixes plus two validation-only workarounds:
    • pointing ILC TypeMapEntryAssembly at the app typemap (_NativeAOT.TypeMap) instead of the root _Microsoft.Android.TypeMaps, because scanning _Mono.Android.TypeMap currently fails on Android.Text.IInputType being present in the ref assembly but absent from the runtime assembly;
    • skipping UncaughtExceptionMarshaler setup for trimmable NativeAOT to avoid the IUncaughtExceptionHandler/JavaProxyThrowable startup path.
  • I do not consider those two workarounds shippable as-is; they need separate root-cause fixes before this can be more than a feasibility proof.
  • x64 builds and packages without libc++ (-nostdlib++ is in the link response and no libc++ entries are in the APK), but I could not run the x64 APK locally: the Android emulator rejects x86_64 system images on this Apple Silicon host (Avd's CPU Architecture 'x86_64' is not supported by the QEMU2 emulator on aarch64 host). x64 runtime behavior still needs validation on an x64 emulator host.

@simonrozsivalsimonrozsival changed the title [WIP] Explore dropping libc++ from NativeAOT linking[WIP] Drop libc++ from Android NativeAOT linkingMay 12, 2026
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 23, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved by merging origin/main into this branch and fixing all reported conflicts in commit 6e8b48a.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved locally by merging origin/main and fixing the conflict hunks in commit 0199a84. Push from the agent is currently blocked by branch protection (GH013: Cannot update this protected ref), so this commit is ready but not yet on the remote branch.

CopilotAI commented May 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Done in 1c6205f. I merged latest origin/main into this branch and resolved all merge conflicts in the affected typemap generator/targets/tests files.

simonrozsivaland others added 5 commits May 27, 2026 17:35
Remove the explicit NativeAOT final-link dependency on libc++/libc++abi and keep the Android NativeAOT link guarded with -nostdlib++.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Centralize gref log message formatting in OSBridge so call sites can pass printf-style arguments directly and formatting is skipped when gref logging is disabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep gref file guards at the call sites, allow literal single-argument gref log calls, and centralize printf-style log formatting helpers for CLR native logging.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
simonrozsivalforce-pushed the dev/simonrozsival/nativeaot-drop-libcpp branch from 1c6205f to 6a4feeaCompareMay 27, 2026 16:12
@simonrozsivalsimonrozsival changed the title [WIP] Drop libc++ from Android NativeAOT linkingDrop libc++ from Android NativeAOT linkingMay 27, 2026
@simonrozsival
simonrozsival marked this pull request as ready for review May 27, 2026 16:20
CopilotAI review requested due to automatic review settings May 27, 2026 16:20

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 (draft/WIP) explores removing the Android NativeAOT link-time dependency on libc++/libc++abi by adjusting MSBuild NativeAOT linking inputs and runtime component packaging, and by refactoring native runtime code paths to avoid pulling in C++ standard library symbols (including introducing minimal C++ allocation/nothrow shims).

Changes:

  • Remove explicit libc++/libc++abi static link inputs and runtime component entries for NativeAOT Android builds.
  • Add NativeAOT-local C++ runtime shims (operator new/delete + std::nothrow) and refactor multiple native components to avoid std::format/other STL usage.
  • Refactor CLR/native logging and GC bridge processing (including callback plumbing for bridge processing and temporary peer map implementation changes).

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.csDrops libc++/libc++abi from the known runtime archive list.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targetsRemoves libc++/libc++abi from NativeAOT link inputs; updates linker/stdlib commentary.
src/native/nativeaot/include/host/host.hhAdds NativeAOT host header shim include.
src/native/nativeaot/include/host/bridge-processing.hhAdapts NativeAOT bridge-processing to callback-based shared implementation.
src/native/nativeaot/host/internal-pinvoke-stubs.ccSimplifies abort path for unimplemented pinvokes to avoid extra std deps.
src/native/nativeaot/host/host.ccRemoves std::format usage from NativeAOT host logging path.
src/native/nativeaot/host/cxx-shims.ccAdds minimal C++ allocation / nothrow shims to avoid libc++ dependency.
src/native/nativeaot/host/CMakeLists.txtAdds cxx-shims.cc to NativeAOT host build.
src/native/nativeaot/host/bridge-processing.ccRemoves std::format usage and wires callbacks for NativeAOT bridge processing.
src/native/common/include/shared/helpers.hhAdds Helpers::abort_applicationf formatted abort helper.
src/native/common/include/shared/cpp-util.hhRemoves C++ ranges/string allocations from diagnostics helpers and logging.
src/native/common/include/runtime-base/timing.hhReplaces std::format timing log formatting with snprintf.
src/native/common/include/runtime-base/timing-internal.hhReplaces std::format timing/internal warnings with snprintf-based logging.
src/native/common/include/runtime-base/strings.hhReplaces some std-container usage and updates formatting/logging to avoid STL pulls.
src/native/common/include/runtime-base/jni-wrappers.hhReplaces new[]/delete[] usage with malloc + placement-new where needed.
src/native/clr/shared/log_functions.ccIntroduces vprintf-style log helpers (log_writev, log_*_fmt).
src/native/clr/shared/helpers.ccAdds implementation of Helpers::abort_applicationf; switches some fatal logging to printf-style.
src/native/clr/runtime-base/util.ccUpdates logging calls to new printf-style logging helpers.
src/native/clr/runtime-base/logger.ccRefactors gref/lref log path storage away from std::string; updates open/log messages.
src/native/clr/runtime-base/android-system-shared.ccReplaces some new[]/delete[] allocations and formatting with malloc/snprintf and fmt logging.
src/native/clr/include/shared/log_types.hhAdds printf-style logging APIs and adjusts macro behavior for XA_HOST_NATIVEAOT builds.
src/native/clr/include/runtime-base/util.hhSwitches to formatted abort/log helpers; adds mmap failure formatted aborts and info logging.
src/native/clr/include/runtime-base/android-system.hhChanges override-dir storage away from std::string for NativeAOT compatibility; API now returns const char*.
src/native/clr/include/host/os-bridge.hhAdds formatted gref logging overloads and refactors internal logging helpers signatures.
src/native/clr/include/host/host-environment.hhUpdates debug/warn logging calls to avoid std::format and skip work when disabled.
src/native/clr/include/host/gc-bridge.hhReplaces std::thread/semaphore with pthread/sem_t for compatibility and reduced STL usage.
src/native/clr/include/host/bridge-processing.hhRemoves now-unneeded CoreCLR no-op overrides after shared callback refactor.
src/native/clr/include/host/bridge-processing-shared.hhIntroduces callback plumbing and switches temporary peer map to robin_map.
src/native/clr/host/os-bridge.ccRefactors stack-trace/gref logging to avoid std::format and centralize formatted logging.
src/native/clr/host/internal-pinvokes-shared.ccUpdates managed->native log forwarding to respect category enablement and use log_write.
src/native/clr/host/host-shared.ccReplaces std::format/string_view literal usage in error logging with printf-style.
src/native/clr/host/gc-bridge.ccMigrates to sem_wait/atomic builtins and updates logging formatting.
src/native/clr/host/fastdev-assemblies.ccUpdates override-dir handling to match AndroidSystem API change to const char*.
src/native/clr/host/bridge-processing.ccImplements callback-based bridge processing shared logic and temporary peer lifecycle handling.
Comments suppressed due to low confidence (1)

src/native/clr/host/os-bridge.cc:203

  • OSBridge::log_it() logs the main line to logcat unconditionally via log_write(...), even when logcat_enabled is false. This makes gref/lref logging emit to logcat even in the “file-only” configuration, and the logcat_enabled parameter only affects stack traces. Consider only calling log_write when logcat_enabled is true (and keep file writes independent), so behavior matches Logger::{gref,lref}_to_logcat().
void OSBridge::log_it (LogCategories category, const char *line, FILE *to, const char *const from, bool logcat_enabled) noexcept
{
log_write (category, LogLevel::Info, line);
// We skip logcat here when logging to file is enabled because _write_stack_trace will output to logcat as well, if enabled
if (to == nullptr) {
if (logcat_enabled) {
_write_stack_trace (nullptr, from, category);
}

Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh
Comment threadsrc/native/clr/include/runtime-base/util.hh Outdated
simonrozsivaland others added 5 commits May 27, 2026 20:58
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the missing standard and logging declarations needed by the native runtime headers when they are compiled directly by the Android runtime ninja builds.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid ambiguous gref log overload resolution, insert temporary GC bridge peers into robin_map without mutating through the iterator proxy, and include robin_map headers in the NativeAOT host build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep the robin_map-backed temporary peer lookup for the CLR host, but use a simple indexed JNI handle table for the NativeAOT host so app-linked static runtimes do not pull in libc++/c++abi symbols.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid allocating a temporary peer slot for every GC bridge component in the NativeAOT host. The NativeAOT path now stores only the temporary peers it creates, while the CLR host continues to use robin_map.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
simonrozsivaland others added 7 commits May 28, 2026 07:15
Remove the CoreCLR-only robin_map implementation so bridge processing uses the same sparse temporary peer list for both CoreCLR and NativeAOT. Leave a note about a possible future lookup optimization using the SCC Count field.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Encapsulate temporary peer storage in a dedicated RAII type and encode temporary peer indexes in the SCC count while the bridge owns the GC cross-reference arguments. Share the implementation across CoreCLR and NativeAOT, keeping only the NativeAOT GCUserPeerable callbacks runtime-specific.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Convert native runtime logging macros to use printf-style helpers across CLR, Mono, and NativeAOT. This removes the NativeAOT-only preformatted logging branch while preserving category gating for debug and info logs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoid calling GetMethodID with a null jclass when initializing the temporary peer map. This preserves the intended abort message when the runtime field lookup fails.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explain that these shims intentionally cover only the no-libc++ allocation symbols needed by the NativeAOT runtime pack while native code is built without C++ exceptions.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop libc++ from NativeAOT linking reduces libUnnamedProject.so by
~625KB. Refresh the BuildReleaseArm64 size-regression reference
descriptions from the latest CI build.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 9, 2026 21:54
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in 8dcf139 by merging the latest origin/main into this branch and fixing the remaining conflicts in the CoreCLR pinvoke override files.

@simonrozsival

Copy link
Copy Markdown
MemberAuthor

@copilot resolve the merge conflicts in this pull request

CopilotAIand others added 2 commits June 26, 2026 21:43
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

CopilotAI commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved in d50cd69 and follow-up 921fef6. I merged the latest origin/main, fixed the remaining conflicts in the GC bridge and apkdesc baselines, and updated the external/xamarin-android-tools/external/Java.Interop submodule pointers from the merged mainline state.

@simonrozsivalsimonrozsival added the drop-libcpp Work to remove the libc++ dependency from Android NativeAOT label Jul 16, 2026
simonrozsival added a commit that referenced this pull request Jul 17, 2026
Use the negative-index TemporaryPeerMap approach from #11311 so CoreCLR and NativeAOT no longer need robin-map for GC bridge processing.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa
simonrozsival added a commit that referenced this pull request Jul 20, 2026
## Summary
Replace the shared CoreCLR/NativeAOT GC bridge's temporary-peer `std::unordered_map` with an allocation-backed `TemporaryPeerMap`.
The design follows the approach previously developed in #11311: an empty strongly connected component temporarily carries an encoded peer-array index in `StronglyConnectedComponent.Count`, so the bridge does not need a general-purpose C++ hash table during its scoped cross-reference pass.
Split from #12142. The two PRs are independent and can merge in either order. Part of #12139.
## Background
During GC bridge processing, each strongly connected component (SCC) must behave like one Java object:
- `Count == 1`: the existing Java peer represents the SCC directly;
- `Count > 1`: the bridge adds circular references so all peers remain alive or are collected together;
- `Count == 0`: there is no Java peer, so the bridge creates a temporary `mono.android.GCUserPeer` solely to represent that SCC while cross-SCC references are established.
The previous implementation stored those temporary peers in `std::unordered_map<size_t, jobject>`, keyed by SCC index. The required key set and capacity are already known before processing begins, and lookup is only needed within one short scope, making a hash table unnecessary.
## Implementation
Files:
- `src/native/clr/include/host/bridge-processing-shared.hh`
- `src/native/clr/host/bridge-processing.cc`
### `TemporaryPeerMap` lifetime
1. The constructor scans all SCCs, rejects pre-existing marker values, and counts exactly how many temporary peers are required.
2. If none are required, it performs no allocation.
3. Otherwise it reserves JNI local-reference capacity for all temporary peers plus slack, then allocates one zero-initialized `jobject` array with `calloc`.
4. `add()` creates the temporary `GCUserPeer`, stores it in the next array slot, and writes the encoded slot index into the SCC's `Count` field.
5. Cross-reference target selection detects the encoded marker and retrieves the peer directly from the array.
6. At the end of the scoped cross-reference pass, the destructor deletes every temporary JNI local reference, resets every marked SCC to `Count == 0`, frees the array, and clears its bookkeeping.
7. Normal weak-global-reference processing starts only after the destructor has restored the original SCC shape.
### Index encoding
`Count` is unsigned, so the temporary index is stored as `~index`, which has the same bit pattern as `-(index + 1)`:
- index zero remains representable;
- the high bit acts as the temporary-peer marker;
- encoding rejects indexes that already use the marker bit;
- decoding verifies the marker and bounds-checks the resulting array index;
- the constructor verifies that runtime-provided SCC counts do not already use the reserved marker space.
### JNI initialization and safety
- cache the `mono.android.GCUserPeer` class and constructor during runtime initialization;
- preserve the existing cached `mono.android.IGCUserPeer` method IDs used for reference callbacks;
- reserve local-reference capacity before creating a potentially large temporary-peer set;
- clear and log an `EnsureLocalCapacity` failure consistently with the previous implementation;
- fail fast on allocation failure, peer-construction failure, invalid markers, capacity overruns, missing peers, and out-of-range indexes;
- preserve existing fail-fast handling for Java exceptions raised by `monodroidAddReference` or `monodroidClearReferences`;
- explicitly delete copy and move construction/assignment so the owning array and JNI local references cannot be shallow-copied.
## Behavior preserved
- temporary peers remain alive until every cross-SCC reference has been added;
- temporary local references are released before the Java GC is triggered;
- zero-, one-, and multi-peer SCC handling remains unchanged;
- cross-reference source/destination selection and `refs_added` bookkeeping remain unchanged;
- the runtime receives its SCC array back with all temporary markers removed;
- CoreCLR and NativeAOT continue to use the same shared bridge implementation and host-specific peer callback hooks.
## Scope and non-goals
- This PR changes only temporary-peer storage; it does not change the GC bridge graph algorithm or collection policy.
- It does not change Java peer APIs, reference callback names, or GC trigger behavior.
- It does not introduce robin-map or another replacement hash table.
- The unrelated logging/path/source-location ownership cleanup remains in #12142.
## Expected impact
- remove `std::unordered_map` from shared CoreCLR/NativeAOT temporary-peer processing;
- remove the associated `std::__ndk1::__next_prime` and hash-table allocation/code roots;
- replace per-node hash-table bookkeeping with one exact-size array allocation;
- make temporary JNI reference ownership and SCC marker restoration explicit through RAII;
- preserve GC bridge semantics while reducing C++ standard-library reachability.
## Validation
The implementation is the isolated GC bridge change previously carried in #12142, plus explicit non-copyable/non-movable ownership semantics for `TemporaryPeerMap`.
- `git diff --check` passed;
- the ownership hardening is declaration-only and introduces no runtime code;
- CI is validating head `d4315727e`.
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 16, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

drop-libcppWork to remove the libc++ dependency from Android NativeAOT

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@simonrozsival