Skip to content

[CoreCLR] Align AddPeer() peer-replacement rule with MonoVM - #12271

Merged
jonathanpeppers merged 1 commit into
mainfrom
jonathanpeppers-coreclr-peer-replace-parity
Jul 31, 2026
Merged

[CoreCLR] Align AddPeer() peer-replacement rule with MonoVM#12271
jonathanpeppers merged 1 commit into
mainfrom
jonathanpeppers-coreclr-peer-replace-parity

Conversation

@jonathanpeppers

Copy link
Copy Markdown
Member

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():

if (target.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable) &&
        !value.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable)) {
    return true;
}

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.

This change:

  • Adds the missing !value.JniManagedPeerState.HasFlag (Replaceable) condition so the two runtimes agree, along with MonoVM's explanatory comment.
  • Gates 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.

Scope is strictly CoreCLR/MonoVM alignment; no diagnostics, counters, or test changes.

Relationship to #10973

This makes the flaky Android.RuntimeTests.JnienvArrayMarshaling.GetObjectArray failure 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.

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
Copilot AI review requested due to automatic review settings July 30, 2026 17:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Replaceable and the incoming peer is not Replaceable.
  • Gate WarnNotReplacing(...) behind ObjectReferenceManager.LogGlobalReferenceMessages to 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 jonathanpeppers added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jul 30, 2026
@jonathanpeppers
jonathanpeppers enabled auto-merge (squash) July 30, 2026 20:38
@jonathanpeppers
jonathanpeppers merged commit 0390f1a into main Jul 31, 2026
45 checks passed
@jonathanpeppers
jonathanpeppers deleted the jonathanpeppers-coreclr-peer-replace-parity branch July 31, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JnienvArrayMarshaling.GetObjectArray fails on CoreCLR with emulator 36.4.10

3 participants