[TrimmableTypeMap] Extract JavaMarshal registered peer tracking - #11750
Conversation
Move the JavaMarshal peer-registration and GC bridge bookkeeping out of JavaMarshalValueManager into a reusable JavaMarshalRegisteredPeers helper, and share the trimmable peer-type/cast helpers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors JavaMarshal’s managed peer tracking by extracting peer registration/GC-bridge bookkeeping out of JavaMarshalValueManager into a dedicated JavaMarshalRegisteredPeers helper, and centralizing shared peer-type / cast-compatibility helpers.
Changes:
- Add
JavaMarshalRegisteredPeersto own registered-peer bookkeeping and GC-bridge integration;JavaMarshalValueManagerdelegates peer tracking to it. - Add
JavaMarshalValueManagerHelperto share peer-type resolution and cast-compatibility checks. - Wire the new source files into
Mono.Android.csproj.
Show a summary per file
| File | Description |
|---|---|
| src/Mono.Android/Mono.Android.csproj | Includes new runtime helper classes in the build. |
| src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs | Delegates peer tracking to JavaMarshalRegisteredPeers and uses shared helper methods. |
| src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs | New extracted peer registration + GC bridge bookkeeping implementation. |
| src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManagerHelper.cs | New shared helpers for peer type resolution and cast compatibility diagnostics. |
Copilot's findings
- Files reviewed: 4/4 changed files
- Comments generated: 2
Drop the JavaMarshalValueManagerHelper extraction from this PR so the change remains focused on registered peer tracking and GC bridge sharing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
🤖 Code Review — faithful refactor, no behavior change
This is a clean code-move refactor: the peer-registration / GC-bridge bookkeeping is extracted from JavaMarshalValueManager into a new sealed JavaMarshalRegisteredPeers : IDisposable, with the value manager delegating AddPeer/PeekPeer/RemovePeer/FinalizePeer/CollectPeers/GetSurfacedPeers.
I diffed the moved code line-by-line against the base version. The only non-mechanical changes are all behavior-preserving, and I verified the two that looked substantive:
GetJniIdentityHashCode (reference)→JniEnvironment.References.GetIdentityHashCode (reference)(inPeekPeer). Equivalent: both ultimately callJniSystem.IdentityHashCode— the baseJniValueManager.GetJniIdentityHashCodejust wraps it, andReferences.GetIdentityHashCodeis a one-liner over the same call. The substitution is needed because this type isn't a value manager, and it keeps the lookup key consistent withvalue.JniIdentityHashCode(set viaJNIEnv.IdentityHash→ the sameReferences.GetIdentityHashCode).Runtime.ObjectReferenceManager→JniEnvironment.Runtime.ObjectReferenceManager(inWarnNotReplacing/FinalizePeer). Equivalent given the single process-wideAndroidRuntime.- Nullability tightened —
WeakReference<IJavaPeerable?>andIJavaPeerable? targetcorrectly type values that can be null (the originals had latent CS8600/CS8625). 👍 sealed+ a directIDisposable.Dispose ()(noDispose(bool)pattern) matches the repo convention for sealed types. 👍
Mono.Android.csproj uses explicit <Compile Include> items (no globbing), so the added include is correct with no duplicate-item risk.
Findings
0 ❌ · 0 llvm-ir and trimmable-typemap managers, the native side abort_unless-es on a second clr_initialize_gc_bridge, so the "exactly one instance per process" contract is worth making explicit before the follow-up lands.
CI
Azure DevOps checks were green at review time (build & smoke, MSBuild, emulator, and most APK suites passing); a couple of jobs (the dotnet-android aggregate and APKs 2) were still queued/in-progress. No failures observed.
Nice, well-scoped split — easy to review independently. Submitting as a comment (not an approval) per review policy.
Generated by Android PR Reviewer for issue #11750 · 1.7K AIC · ⌖ 66.5 AIC · ⊞ 40K
Comment /review to run again
Address PR review feedback on the extracted peer-tracking type: - The GC bridge holds a native GCHandle to this instance and clr_initialize_gc_bridge is a process-global, one-shot init that aborts if run twice. Constructing a second instance (e.g. from a second value manager) would abort the process. Make it a singleton with a private constructor + static Instance so exactly one instance can ever exist per process. - Remove the IDisposable/Dispose/ThrowIfDisposed members: the instance lives for the lifetime of the process and is never collected (the GC bridge keeps a native reference), so Dispose was never meant to be called and only invited confusion about leaks/reuse. - Document the process-lifetime and single-instance invariants in an XML summary on the type. - JavaMarshalValueManager now uses JavaMarshalRegisteredPeers.Instance and no longer disposes it. - Drop a stray double blank line in BridgeProcessingFinished. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Since the registered-peer tracking is process-wide and never torn down, there's no need for an instance or for the native side to hold a handle back to it. Make the type purely static and drop the round-trip handle. C#: - JavaMarshalRegisteredPeers becomes a static class. The constructor is replaced by an idempotent `internal static void InitializeIfNeeded()` guarded by a lock + flag: the first call runs the one-shot GC-bridge init, subsequent calls return immediately. - BridgeProcessingFinished no longer receives the registered-peers handle and no longer resolves it via GCHandle<T>.FromIntPtr; it calls the now static ProcessCollectedContexts directly. - clr_initialize_gc_bridge P/Invoke drops the IntPtr handle parameter and the finished callback is now delegate* unmanaged<MarkCrossReferencesArgs*, void>. - JavaMarshalValueManager calls InitializeIfNeeded() from its constructor and uses the static peer-tracking methods. Native (CoreCLR/NativeAOT host): - BridgeProcessingFinishedFtn drops its intptr_t parameter. - GCBridge::initialize_callback drops the java_marshal_value_manager argument; the java_marshal_value_manager_handle field and its use in bridge_processing() are removed. - clr_initialize_gc_bridge (pinvoke entry + declaration) drops the handle parameter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
🤖 Review summary — looks good overall
This is a clean, well-structured mechanical refactor: peer-registration and GC-bridge bookkeeping is extracted out of JavaMarshalValueManager into a new process-wide static JavaMarshalRegisteredPeers, and the now-unused intptr_t java_marshal_value_manager_handle parameter is dropped from clr_initialize_gc_bridge and the bridge-finished callback. The new doc comments explaining the process-lifetime / one-shot-initialization semantics are a nice touch.
Verifications I ran (all behavior-preserving):
- ✅ The native callback signature change (
clr_initialize_gc_bridge,BridgeProcessingStarted/BridgeProcessingFinished) is consistent across the managed P/Invokes (RuntimeNativeMethods.cs),internal-pinvokes.hh,internal-pinvokes-shared.cc,gc-bridge.hh/gc-bridge.cc— no danglingjava_marshal_value_managerreferences remain. - ✅
PeekPeerswitching fromGetJniIdentityHashCode (reference)toJniEnvironment.References.GetIdentityHashCode (reference)is equivalent — both resolve toJniSystem.IdentityHashCode (reference). - ✅
FinalizePeer's twoRemovePeer (value)calls are in mutually exclusive branches (invalid/local vs. valid/non-local), matching the upstreamManagedValueManagerstructure — not a double-removal. - ✅
InitializeIfNeededuses a plainlock+boolflag (not double-checked locking), so it's correctly idempotent.
Suggestions (non-blocking, posted inline):
- 💡 Resource management — the refactor drops the
ThrowIfDisposed ()guard from the five delegating peer methods whileCreatePeer/ActivatePeerkeep it; worth confirming that's intentional (and if so, thedisposed/ThrowIfDisposed ()machinery is now nearly vestigial). - 💡 Code organization — the new helpers are
public staticon an internal class with a single same-assembly caller;internal staticwould matchInitializeIfNeededand repo convention.
Severity counts: 0 ❌ · 0
⏳ CI: the public dotnet-android builds (Mac/Windows/Linux) were still in progress at review time — not failing, but not yet green. Please hold the merge until CI goes green; the two suggestions above are not blocking.
Generated by Android PR Reviewer for issue #11750 · 1.7K AIC · ⌖ 48.2 AIC · ⊞ 37.9K
Comment /review to run again
jonathanpeppers
left a comment
There was a problem hiding this comment.
Looks good — making the GC-bridge bookkeeping a process-wide static matches its actual lifetime semantics (one-shot clr_initialize_gc_bridge, detached bridge-processing thread that can never be torn down), and removes the awkward managed→native→managed handle round-trip.
Why this is effectively behavior-neutral
- Peer table is already process-global.
HandleContext.referenceTrackingHandleswas already astatic readonlydictionary nested inJavaMarshalValueManager, so promotingRegisteredInstances/CollectedContextsto statics changes ownership, not lifetime or sharing. - GC-bridge init was already one-shot. Native
initialize_callbackaborts if called twice; the old constructor path implicitly relied on a single value manager.InitializeIfNeeded(lock + flag) makes that explicit and idempotent, so multiple future value managers (llvm-ir, trimmable-typemap) can safely share it. - No managed instance was ever needed natively. The old
intptr_t java_marshal_value_manager_handlewas only round-tripped back intoBridgeProcessingFinishedto call instance methods that touched static state anyway. Dropping the handle and the native field is a pure simplification. - Callbacks are semantically identical.
BridgeProcessingStarted/BridgeProcessingFinished/ProcessCollectedContextswere moved verbatim; the only delta isinstance.ProcessCollectedContexts→ static call. - API surface preserved.
AddPeer/PeekPeer/RemovePeer/FinalizePeer/CollectPeers/GetSurfacedPeersoverrides still exist and forward 1:1; locking discipline (lock (RegisteredInstances),lock (referenceTrackingHandles)) is unchanged. - Equivalent helpers.
GetJniIdentityHashCode(reference)→JniEnvironment.References.GetIdentityHashCode(reference)andRuntime.ObjectReferenceManager→JniEnvironment.Runtime.ObjectReferenceManagerboth resolve to the same underlying calls (JniSystem.IdentityHashCode/ the value manager's ownRuntime). - Minor nullability tightening.
WeakReference<IJavaPeerable>→WeakReference<IJavaPeerable?>andIJavaPeerable target→IJavaPeerable? targetinRemovePeerare correctness improvements that matchTarget's actual signature; no runtime change.
Note (non-blocking)
ThrowIfDisposed() is no longer invoked from the peer methods (it remains in CreatePeer). Since the value manager is process-lifetime and the underlying state is now correctly modeled as process-global, this is fine — but the disposed field / ThrowIfDisposed helper is now nearly dead and could be cleaned up in a follow-up if desired.
…TypeManagers (#11799) ## Summary Bumps **external/Java.Interop** and aligns Mono.Android's reflection-based type and value managers with the new base-class contracts. The Java.Interop bump relaxes the `[DynamicallyAccessedMembers]` (DAM) requirements on the virtual members of `JniRuntime.ReflectionJniTypeManager` / `JniRuntime.ReflectionJniValueManager`, so the Mono.Android overrides no longer need to repeat those annotations and instead rely on targeted trimmer/AOT suppressions. This is a standalone slice of #11617 with no dependency on the trimmable type-map scanner/emitter or array codegen work. ## Changes - `external/Java.Interop` → `8d544738a` (from `70493645c`). - Drop now-redundant `[DynamicallyAccessedMembers]` annotations from the overrides in `AndroidTypeManager`, `ManagedTypeManager`, and `TrimmableTypeMapTypeManager` (`GetInvokerTypeCore`, `GetTypeForSimpleReference`, `RegisterNativeMembers`, `ActivatePeer`), replacing them with `[UnconditionalSuppressMessage]` where the trimmer still needs reassurance. - `JavaMarshalValueManager` now extends `JniRuntime.ReflectionJniValueManager` directly. It is marked `sealed` and carries `[RequiresDynamicCode]` / `[RequiresUnreferencedCode]`, uses the base `EnsureNotDisposed ()` helper, and drops its own dispose tracking and `ActivatePeer` override. - Remove the superseded `AndroidReflectionJniValueManager` and `SimpleValueManager` (and their `Mono.Android.csproj` entries). - `JNIEnvInit.CreateValueManager` creates the value manager through a local helper with the appropriate trimming/AOT suppressions for both the CoreCLR and NativeAOT paths. - No NativeAOT default change; the trimmable type/value managers are **not** part of this PR. ## Tests / baselines - Update the API-compatibility baseline: `Android.Graphics.ColorValueMarshaler.CreateGenericValue`'s `targetType` parameter no longer carries a DAM attribute (inherited from the Java.Interop base-class change). - Refresh `SimpleDotNet` CoreCLR/NativeAOT apkdesc size baselines and the NativeAOT `BuildHasNoWarnings` count, plus `BuildTest2`. ## Context Carved out of #11617. `JavaMarshalRegisteredPeers` extraction already merged via #11750. The trimmable managers, scanner/emitter, manifest, and R8 changes ship in their own PRs (#11749/#11751/#11753/#11769/#11794/#11796/#11798). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Goal
Extract the JavaMarshal peer-registration and GC-bridge bookkeeping from the monolithic
JavaMarshalValueManagerintoJavaMarshalRegisteredPeers. This will allow us to share code between "coreclr llvm-ir" and "coreclr/nativeaot trimmable typemap" value managers which interact with the CoreCLR GC bridge.This is the narrow Peer registration / GC bridge slice from #11617, split out so the common infrastructure can be reviewed independently before the broader Java.Interop value-manager/type-manager work lands.
Change map
JavaMarshalRegisteredPeers, containing registered peer tracking, reference-tracking handles, CoreCLR GC bridge callbacks, finalization, and surfaced-peer enumeration.JavaMarshalValueManager.JavaMarshalValueManagerfocused on value-manager behavior by delegatingAddPeer,PeekPeer,RemovePeer,FinalizePeer,CollectPeers, andGetSurfacedPeers.JavaMarshalRegisteredPeers.InitializeIfNeeded ()performs the one-shotclr_initialize_gc_bridgeregistration; the value manager calls it from its constructor.clr_initialize_gc_bridgeand the nativeGCBridgeno longer take/store ajava_marshal_value_managerhandle, and the finished callback no longer receives it.JavaMarshalRegisteredPeers.cstoMono.Android.csproj.The main deletion theme, matching #11617, is: remove peer-registration/GC-bridge scaffolding from the old value manager shape. The main addition theme is: make that shared behavior reusable by multiple value managers later.
Design notes
The registered-peer table and the GC-bridge registration are process-wide and never torn down (the detached bridge-processing thread cannot be stopped, and
clr_initialize_gc_bridgeaborts if it runs twice). To reflect that:JavaMarshalRegisteredPeersis a purely static type. Initialization happens through an idempotentinternal static void InitializeIfNeeded ()(lock + flag): the first call performs the one-shot init, subsequent calls return immediately, so every value manager can safely call it.Dispose/IDisposable— the type lives for the lifetime of the process and is never collected.clr_initialize_gc_bridgedrops itsIntPtrparameter, the finished callback becomesdelegate* unmanaged<MarkCrossReferencesArgs*, void>, and the correspondingintptr_tplumbing is removed from the CoreCLR/NativeAOT host (gc-bridge.hh/.cc,internal-pinvokes).Review notes / intentional non-goals
CreatePeerbehavior.CoreClrJavaMarshalValueManagerorTrimmableTypeMapValueManager.Testing
Not run locally (no provisioned SDK/NDK in the authoring environment); CI's CoreCLR and NativeAOT device legs exercise this path and will validate.