Skip to content

[Java.Interop] Decouple built-in proxies from ManagedPeer#12152

Open
simonrozsival wants to merge 5 commits into
mainfrom
dev/simonrozsival/decouple-proxies-from-managedpeer
Open

[Java.Interop] Decouple built-in proxies from ManagedPeer#12152
simonrozsival wants to merge 5 commits into
mainfrom
dev/simonrozsival/decouple-proxies-from-managedpeer

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jul 17, 2026

Copy link
Copy Markdown
Member

Summary

  • register JavaProxyObject's native equals, hashCode, and toString callbacks with UTF-8 JniNativeMethod entries and [UnmanagedCallersOnly] function pointers
  • remove the ManagedPeer.registerNativeMembers() Java initializers from JavaProxyObject and JavaProxyThrowable
  • preserve the separate trimmable proxy's Java identity semantics
  • add focused JVM coverage for managed equality, hash code, and string delegation

Context

This is the first implementation phase of #11727.

JavaProxyObject was using ManagedPeer only as a generic native-registration trampoline. It can register its three callbacks directly while retaining the existing non-trimmable/standalone behavior. Raw UCO function pointers avoid delegate marshaling, delegate lifetime management, and AOT suppressions. JNI jboolean is an unsigned 8-bit value, so the equals(Object) callback returns C# byte through a delegate* unmanaged<IntPtr, IntPtr, IntPtr, byte> function pointer; 0 and 1 map directly to JNI_FALSE and JNI_TRUE.

JavaProxyThrowable passed an empty registration string and has no managed native-registration callback, so its ManagedPeer call registered nothing and needs no replacement.

ManagedPeer itself remains for the existing JavaInterop1 JCW path. Removing that path and deleting ManagedPeer will happen separately after the supported consumers migrate to XAJavaInterop1.

Testing

  • git diff --check
  • Java.Interop JVM tests not run because the local dotnet/android SDK is not built

Register JavaProxyObject native methods directly from managed code and remove the no-op JavaProxyThrowable registration. Preserve the separate trimmable identity proxy behavior and add focused JVM coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 563374a5-ebc3-438c-b134-665c89c28361
Copilot AI review requested due to automatic review settings July 17, 2026 10:44

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

This PR is the first phase of decoupling Java.Interop’s built-in proxy types from ManagedPeer, by moving native registration for JavaProxyObject’s equals/hashCode/toString callbacks into managed code and removing the (now unnecessary) Java-side ManagedPeer.registerNativeMembers() initializers. It keeps the trimmable proxy’s identity semantics distinct, and adds focused JVM tests to validate delegation behavior.

Changes:

  • Register JavaProxyObject native callbacks (equals, hashCode, toString) directly from the managed JavaProxyObject static constructor.
  • Remove ManagedPeer.registerNativeMembers() static initializers from Java JavaProxyObject and JavaProxyThrowable.
  • Add JVM test coverage verifying that JavaProxyObject’s Java method calls delegate to the wrapped managed object.
Show a summary per file
File Description
src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java Clarifies trimmable proxy identity semantics vs the non-trimmable native-callback proxy behavior.
external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs Adds JVM coverage asserting proxy equals/hashCode/toString delegate to the managed wrapped value.
external/Java.Interop/src/Java.Interop/java/net/dot/jni/internal/JavaProxyThrowable.java Removes no-op ManagedPeer.registerNativeMembers() initializer.
external/Java.Interop/src/Java.Interop/java/net/dot/jni/internal/JavaProxyObject.java Removes ManagedPeer.registerNativeMembers() initializer; relies on managed-side native registration.
external/Java.Interop/src/Java.Interop/Java.Interop/JavaProxyObject.cs Implements managed static-constructor native registration for equals/hashCode/toString.

Copilot's findings

  • Files reviewed: 5/5 changed files
  • Comments generated: 0

simonrozsival and others added 2 commits July 17, 2026 12:54
Register the proxy natives with UTF-8 JniNativeMethod entries and direct unmanaged function pointers, avoiding delegate marshaling and AOT suppressions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 563374a5-ebc3-438c-b134-665c89c28361
Perform the one-time UCO registration under the proxy cache lock immediately before constructing the first JavaProxyObject, avoiding static-constructor timing and allowing retries after registration failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 563374a5-ebc3-438c-b134-665c89c28361
simonrozsival and others added 2 commits July 17, 2026 13:05
Keep RegisterNativeMethods safe at the signature level and scope pointer operations to an explicit unsafe block.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 563374a5-ebc3-438c-b134-665c89c28361
@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jul 17, 2026
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Android PR Reviewer completed successfully!

@github-actions github-actions Bot 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.

⚠️ Needs Changes (1 question to confirm)

Nice, focused change. Decoupling the built-in proxies from ManagedPeer and registering the three callbacks directly with blittable JniNativeMethod + [UnmanagedCallersOnly] function pointers is the right direction for AOT and avoids delegate marshaling/lifetime concerns.

Positive callouts:

  • Returning byte (not bool) for the equals jboolean callback is actually more correct than the old delegate — default bool marshaling is a 4-byte Windows BOOL, whereas JNI jboolean is an unsigned 8-bit value. Good catch, and the 0/1 mapping is clean.
  • Lazy registration under the existing CachedValues lock, before the first peer is allocated, is a tidy way to guarantee the natives are in place before Java can call them.
  • Good JVM coverage in ProxyObjectMethodsDelegateToManagedValue exercising equality, hash code, and toString delegation to the wrapped managed value.
  • The trimmable-copy comment is clearer than before.

To confirm / consider (see inline):

  • ⚠️ The process-wide static bool nativeMethodsRegistered replaces a per-class-load Java static initializer. Please confirm no runtime-recreation / classloader-reload path can observe a stale true and skip registration.
  • 💡 Duplicate JavaProxyObject type-name literal vs the existing JniTypeName const.

CI: checks are still pending (no failures yet); the PR notes JVM tests weren't run locally since the SDK wasn't built, so rely on pipeline results before merge.

Generated by Android PR Reviewer for #12152 · 118.5 AIC · ⌖ 13.1 AIC · ⊞ 6.8K
Comment /review to run again


static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaProxyObject));
static readonly ConditionalWeakTable<object, JavaProxyObject> CachedValues = new ConditionalWeakTable<object, JavaProxyObject> ();
static bool nativeMethodsRegistered;

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.

🤖 ⚠️ JNI interopnativeMethodsRegistered is a process-wide static flag, but the previous approach registered the callbacks from a Java static { ManagedPeer.registerNativeMembers(...) } initializer, which re-runs every time the class is loaded (e.g., under a fresh classloader / recreated runtime). With this flag, if a second JniRuntime is created in the same process — or the class is reloaded under a new classloader — registration is skipped and the new class's equals/hashCode/toString natives are left unregistered, leading to UnsatisfiedLinkError at runtime. On Android this is fine (single runtime per process), but please confirm no test/runtime-restart scenario reuses this stale flag. If reuse is possible, key the flag off the runtime instance rather than a plain bool.

(Rule: JNI native registration lifecycle)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is fine, we are only targeting Android.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drop-javainterop1 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