[Java.Interop] Decouple built-in proxies from ManagedPeer#12152
[Java.Interop] Decouple built-in proxies from ManagedPeer#12152simonrozsival wants to merge 5 commits into
Conversation
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
There was a problem hiding this comment.
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
JavaProxyObjectnative callbacks (equals,hashCode,toString) directly from the managedJavaProxyObjectstatic constructor. - Remove
ManagedPeer.registerNativeMembers()static initializers from JavaJavaProxyObjectandJavaProxyThrowable. - 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
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
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
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
⚠️ 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(notbool) for theequalsjbooleancallback is actually more correct than the old delegate — defaultboolmarshaling is a 4-byte WindowsBOOL, whereas JNIjbooleanis an unsigned 8-bit value. Good catch, and the0/1mapping is clean. - Lazy registration under the existing
CachedValueslock, 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
ProxyObjectMethodsDelegateToManagedValueexercising equality, hash code, andtoStringdelegation to the wrapped managed value. - The trimmable-copy comment is clearer than before.
To confirm / consider (see inline):
⚠️ The process-widestatic bool nativeMethodsRegisteredreplaces a per-class-load Java static initializer. Please confirm no runtime-recreation / classloader-reload path can observe a staletrueand skip registration.- 💡 Duplicate
JavaProxyObjecttype-name literal vs the existingJniTypeNameconst.
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; |
There was a problem hiding this comment.
🤖 nativeMethodsRegistered 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)
There was a problem hiding this comment.
This is fine, we are only targeting Android.
Summary
JavaProxyObject's nativeequals,hashCode, andtoStringcallbacks with UTF-8JniNativeMethodentries and[UnmanagedCallersOnly]function pointersManagedPeer.registerNativeMembers()Java initializers fromJavaProxyObjectandJavaProxyThrowableContext
This is the first implementation phase of #11727.
JavaProxyObjectwas usingManagedPeeronly 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. JNIjbooleanis an unsigned 8-bit value, so theequals(Object)callback returns C#bytethrough adelegate* unmanaged<IntPtr, IntPtr, IntPtr, byte>function pointer;0and1map directly toJNI_FALSEandJNI_TRUE.JavaProxyThrowablepassed an empty registration string and has no managed native-registration callback, so itsManagedPeercall registered nothing and needs no replacement.ManagedPeeritself remains for the existingJavaInterop1JCW path. Removing that path and deletingManagedPeerwill happen separately after the supported consumers migrate toXAJavaInterop1.Testing
git diff --check