Skip to content

[Java.Interop] Extend peer cleanup regression coverage - #13292

Open
simonrozsival wants to merge 1 commit into
mainfrom
simonrozsival-main-peer-cleanup-fix
Open

simonrozsival wants to merge 1 commit into
mainfrom
simonrozsival-main-peer-cleanup-fix

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Refs #13290.

main already contains both production corrections relevant to this issue: e5b44a4186 (#13016) releases invalid native control blocks after finalization callbacks, and c663cd110f (#12772) routes unregistered disposal through the normal managed-disposal path. This PR adds regression coverage and documents that existing contract; it does not change production behavior or service the affected release branches.

Extend JavaPeerControlBlockTests from 30 to 49 cases:

  • Cover Java.Interop.JavaObject and Java.Interop.JavaException after unregistration, including aliases whose registered wrapper must remain usable, registered-peer no-op disposal, callback entry with a valid JNI reference, repeated disposal, and retry after a managed disposal callback throws.
  • Assert native control-block release for the exact Android.Graphics.Rect.UnregisterFromRuntime() / DisposeUnlessReferenced() sequence from the issue, and check JNI-reference deletion separately in the dedicated JniReferenceLeak run.
  • Exercise throwing finalization callbacks with global, local, invalid, and absent initial control blocks. Extend valid-reference reconstruction and registration checks to initially registered peers, including callbacks that throw.

Document that unregistered peers receive normal managed disposal, release their JNI reference and native block before finalization is suppressed, and do not invalidate another registered wrapper for the same Java instance.

Local validation

Built the in-tree Release SDK and tested on an Android 15/API 35 ARM64 emulator. The source-pinned SDK is 12.0.100-alpha.1.26523.103; these tests target net11.0-android, using the source-built Android pack 37.99.0 and runtime packs 11.0.0-preview.7.26381.103, not the installed RC2 Android workload.

Both make prepare CONFIGURATION=Release and make leeroy CONFIGURATION=Release MSBUILD_ARGS='-v:minimal -m:4 -nr:false' passed. leeroy includes the additional API-level bindings before local workload configuration.

Runtime Category Result
CoreCLR PeerControlBlock 43 passed, 0 failed, 0 skipped
CoreCLR JniReferenceLeak 8 passed, 0 failed, 0 skipped
NativeAOT PeerControlBlock 43 passed, 0 failed, 0 skipped
NativeAOT JniReferenceLeak 8 passed, 0 failed, 0 skipped

Each dedicated reference run includes six peer-control-block cases and two existing class-lookup checks. The actual TRX results contain the same 49 peer-lifetime case names on both runtimes: 102 passing results in total, including the four ancillary checks.

Negative control: Temporarily restoring only the old DisposePeerUnlessReferenced() call to private DisposePeer(h, value), then rebuilding the SDK and rerunning PeerControlBlock, produced exactly seven expected failures: two missing throwing callbacks, four missing managed-disposal callbacks, and the retained native-block pointer in the Rect reproduction. The correct production source was restored byte-for-byte, the SDK rebuilt, and both CoreCLR categories passed again. No mutation remains in this PR.

Exact successful build/install and test commands

Commands ran with isolated SDK, JDK, NDK, and NuGet directories; AndroidNdkDirectory points to the prepared local NDK. All build/install commands below passed. The test outcomes are listed in the table above.

SDK preparation and build:

make prepare CONFIGURATION=Release
make leeroy CONFIGURATION=Release MSBUILD_ARGS='-v:minimal -m:4 -nr:false'

CoreCLR lifetime cases:

export ANDROID_SERIAL=emulator-5584
./dotnet-local.sh build -t:Install -c Release tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -p:RuntimeIdentifier=android-arm64 -p:Device=emulator-5584 -p:IncludeCategories=PeerControlBlock -v:minimal
(
  cd tests/Mono.Android-Tests/Mono.Android-Tests
  ../../../dotnet-local.sh test Mono.Android.NET-Tests.csproj --no-build -c Release -p:RuntimeIdentifier=android-arm64 -p:Device=emulator-5584 -p:IncludeCategories=PeerControlBlock --report-trx --results-directory ../../../bin/TestRelease/TestResults/PeerControlBlock-CoreCLR-Restored
)

CoreCLR dedicated reference cases:

./dotnet-local.sh build -t:Install -c Release tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -p:RuntimeIdentifier=android-arm64 -p:Device=emulator-5584 -p:IncludeCategories=JniReferenceLeak -v:minimal
(
  cd tests/Mono.Android-Tests/Mono.Android-Tests
  ../../../dotnet-local.sh test Mono.Android.NET-Tests.csproj --no-build -c Release -p:RuntimeIdentifier=android-arm64 -p:Device=emulator-5584 -p:IncludeCategories=JniReferenceLeak --report-trx --results-directory ../../../bin/TestRelease/TestResults/JniReferenceLeak-CoreCLR-Restored
)

NativeAOT lifetime cases:

./dotnet-local.sh build -t:Install -c Release tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -p:RuntimeIdentifier=android-arm64 -p:Device=emulator-5584 -p:IncludeCategories=PeerControlBlock -p:PublishAot=true -p:AndroidNdkDirectory="$AndroidNdkDirectory" -v:minimal
(
  cd tests/Mono.Android-Tests/Mono.Android-Tests
  ../../../dotnet-local.sh test Mono.Android.NET-Tests.csproj --no-build -c Release -p:RuntimeIdentifier=android-arm64 -p:Device=emulator-5584 -p:IncludeCategories=PeerControlBlock -p:PublishAot=true -p:AndroidNdkDirectory="$AndroidNdkDirectory" --report-trx --results-directory ../../../bin/TestRelease/TestResults/PeerControlBlock-NativeAOT
)

NativeAOT dedicated reference cases:

./dotnet-local.sh clean tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -c Release -p:RuntimeIdentifier=android-arm64 -p:PublishAot=true -p:IncludeCategories=JniReferenceLeak -p:BuildProjectReferences=false -v:minimal
./dotnet-local.sh build -t:Install -c Release tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -p:RuntimeIdentifier=android-arm64 -p:Device=emulator-5584 -p:IncludeCategories=JniReferenceLeak -p:PublishAot=true -p:AndroidNdkDirectory="$AndroidNdkDirectory" -v:minimal
(
  cd tests/Mono.Android-Tests/Mono.Android-Tests
  ../../../dotnet-local.sh test Mono.Android.NET-Tests.csproj --no-build -c Release -p:RuntimeIdentifier=android-arm64 -p:Device=emulator-5584 -p:IncludeCategories=JniReferenceLeak -p:PublishAot=true -p:AndroidNdkDirectory="$AndroidNdkDirectory" --report-trx --results-directory ../../../bin/TestRelease/TestResults/JniReferenceLeak-NativeAOT-Clean
)

The scoped clean passed and was needed because the first incremental NativeAOT category switch reran the ordinary category. That run was not counted as dedicated reference validation; the clean rebuild produced the verified eight-case result.

Coverage is limited to Android 15 ARM64 on the current source-built CoreCLR and NativeAOT implementations. These results do not validate .NET 10 servicing or a future .NET 11 backport.


Pull Request
title and
description
should follow the
commit-messages.md workflow documentation, and in particular should include:

  • Useful description of why the change is necessary.
  • Links to issues fixed
  • Unit tests

Copilot AI balanced review requested due to automatic review settings October 28, 2026 13:07

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.

🟢 Approval recommended

No active review issues are identified for the peer-cleanup regression coverage and disposal-contract documentation changes.

No open issues 🎉

What changed in this PR

Adds regression coverage for existing Java.Interop peer-cleanup fixes related to #13290 and documents their disposal contract. Production behavior and release branches are unchanged.

Changes:

  • Expands peer-lifetime coverage from 30 to 49 cases, including unregistered disposal, aliases, throwing callbacks, and reconstructed references.
  • Adds the Android.Graphics.Rect reproduction and separate JNI-reference deletion checks.
  • Documents unregistered-peer cleanup and preservation of another registered wrapper.
File Description
tests/​Mono.Android-Tests/​Java.Interop-Tests/​Java.Interop/​JavaPeerControlBlockTests.cs Extends disposal, finalization, native-block, and JNI-reference regression coverage.
external/​Java.Interop/​src/​Java.Interop/​Documentation/​Java.Interop/​IJavaPeerable.xml Clarifies the existing unregistered-peer disposal and cleanup contract.

Review effort: Balanced

jonathanpeppers pushed a commit that referenced this pull request Oct 28, 2026
Backport only the unregistered-disposal correction from c663cd1
(#12772), without its peer-identity and activation changes.

DisposePeerUnlessReferenced() bypasses Disposed()/Dispose(true) when it
calls the private reference-deletion helper.  JavaObject and
JavaException then retain their native control blocks after the JNI
reference is cleared and finalization is suppressed.  Use the public
DisposePeer(value) path so managed disposal precedes normal cleanup.

Add a distinct JavaVMFixture-based fixture for this branch's host JVM
and Android inclusion paths.  Cover valid-reference callback entry,
native-block release, registered no-ops, alias winners, throwing
callbacks and retry, repeated disposal, exact Rect disposal, and
separate warmed global-reference deletion checks.

The source-built Release SDK passes 12 host-JVM tests and 9 ordinary
plus 7 dedicated GREF tests on each of CoreCLR and NativeAOT.  The
original helper produces exactly 6 host and 7 device failures; restore
the production correction before repeating positive validation.

Complement the finalization backport in #13294 without duplicating
its implementation or JavaPeerControlBlockTests fixture.

Refs #13290, #13292, #13294

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 97270e0d-9fba-4a8b-8a04-9b7c25e1cd21
Context: #13290

Main already contains the production fixes in e5b44a4 and c663cd1.
Add coverage and document the existing unregistered-disposal contract.

Add 19 peer-lifetime cases for unregistered disposal and callbacks.
Cover native-block release, alias ownership, exceptions, and JNI handles.
Extend reconstruction checks to registered finalization callbacks.

Source-built Release CoreCLR and NativeAOT each pass all 49 fixture cases.
The old disposal bypass produces seven expected regression failures.
Restore the correct production path and repeat positive validation.

No production behavior changes or servicing backports are included.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f7772f6f-f6e3-4b4a-b9f2-5abe3f7fd82f
@simonrozsival
simonrozsival force-pushed the simonrozsival-main-peer-cleanup-fix branch from 1e0e0ab to 8b99f07 Compare November 4, 2026 11:59
@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Nov 6, 2026

This branch has not been deployed

No deployments
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.

2 participants