From 882ce7ca044147ca75f32bb48ab683fb0e2d0bc4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 3 May 2026 08:58:24 +0200 Subject: [PATCH 1/3] Use trimmable Java proxy runtime sources Add trimmable-specific java_runtime jars that replace Java.Interop's JavaProxyObject and JavaProxyThrowable sources only for the trimmable typemap path. Keep the existing runtime jars on Java.Interop's native-registration behavior and select the trimmable jars when _AndroidTypeMapImplementation is trimmable. Re-enable the affected Java.Interop runtime tests and add focused coverage for JavaProxyObject marshaling/object methods under the trimmable typemap path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../installers/create-installers.targets | 4 ++ .../Xamarin.Android.Common.targets | 12 +++- src/java-runtime/java-runtime.targets | 20 +++++- .../net/dot/jni/internal/JavaProxyObject.java | 43 +++++++++++++ .../dot/jni/internal/JavaProxyThrowable.java | 29 +++++++++ .../TrimmableTypeMapTypeManagerTests.cs | 63 +++++++++++++++++++ .../NUnitInstrumentation.cs | 27 -------- 7 files changed, 168 insertions(+), 30 deletions(-) create mode 100644 src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java create mode 100644 src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyThrowable.java diff --git a/build-tools/installers/create-installers.targets b/build-tools/installers/create-installers.targets index c2cf2524afc..9a722d09ef0 100644 --- a/build-tools/installers/create-installers.targets +++ b/build-tools/installers/create-installers.targets @@ -129,12 +129,16 @@ <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)bundletool.jar" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_net6.jar" ExcludeFromLegacy="true" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_fastdev_net6.jar" ExcludeFromLegacy="true" /> + <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_trimmable_net6.jar" ExcludeFromLegacy="true" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_net6.dex" ExcludeFromLegacy="true" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_fastdev_net6.dex" ExcludeFromLegacy="true" /> + <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_trimmable_net6.dex" ExcludeFromLegacy="true" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_clr.jar" ExcludeFromLegacy="true" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_fastdev_clr.jar" ExcludeFromLegacy="true" /> + <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_trimmable_clr.jar" ExcludeFromLegacy="true" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_clr.dex" ExcludeFromLegacy="true" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_fastdev_clr.dex" ExcludeFromLegacy="true" /> + <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)java_runtime_trimmable_clr.dex" ExcludeFromLegacy="true" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)manifestmerger.jar" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)proguard-android.txt" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)protobuf-net.dll" /> diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index ff9c93940e6..8b010889974 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1392,13 +1392,21 @@ because xbuild doesn't support framework reference assemblies. - + <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime_net6.jar - + + <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime_trimmable_net6.jar + + + <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime_clr.jar + + + <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime_trimmable_clr.jar + $(IntermediateOutputPath)release-net6.txt ..\..\src-ThirdParty\bazel\java\mono\android\debug\MultiDexLoader.java;java\mono\android\debug-net6\BuildConfig.java;java\mono\android\debug\BuildConfig.java;java\mono\android\release\BuildConfig.java;java\mono\android\clr\MonoPackageManager.java + <_RuntimeOutput Include="$(OutputPath)java_runtime_trimmable_net6.jar"> + $(OutputPath)java_runtime_trimmable_net6.jar + $(OutputPath)java_runtime_trimmable_net6.dex + $(IntermediateOutputPath)release-trimmable-net6 + $(IntermediateOutputPath)release-trimmable-net6.txt + ..\..\src-ThirdParty\bazel\java\mono\android\debug\MultiDexLoader.java;java\mono\android\debug-net6\BuildConfig.java;java\mono\android\debug\BuildConfig.java;java\mono\android\release\BuildConfig.java;java\mono\android\clr\MonoPackageManager.java;$(JavaInteropSourceDirectory)\src\Java.Interop\java\net\dot\jni\internal\JavaProxyObject.java;$(JavaInteropSourceDirectory)\src\Java.Interop\java\net\dot\jni\internal\JavaProxyThrowable.java + java-trimmable\net\dot\jni\internal\JavaProxyObject.java;java-trimmable\net\dot\jni\internal\JavaProxyThrowable.java + <_RuntimeOutput Include="$(OutputPath)java_runtime_fastdev_net6.jar"> $(OutputPath)java_runtime_fastdev_net6.jar $(OutputPath)java_runtime_fastdev_net6.dex @@ -39,6 +47,14 @@ $(IntermediateOutputPath)release-clr.txt ..\..\src-ThirdParty\bazel\java\mono\android\debug\MultiDexLoader.java;java\mono\android\debug-net6\BuildConfig.java;java\mono\android\debug\BuildConfig.java;java\mono\android\release\BuildConfig.java;java\mono\android\MonoPackageManager.java + <_RuntimeOutput Include="$(OutputPath)java_runtime_trimmable_clr.jar"> + $(OutputPath)java_runtime_trimmable_clr.jar + $(OutputPath)java_runtime_trimmable_clr.dex + $(IntermediateOutputPath)release-trimmable-clr + $(IntermediateOutputPath)release-trimmable-clr.txt + ..\..\src-ThirdParty\bazel\java\mono\android\debug\MultiDexLoader.java;java\mono\android\debug-net6\BuildConfig.java;java\mono\android\debug\BuildConfig.java;java\mono\android\release\BuildConfig.java;java\mono\android\MonoPackageManager.java;$(JavaInteropSourceDirectory)\src\Java.Interop\java\net\dot\jni\internal\JavaProxyObject.java;$(JavaInteropSourceDirectory)\src\Java.Interop\java\net\dot\jni\internal\JavaProxyThrowable.java + java-trimmable\net\dot\jni\internal\JavaProxyObject.java;java-trimmable\net\dot\jni\internal\JavaProxyThrowable.java + <_RuntimeOutput Include="$(OutputPath)java_runtime_fastdev_clr.jar"> $(OutputPath)java_runtime_fastdev_clr.jar $(OutputPath)java_runtime_fastdev_clr.dex @@ -50,14 +66,16 @@ + <_RuntimeSource Include="@(AllRuntimeSource)" /> <_RuntimeSource Remove="%(_RuntimeOutput.RemoveItems)" /> + <_RuntimeSource Include="%(_RuntimeOutput.AddItems)" Condition=" '%(_RuntimeOutput.AddItems)' != '' " /> managedReferences = new ArrayList(); + + // This trimmable runtime copy cannot use Java.Interop's native object methods: + // those are registered through ManagedPeer.registerNativeMembers, which is not + // supported in the trimmable typemap path. + @Override + public boolean equals(Object obj) + { + return this == obj; + } + + @Override + public int hashCode() + { + return System.identityHashCode (this); + } + + @Override + public String toString() + { + return super.toString (); + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyThrowable.java b/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyThrowable.java new file mode 100644 index 00000000000..f5379edf403 --- /dev/null +++ b/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyThrowable.java @@ -0,0 +1,29 @@ +package net.dot.jni.internal; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +/* package */ final class JavaProxyThrowable + extends java.lang.Error + implements GCUserPeerable +{ + ArrayList managedReferences = new ArrayList(); + + public JavaProxyThrowable () { + } + + public JavaProxyThrowable (String message) { + super (message); + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index 54459642bd7..e3378564320 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -179,6 +179,69 @@ public void RegisteredPeer_CanCreateGenericHolder () Assert.AreEqual (42, holder.Value); } + [Test] + public void JavaProxyObject_ValueMarshalerUsesProxyType () + { + AssumeTrimmableTypeMapEnabled (); + + var value = new object (); + var marshaler = JniEnvironment.Runtime.ValueManager.GetValueMarshaler (typeof (object)); + var state = marshaler.CreateObjectReferenceArgumentState (value); + + try { + Assert.AreEqual ("net/dot/jni/internal/JavaProxyObject", JNIEnv.GetClassNameFromInstance (state.ReferenceValue.Handle)); + } finally { + marshaler.DestroyArgumentState (value, ref state); + } + } + + [Test] + public void JavaProxyObject_CanBeUsedInObjectArray () + { + AssumeTrimmableTypeMapEnabled (); + + using var values = new JavaObjectArray (1); + values [0] = new object (); + + Assert.AreEqual ("[Ljava/lang/Object;", values.GetJniTypeName ()); + } + + [Test] + public void JavaProxyObject_ObjectMethodsUseJavaIdentitySemantics () + { + AssumeTrimmableTypeMapEnabled (); + + var value = new object (); + var other = new object (); + var marshaler = JniEnvironment.Runtime.ValueManager.GetValueMarshaler (typeof (object)); + var state = marshaler.CreateObjectReferenceArgumentState (value); + var otherState = marshaler.CreateObjectReferenceArgumentState (other); + + try { + IntPtr proxyClass = JNIEnv.GetObjectClass (state.ReferenceValue.Handle); + try { + IntPtr equals = JNIEnv.GetMethodID (proxyClass, "equals", "(Ljava/lang/Object;)Z"); + IntPtr hashCode = JNIEnv.GetMethodID (proxyClass, "hashCode", "()I"); + IntPtr toString = JNIEnv.GetMethodID (proxyClass, "toString", "()Ljava/lang/String;"); + + Assert.IsTrue (JNIEnv.CallBooleanMethod (state.ReferenceValue.Handle, equals, new JValue (state.ReferenceValue.Handle))); + Assert.IsFalse (JNIEnv.CallBooleanMethod (state.ReferenceValue.Handle, equals, new JValue (otherState.ReferenceValue.Handle))); + Assert.AreEqual ( + JNIEnv.CallIntMethod (state.ReferenceValue.Handle, hashCode), + JNIEnv.CallIntMethod (state.ReferenceValue.Handle, hashCode)); + var proxyString = JNIEnv.GetString (JNIEnv.CallObjectMethod (state.ReferenceValue.Handle, toString), JniHandleOwnership.TransferLocalRef); + Assert.IsTrue ( + proxyString.StartsWith ("net.dot.jni.internal.JavaProxyObject@", StringComparison.Ordinal), + proxyString); + } finally { + JNIEnv.DeleteLocalRef (proxyClass); + } + } finally { + marshaler.DestroyArgumentState (other, ref otherState); + marshaler.DestroyArgumentState (value, ref state); + } + } + static ConcurrentDictionary GetProxyCache (TrimmableTypeMap instance) { var field = typeof (TrimmableTypeMap).GetField ("_proxyCache", BindingFlags.Instance | BindingFlags.NonPublic); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs index b2a9ecabeeb..f79553258c3 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs @@ -34,33 +34,6 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // net.dot.jni.test.CallVirtualFromConstructorDerived Java class not in APK "Java.InteropTests.InvokeVirtualFromConstructorTests", - // net.dot.jni.internal.JavaProxyObject. calls - // net.dot.jni.ManagedPeer.registerNativeMembers, which the trimmable - // typemap path rejects (Native methods must be registered by JCW - // static initializer blocks). Fixing this requires a parallel - // Android-trimmable variant of JavaProxyObject.java that registers - // its native equals/hashCode/toString via mono.android.Runtime.register - // — an architectural change tracked separately from the JavaCast / JavaAs - // work in this PR. See https://github.com/dotnet/android/issues/11170. - "Java.InteropTests.JavaObjectArray_object_ContractTest", - - // Same root cause as above (JavaProxyObject static init). - "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateArgumentState", - "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateGenericArgumentState", - "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateGenericObjectReferenceArgumentState", - "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateGenericValue", - "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateObjectReferenceArgumentState", - "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateValue", - "Java.InteropTests.JniValueMarshaler_object_ContractTests.SpecificTypesAreUsed", - - // net.dot.jni.test.GetThis static init — same JavaProxy* - // root cause as the JavaProxyObject exclusions above. - "Java.InteropTests.JavaObjectTest.DisposeAccessesThis", - - // net.dot.jni.internal.JavaProxyThrowable static init — same JavaProxy* - // root cause as the JavaProxyObject exclusions above. - "Java.InteropTests.JavaExceptionTests.InnerExceptionIsNotAProxy", - // JNI method remapping not supported in trimmable typemap "Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodName", "Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodWithStaticMethod", From 0644b70d2998595caee3c0bc8bd9372fbd5f03b9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 4 May 2026 09:53:04 +0200 Subject: [PATCH 2/3] Address trimmable runtime review comments Add comments documenting runtime jar selection and Java proxy identity semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets | 2 ++ src/java-runtime/java-runtime.targets | 1 + .../java-trimmable/net/dot/jni/internal/JavaProxyObject.java | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 8b010889974..f135fa2e967 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1391,6 +1391,8 @@ because xbuild doesn't support framework reference assemblies. + <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime_net6.jar diff --git a/src/java-runtime/java-runtime.targets b/src/java-runtime/java-runtime.targets index 664aab9cb48..2e9a27f223e 100644 --- a/src/java-runtime/java-runtime.targets +++ b/src/java-runtime/java-runtime.targets @@ -71,6 +71,7 @@ > + <_RuntimeSource Include="@(AllRuntimeSource)" /> diff --git a/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java b/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java index 7482cbed9c0..50eef50c63b 100644 --- a/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java +++ b/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java @@ -13,6 +13,8 @@ // This trimmable runtime copy cannot use Java.Interop's native object methods: // those are registered through ManagedPeer.registerNativeMembers, which is not // supported in the trimmable typemap path. + // Trimmable proxies use Java identity semantics: equals/hashCode/toString + // do not delegate to the wrapped .NET object. @Override public boolean equals(Object obj) { From 80d4af4bb0ac2a3a815fc2fb9605433bd64149bc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 5 May 2026 13:47:27 +0200 Subject: [PATCH 3/3] Address Java proxy review comments Use repository Java formatting for trimmable proxy sources and strengthen the proxy hashCode test so it compares against Java identityHashCode instead of itself. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../net/dot/jni/internal/JavaProxyObject.java | 8 ++-- .../dot/jni/internal/JavaProxyThrowable.java | 8 ++-- .../TrimmableTypeMapTypeManagerTests.cs | 45 ++++++++++++------- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java b/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java index 50eef50c63b..f18107b89e5 100644 --- a/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java +++ b/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyObject.java @@ -8,7 +8,7 @@ extends java.lang.Object implements GCUserPeerable { - ArrayList managedReferences = new ArrayList(); + ArrayList managedReferences = new ArrayList (); // This trimmable runtime copy cannot use Java.Interop's native object methods: // those are registered through ManagedPeer.registerNativeMembers, which is not @@ -16,19 +16,19 @@ // Trimmable proxies use Java identity semantics: equals/hashCode/toString // do not delegate to the wrapped .NET object. @Override - public boolean equals(Object obj) + public boolean equals (Object obj) { return this == obj; } @Override - public int hashCode() + public int hashCode () { return System.identityHashCode (this); } @Override - public String toString() + public String toString () { return super.toString (); } diff --git a/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyThrowable.java b/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyThrowable.java index f5379edf403..4b5bad5874d 100644 --- a/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyThrowable.java +++ b/src/java-runtime/java-trimmable/net/dot/jni/internal/JavaProxyThrowable.java @@ -8,12 +8,14 @@ extends java.lang.Error implements GCUserPeerable { - ArrayList managedReferences = new ArrayList(); + ArrayList managedReferences = new ArrayList (); - public JavaProxyThrowable () { + public JavaProxyThrowable () + { } - public JavaProxyThrowable (String message) { + public JavaProxyThrowable (String message) + { super (message); } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index e3378564320..e1f0888900c 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -218,23 +218,38 @@ public void JavaProxyObject_ObjectMethodsUseJavaIdentitySemantics () var otherState = marshaler.CreateObjectReferenceArgumentState (other); try { - IntPtr proxyClass = JNIEnv.GetObjectClass (state.ReferenceValue.Handle); + var localProxy = state.ReferenceValue.NewLocalRef (); + var localOtherProxy = otherState.ReferenceValue.NewLocalRef (); + try { - IntPtr equals = JNIEnv.GetMethodID (proxyClass, "equals", "(Ljava/lang/Object;)Z"); - IntPtr hashCode = JNIEnv.GetMethodID (proxyClass, "hashCode", "()I"); - IntPtr toString = JNIEnv.GetMethodID (proxyClass, "toString", "()Ljava/lang/String;"); - - Assert.IsTrue (JNIEnv.CallBooleanMethod (state.ReferenceValue.Handle, equals, new JValue (state.ReferenceValue.Handle))); - Assert.IsFalse (JNIEnv.CallBooleanMethod (state.ReferenceValue.Handle, equals, new JValue (otherState.ReferenceValue.Handle))); - Assert.AreEqual ( - JNIEnv.CallIntMethod (state.ReferenceValue.Handle, hashCode), - JNIEnv.CallIntMethod (state.ReferenceValue.Handle, hashCode)); - var proxyString = JNIEnv.GetString (JNIEnv.CallObjectMethod (state.ReferenceValue.Handle, toString), JniHandleOwnership.TransferLocalRef); - Assert.IsTrue ( - proxyString.StartsWith ("net.dot.jni.internal.JavaProxyObject@", StringComparison.Ordinal), - proxyString); + IntPtr proxyClass = JNIEnv.GetObjectClass (localProxy.Handle); + try { + IntPtr equals = JNIEnv.GetMethodID (proxyClass, "equals", "(Ljava/lang/Object;)Z"); + IntPtr hashCode = JNIEnv.GetMethodID (proxyClass, "hashCode", "()I"); + IntPtr toString = JNIEnv.GetMethodID (proxyClass, "toString", "()Ljava/lang/String;"); + var systemClass = JniEnvironment.Types.FindClass ("java/lang/System"); + + try { + IntPtr identityHashCode = JNIEnv.GetStaticMethodID (systemClass.Handle, "identityHashCode", "(Ljava/lang/Object;)I"); + + Assert.IsTrue (JNIEnv.CallBooleanMethod (localProxy.Handle, equals, new JValue (localProxy.Handle))); + Assert.IsFalse (JNIEnv.CallBooleanMethod (localProxy.Handle, equals, new JValue (localOtherProxy.Handle))); + Assert.AreEqual ( + JNIEnv.CallStaticIntMethod (systemClass.Handle, identityHashCode, new JValue (localProxy.Handle)), + JNIEnv.CallIntMethod (localProxy.Handle, hashCode)); + var proxyString = JNIEnv.GetString (JNIEnv.CallObjectMethod (localProxy.Handle, toString), JniHandleOwnership.TransferLocalRef); + Assert.IsTrue ( + proxyString.StartsWith ("net.dot.jni.internal.JavaProxyObject@", StringComparison.Ordinal), + proxyString); + } finally { + JniObjectReference.Dispose (ref systemClass); + } + } finally { + JNIEnv.DeleteLocalRef (proxyClass); + } } finally { - JNIEnv.DeleteLocalRef (proxyClass); + JniObjectReference.Dispose (ref localProxy); + JniObjectReference.Dispose (ref localOtherProxy); } } finally { marshaler.DestroyArgumentState (other, ref otherState);