Skip to content

Commit d5dfa0a

Browse files
authored
[Java.Interop] Java.Lang.Object, Mono.Android Unification Changes (#1293)
Context: dotnet/android#9640 Context: 25de1f3 Context: 3043d89 dotnet/android#9640 completes "unification" with dotnet/java-interop, updating `Java.Lang.Object` to inherit `Java.Interop.JavaObject`, just like `src/Java.Base`. The biggest impediment with this unification step is that the semantics for referring to a Java instance are quite different: * .NET for Android née Xamarin.Android née Mono for Android uses an `(IntPtr, JniHandleOwnership)` pair to specify the JNI object reference pointer value and what should be done with it. The *type* of the JNI object reference is *inferred* by the `JniHandleOwnership` value, e.g. `JniHandleOwnership.TransferLocalRef` means that the `IntPtr` value is a JNI local reference. * dotnet/java-interop uses a `(ref JniObjectReference, JniObjectReferenceOptions)` pair to specify the JNI object reference and what can be done with it. The `JniObjectReference` value contains the type, but -- due to "pointer trickery"; see 25de1f3 -- it might be *so* invalid that it cannot be touched at all, and `JniObjectReferenceOptions` will let us know. Which brings us to the conundrum: how do we implement the `Java.Lang.Throwable(IntPtr, JniHandleOwnership)` constructor? namespace Java.Lang { public class Throwable { public Throwable(IntPtr handle, JniHandleOwnership transfer); } } The "obvious and wrong" solution would be to use `ref` with a temporary… public Throwable(IntPtr handle, JniHandleOwnership transfer) : base (ref new JniObjectReference(handle), …) { } …but that won't compile. Additionally, we want to be able to provide `message` and `innerException` parameters to `System.Exception`, but the `JavaException(ref JniObjectReference, JniObjectReferenceOptions)` constructor requires a valid JNI Object Reference to do that. If `Java.Lang.Throwable` tries to "work around" this by using the `JavaException(string, Exception)` constructor: public Throwable(IntPtr handle, JniHandleOwnership transfer) : base (_GetMessage (handle), _GetInnerException (handle)) { … } then the `JavaException(string, Exception)` constructor will try to invoke the `E(String)` constructor on the Java side, which: 1. Is semantically wrong, because we may already have a Java instance in `handle`, so why are we creating a new instance? But- 2. This fails for constructor subclasses which don't provide a `E(String)` constructor! Specifically, the [`JnienvTest.ActivatedDirectThrowableSubclassesShouldBeRegistered`][0] unit test starts crashing for "bizarre" reasons. Fix these issues by adding the new `JavaException` constructor: namespace Java.Interop { partial class JavaException { protected JavaException ( ref JniObjectReference reference, JniObjectReferenceOptions transfer, JniObjectReference throwableOverride); } } The new `throwableOverride` parameter is used to obtain the `message` and `innerException` values to provide to the `System.Exception(string, Exception)` constructor, allowing `reference` to be a "do not touch" value. Additionally, add a `protected SetJavaStackTrace()` method which is used to set the `JavaException.JavaStackTrace` property based on a `JniObjectReference` value. Update `Java.Interop.JavaObject` to be `[Serializable]`, and to mark its fields as *`[NonSerialized]`*. This is needed to fix the `InstallAndRunTests.JsonDeserializationCreatesJavaHandle()` tests: E AndroidRuntime: android.runtime.JavaProxyThrowable: [System.Runtime.Serialization.InvalidDataContractException]: AttributedTypesCannotInheritFromNonAttributedSerializableTypes, Java.Lang.Object, Java.Interop.JavaObject Finally, update `Java.Interop.JniRuntime.JniValueManager.ConstructPeer()` to *not* attempt to call `AddPeer()`/etc. when `peer.JniManagedPeerState` has `JniManagedPeerStates.Activatable` set. Failure to do so was causing `BindingTests.JavaSideActivation()` to fail, as it was "losing" a GREF. `JavaSideActivation()` is the scenario described in 3043d89, in which: 1. The static Java method `CallMethodFromCtor.NewInstance()` creates an instance of `ConstructorTest`, from Java. 2. The default constructor in the Java Callable Wrapper (JCW) for `ConstructorTest` chains to the Java `CallMethodFromCtor()` default constructor. 3. The Java `CallMethodFromCtor()` default constructor invokes the method `calledFromCtor()`, which is overridden in the C# `ConstructorTest.CalledFromCtor()` method. 4. This causes the `CallMethodFromCtor.n_CalledFromCtor()` marshal method to be invoked, which calls `Java.Lang.Object.GetObject<CallMethodFromCtor>()`, which is akin to `runtime.ValueManager.GetValue<CallMethodFromCtor>(…)`. 5. `Object.GetObject<CallMethodFromCtor>()` doesn't find an already existing object mapping for the instance we're still trying to construct, so it creates a "proxy" object via [`TypeManager.CreateInstance()`][2], which winds up invoking the `ConstructorTest(IntPtr, JniHandleOwnership)` "activation" constructor to create the "proxy". The activation constructor eventually invokes `JavaObject.Construct()` / `JniRuntime.JniValueManager.ConstructPeer()`. This is the first "important" GREF created. Once the proxy is created, it's state is set to `JniManagedPeerStates.Activatable`. 6. The `ConstructorTest.CalledFromCtor()` C# override is invoked, control returns to the Java `CallMethodFromCtor` constructor, which completes, allowing the JCW `ConstructorTest` constructor to execute, which calls [`TypeManager.n_Activate(…)`][3], which is equivalent to `ManagedPeer.Construct()`. 7. `TypeManager.n_Activate(…)` sees that the instance "needs activation", and invokes the C# `ConstructorTest()` default constructor. This eventually hits (again) `JniRuntime.JniValueManager.ConstructPeer()`. The problem was that on the second `JniRuntime.JniValueManager.ConstructPeer()` invocation in (7), another GREF was created, replacing the GREF created in (5), but without *disposing* of the GREF created in (5). This caused a GREF leak, which the `JavaSideActivation()` test was looking for. Updating `JniRuntime.JniValueManager.ConstructPeer()` to bail when it encounters a Peer with `JniManagedPeerStates.Activatable` avoids the GREF creation in (7), thus avoiding the GREF leak. [0]: https://github.com/dotnet/android/blob/f9f421b109b2a0f8f805ed14ab1c8dbe651f563e/tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs#L285-L305 [1]: https://github.com/dotnet/android/blob/main/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/BindingTests.cs#L97-L115 [2]: https://github.com/dotnet/android/blob/f9f421b109b2a0f8f805ed14ab1c8dbe651f563e/src/Mono.Android/Java.Interop/TypeManager.cs#L240-L332 [3]: https://github.com/dotnet/android/blob/f9f421b109b2a0f8f805ed14ab1c8dbe651f563e/src/Mono.Android/Java.Interop/TypeManager.cs#L132-L167
1 parent c86ae26 commit d5dfa0a

File tree

4 files changed

+60
-15
lines changed

4 files changed

+60
-15
lines changed

src/Java.Interop/Java.Interop/JavaException.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public unsafe JavaException ()
3838
var peer = JniPeerMembers.InstanceMethods.StartCreateInstance ("()V", GetType (), null);
3939
Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose);
4040
JniPeerMembers.InstanceMethods.FinishCreateInstance ("()V", this, null);
41-
JavaStackTrace = GetJavaStack (PeerReference);
41+
SetJavaStackTrace ();
4242
}
4343

4444
public unsafe JavaException (string message)
@@ -55,7 +55,7 @@ public unsafe JavaException (string message)
5555
} finally {
5656
JniObjectReference.Dispose (ref native_message, JniObjectReferenceOptions.CopyAndDispose);
5757
}
58-
JavaStackTrace = GetJavaStack (PeerReference);
58+
SetJavaStackTrace ();
5959
}
6060

6161
public unsafe JavaException (string message, Exception innerException)
@@ -72,15 +72,22 @@ public unsafe JavaException (string message, Exception innerException)
7272
} finally {
7373
JniObjectReference.Dispose (ref native_message, JniObjectReferenceOptions.CopyAndDispose);
7474
}
75-
JavaStackTrace = GetJavaStack (PeerReference);
75+
SetJavaStackTrace ();
76+
}
77+
78+
protected JavaException (ref JniObjectReference reference, JniObjectReferenceOptions transfer, JniObjectReference throwableOverride)
79+
: base (GetMessage (throwableOverride), GetCause (throwableOverride))
80+
{
81+
Construct (ref reference, transfer);
82+
SetJavaStackTrace (throwableOverride);
7683
}
7784

7885
public JavaException (ref JniObjectReference reference, JniObjectReferenceOptions transfer)
7986
: base (GetMessage (ref reference, transfer), GetCause (ref reference, transfer))
8087
{
8188
Construct (ref reference, transfer);
8289
if (PeerReference.IsValid)
83-
JavaStackTrace = GetJavaStack (PeerReference);
90+
SetJavaStackTrace ();
8491
}
8592

8693
protected void Construct (ref JniObjectReference reference, JniObjectReferenceOptions options)
@@ -183,6 +190,13 @@ public override unsafe int GetHashCode ()
183190
{
184191
if (transfer == JniObjectReferenceOptions.None)
185192
return null;
193+
return GetMessage (reference);
194+
}
195+
196+
static string? GetMessage (JniObjectReference reference)
197+
{
198+
if (!reference.IsValid)
199+
return null;
186200

187201
var m = _members.InstanceMethods.GetMethodInfo ("getMessage.()Ljava/lang/String;");
188202
var s = JniEnvironment.InstanceMethods.CallObjectMethod (reference, m);
@@ -193,12 +207,30 @@ public override unsafe int GetHashCode ()
193207
{
194208
if (transfer == JniObjectReferenceOptions.None)
195209
return null;
210+
return GetCause (reference);
211+
}
212+
213+
static Exception? GetCause (JniObjectReference reference)
214+
{
215+
if (!reference.IsValid)
216+
return null;
196217

197218
var m = _members.InstanceMethods.GetMethodInfo ("getCause.()Ljava/lang/Throwable;");
198219
var e = JniEnvironment.InstanceMethods.CallObjectMethod (reference, m);
199220
return JniEnvironment.Runtime.GetExceptionForThrowable (ref e, JniObjectReferenceOptions.CopyAndDispose);
200221
}
201222

223+
protected void SetJavaStackTrace (JniObjectReference peerReferenceOverride = default)
224+
{
225+
if (!peerReferenceOverride.IsValid) {
226+
peerReferenceOverride = PeerReference;
227+
}
228+
if (!peerReferenceOverride.IsValid) {
229+
return;
230+
}
231+
JavaStackTrace = GetJavaStack (peerReferenceOverride);
232+
}
233+
202234
unsafe string? GetJavaStack (JniObjectReference handle)
203235
{
204236
using (var StringWriter_class = new JniType ("java/io/StringWriter"))

src/Java.Interop/Java.Interop/JavaObject.cs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,35 @@
22

33
using System;
44
using System.Diagnostics.CodeAnalysis;
5+
using System.Runtime.Serialization;
56

67
namespace Java.Interop
78
{
89
[JniTypeSignature ("java/lang/Object", GenerateJavaPeer=false)]
10+
[Serializable]
911
unsafe public class JavaObject : IJavaPeerable
1012
{
1113
internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;
1214

1315
readonly static JniPeerMembers _members = new JniPeerMembers ("java/lang/Object", typeof (JavaObject));
1416

15-
public int JniIdentityHashCode { get; private set; }
16-
public JniManagedPeerStates JniManagedPeerState { get; private set; }
17+
[NonSerialized] int identityHashCode;
18+
[NonSerialized] JniManagedPeerStates managedPeerState;
19+
20+
public int JniIdentityHashCode => identityHashCode;
21+
22+
public JniManagedPeerStates JniManagedPeerState => managedPeerState;
1723

1824
#if FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES
19-
JniObjectReference reference;
25+
[NonSerialized] JniObjectReference reference;
2026
#endif // FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES
2127
#if FEATURE_JNIOBJECTREFERENCE_INTPTRS
22-
IntPtr handle;
23-
JniObjectReferenceType handle_type;
28+
[NonSerialized] IntPtr handle;
29+
[NonSerialized] JniObjectReferenceType handle_type;
2430
#pragma warning disable 0169
2531
// Used by JavaInteropGCBridge
26-
IntPtr weak_handle;
27-
int refs_added;
32+
[NonSerialized] IntPtr weak_handle;
33+
[NonSerialized] int refs_added;
2834
#pragma warning restore 0169
2935
#endif // FEATURE_JNIOBJECTREFERENCE_INTPTRS
3036

@@ -151,12 +157,12 @@ void IJavaPeerable.Finalized ()
151157

152158
void IJavaPeerable.SetJniIdentityHashCode (int value)
153159
{
154-
JniIdentityHashCode = value;
160+
identityHashCode = value;
155161
}
156162

157163
void IJavaPeerable.SetJniManagedPeerState (JniManagedPeerStates value)
158164
{
159-
JniManagedPeerState = value;
165+
managedPeerState = value;
160166
}
161167

162168
void IJavaPeerable.SetPeerReference (JniObjectReference reference)

src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,13 @@ public void ConstructPeer (IJavaPeerable peer, ref JniObjectReference reference,
9797

9898
var newRef = peer.PeerReference;
9999
if (newRef.IsValid) {
100-
// Activation! See ManagedPeer.RunConstructor
101-
peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Activatable);
100+
JniObjectReference.Dispose (ref reference, options);
101+
102+
// Activation? See ManagedPeer.Construct, CreatePeer
103+
// Instance was already added, don't add again
104+
if (peer.JniManagedPeerState.HasFlag (JniManagedPeerStates.Activatable)) {
105+
return;
106+
}
102107
JniObjectReference.Dispose (ref reference, options);
103108
newRef = newRef.NewGlobalRef ();
104109
} else if (options == JniObjectReferenceOptions.None) {

src/Java.Interop/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,7 @@ static Java.Interop.JniEnvironment.BeginMarshalMethod(nint jnienv, out Java.Inte
33
static Java.Interop.JniEnvironment.EndMarshalMethod(ref Java.Interop.JniTransition transition) -> void
44
virtual Java.Interop.JniRuntime.OnEnterMarshalMethod() -> void
55
virtual Java.Interop.JniRuntime.OnUserUnhandledException(ref Java.Interop.JniTransition transition, System.Exception! e) -> void
6+
Java.Interop.JavaException.JavaException(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions transfer, Java.Interop.JniObjectReference throwableOverride) -> void
7+
Java.Interop.JavaException.SetJavaStackTrace(Java.Interop.JniObjectReference peerReferenceOverride = default(Java.Interop.JniObjectReference)) -> void
68
Java.Interop.JniTypeSignatureAttribute.InvokerType.get -> System.Type?
79
Java.Interop.JniTypeSignatureAttribute.InvokerType.set -> void

0 commit comments

Comments
 (0)