[CoreCLR] Align AddPeer() peer-replacement rule with MonoVM - #12271
Merged
jonathanpeppers merged 1 commit intoJul 31, 2026
Conversation
Context: #9862 `Replaceable` marks an *implicit intermediary* peer: when a Java constructor calls an overridden virtual method, control enters managed code before any peer is registered, so Java.Interop invents one via the `(IntPtr, JniHandleOwnership)` constructor and flags it `Replaceable`. The intended rule is that a real, developer-created MCW may displace an intermediary -- but one intermediary must *not* displace another. MonoVM implements both halves of that rule in `JavaObjectValueManager.ShouldReplaceMapping()`. CoreCLR's `JavaMarshalRegisteredPeers.AddPeer()` only checked the first half, so replaceable-replaces-replaceable could happen. Replacing a live registration orphans any already-cached reference to the displaced peer; `Android.App.Application.Context` is the notable victim, as it caches the peer it first sees and never re-reads it, so after a swap `PeekPeer()` starts handing out a different managed peer for the same Java instance. Add the missing `!value.JniManagedPeerState.HasFlag (Replaceable)` condition so the two runtimes agree, and gate the "not replacing" warning behind `ObjectReferenceManager.LogGlobalReferenceMessages` -- mirroring MonoVM's `Logger.LogGlobalRef` guard. With the parity fix in place the declined path becomes a normal startup path rather than a rare one, and the warning's arguments are evaluated eagerly, including two JNI round-trips for `GetJniTypeNameFromInstance()`, even when the sink discards them. This makes the flaky `JnienvArrayMarshaling.GetObjectArray` failure (#10973) rarer, but does not eliminate it: a CI run with this change active still failed with tracing confirming zero replacements had occurred, so a second, rarer mechanism remains unidentified. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d36c22c5-b1d4-4363-8010-af5b57c7ced9
Contributor
There was a problem hiding this comment.
Pull request overview
Aligns CoreCLR’s managed-peer registration behavior with MonoVM so that an implicit intermediary (Replaceable) peer can be displaced by a real developer-created peer, but a Replaceable peer cannot displace another Replaceable peer. It also avoids paying the cost of “not replacing” diagnostics unless global-reference logging is enabled.
Changes:
- Add MonoVM-parity guard: only replace an existing peer when the existing peer is
Replaceableand the incoming peer is notReplaceable. - Gate
WarnNotReplacing(...)behindObjectReferenceManager.LogGlobalReferenceMessagesto avoid unnecessary work on common startup paths.
Comments suppressed due to low confidence (1)
src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs:132
- 💡 For readability, avoid abbreviations like “w/o” in comments (especially in core runtime code). Spelling it out makes the rationale easier to scan.
// transition into managed code w/o a registered instance, and
jonathanpeppers
enabled auto-merge (squash)
July 30, 2026 20:38
rolfbjarne
approved these changes
Jul 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context: #9862
Replaceablemarks an implicit intermediary peer: when a Java constructor calls an overridden virtual method, control enters managed code before any peer is registered, so Java.Interop invents one via the(IntPtr, JniHandleOwnership)constructor and flags itReplaceable. The intended rule is that a real, developer-created MCW may displace an intermediary — but one intermediary must not displace another.MonoVM implements both halves of that rule in
JavaObjectValueManager.ShouldReplaceMapping():CoreCLR's
JavaMarshalRegisteredPeers.AddPeer()only checked the first half, so replaceable-replaces-replaceable could happen. Replacing a live registration orphans any already-cached reference to the displaced peer;Android.App.Application.Contextis the notable victim, as it caches the peer it first sees and never re-reads it, so after a swapPeekPeer()starts handing out a different managed peer for the same Java instance.This change:
!value.JniManagedPeerState.HasFlag (Replaceable)condition so the two runtimes agree, along with MonoVM's explanatory comment.ObjectReferenceManager.LogGlobalReferenceMessages, mirroring MonoVM'sLogger.LogGlobalRefguard. With the parity fix in place the declined path becomes a normal startup path rather than a rare one, and the warning's arguments are evaluated eagerly — including two JNI round-trips forGetJniTypeNameFromInstance()— even when the sink discards them.Scope is strictly CoreCLR/MonoVM alignment; no diagnostics, counters, or test changes.
Relationship to #10973
This makes the flaky
Android.RuntimeTests.JnienvArrayMarshaling.GetObjectArrayfailure rarer, but does not eliminate it, so it does not close #10973. Across ~13 instrumented CI rounds on #12245 the guard dropped that test's failure rate from roughly 0.5 to roughly 0.125 failures per requeue round (p ≈ 0.035), but a run with the guard active still failed, with tracing confirming zero replacements had occurred — so a second, rarer mechanism exists and is still unidentified.