Skip to content

Commit

Permalink
[Java.Interop] Add JniRuntime.JniValueManager.TryCreatePeer()
Browse files Browse the repository at this point in the history
Context: 78d5937
Context: dotnet/android@7a772f0
Context: dotnet/android#9716
Context: dotnet/android@694e975

dotnet/android@7a772f03 added the beginnings of a NativeAOT sample
to dotnet/android which built a ".NET for Android" app using
NativeAOT, which "rhymed with" the `Hello-NativeAOTFromAndroid`
sample in 78d5937.

Further work on the sample showed that it was lacking support for
`Android.App.Application` subclasses.  dotnet/android#9716 began
fixing that oversight, but in the process was triggering a stack
overflow because when it needed to create a "proxy" peer around the
`my.MainApplication` Java type, which subclassed
`android.app.Application`, instead of creating an instance of the
expected `MainApplication` C# type, it instead created an instance
of `Android.App.Application`.  This was visible from the logs:

	Created PeerReference=0x2d06/G IdentityHashCode=0x8edcb07 Instance=0x957d2a Instance.Type=Android.App.Application, Java.Type=my/MainApplication

Note that `Instance.Type` is `Android.App.Application`, not the
in-sample `MainApplication` C# type.

Because the runtime type was `Android.App.Application`, when we later
attempted to dispatch the `Application.OnCreate()` override, this
resulted in a *virtual* invocation of the Java `Application.onCreate()`
method instead of a *non-virtual* invocation of
`Application.onCreate()`.  This virtual invocation was the root of a
recursive loop which eventually resulted in a stack overflow.

The fix in dotnet/android@694e975e was to fix
`NativeAotTypeManager.CreatePeer()` so that it properly checked for a
binding of the *runtime type* of the Java instance *before* using the
"fallback" type provided to `Object.GetObject<T>()` in the
`Application.n_OnCreate()` method:

	partial class Application {
	    static void n_OnCreate (IntPtr jnienv, IntPtr native__this)
	    {
	        // …
	        var __this = global::Java.Lang.Object.GetObject<
		    Android.App.Application     // This is the "fallback" NativeAotTypeManager
		> (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
	        __this.OnCreate ();
	        // …
	    }
	}

All well and good.

The problem is that `NativeAotTypeManager` in dotnet/android needs to
support *both* dotnet/java-interop "activation constructors" with a
signature of `(ref JniObjectReference, JniObjectReferenceOptions)`,
*and* the .NET for Android signature of `(IntPtr, JniHandleOwnership)`.
Trying to support both constructors resulted in the need to copy
*all* of `JniRuntime.JniValueManager.CreatePeer()` *and dependencies*,
which felt a bit excessive.

Add a new `JniRuntime.JniValueManager.TryCreatePeer()` method, which
will invoke the activation constructor to create an `IJavaPeerable`:

	partial class JniRuntime {
	  partial class JniValueManager {
	    protected virtual IJavaPeerable? TryCreatePeer (
	        ref JniObjectReference reference,
	        JniObjectReferenceOptions options,
	        Type targetType);
	  }
	}

If the activation constructor is not found, then `TryCreatePeer()`
shall return `null`, allowing `CreatePeerInstance()` to try for
a base type or, ultimately, the fallback type.

This will allow a future dotnet/android PR to *remove*
`NativeAotTypeManager.CreatePeer()` and its dependencies entirely,
and instead do:

	partial class NativeAotTypeManager {
	    const   BindingFlags        ActivationConstructorBindingFlags   = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
	    static  readonly    Type[]  XAConstructorSignature  = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) };
	    protected override IJavaPeerable TryCreatePeer (ref JniObjectReference reference, JniObjectReferenceOptions options, Type type)
	    {
	        var c = type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null);
	        if (c != null) {
	            var args = new object[] {
	                reference.Handle,
	                JniHandleOwnership.DoNotTransfer,
	            };
	            var p       = (IJavaPeerable) c.Invoke (args);
	            JniObjectReference.Dispose (ref reference, options);
	            return p;
	        }
	        return base.TryCreatePeer (ref reference, options, type);
	    }
	}

vastly reducing the code it needs to care about.
  • Loading branch information
jonpryor committed Feb 1, 2025
1 parent e288589 commit ae9346a
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 30 deletions.
60 changes: 30 additions & 30 deletions src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -336,31 +336,21 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)

JniObjectReference.Dispose (ref targetClass);

var ctor = GetPeerConstructor (ref refClass, targetType);
if (ctor == null) {
var peer = CreatePeerInstance (ref refClass, targetType, ref reference, transfer);
if (peer == null) {
throw new NotSupportedException (string.Format ("Could not find an appropriate constructable wrapper type for Java type '{0}', targetType='{1}'.",
JniEnvironment.Types.GetJniTypeNameFromInstance (reference), targetType));
}

var acts = new object[] {
reference,
transfer,
};
try {
var peer = (IJavaPeerable) ctor.Invoke (acts);
peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable);
return peer;
} finally {
reference = (JniObjectReference) acts [0];
}
peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable);
return peer;
}

static readonly Type ByRefJniObjectReference = typeof (JniObjectReference).MakeByRefType ();

ConstructorInfo? GetPeerConstructor (
IJavaPeerable? CreatePeerInstance (
ref JniObjectReference klass,
[DynamicallyAccessedMembers (Constructors)]
Type fallbackType)
Type fallbackType,
ref JniObjectReference reference,
JniObjectReferenceOptions transfer)
{
var jniTypeName = JniEnvironment.Types.GetJniTypeNameFromClass (klass);

Expand All @@ -373,11 +363,12 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)
type = Runtime.TypeManager.GetType (sig);

if (type != null) {
var ctor = GetActivationConstructor (type);
type = GetInvokerType (type) ?? type;
var peer = TryCreatePeer (ref reference, transfer, type);

if (ctor != null) {
if (peer != null) {
JniObjectReference.Dispose (ref klass);
return ctor;
return peer;
}
}

Expand All @@ -391,20 +382,29 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)
}
JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose);

return GetActivationConstructor (fallbackType);
return TryCreatePeer (ref reference, transfer, fallbackType);
}

static ConstructorInfo? GetActivationConstructor (
const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
static readonly Type ByRefJniObjectReference = typeof (JniObjectReference).MakeByRefType ();
static readonly Type[] JIConstructorSignature = new Type [] { ByRefJniObjectReference, typeof (JniObjectReferenceOptions) };


protected virtual IJavaPeerable? TryCreatePeer (
ref JniObjectReference reference,
JniObjectReferenceOptions options,
[DynamicallyAccessedMembers (Constructors)]
Type type)
{
if (type.IsAbstract || type.IsInterface) {
type = GetInvokerType (type) ?? type;
}
foreach (var c in type.GetConstructors (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) {
var p = c.GetParameters ();
if (p.Length == 2 && p [0].ParameterType == ByRefJniObjectReference && p [1].ParameterType == typeof (JniObjectReferenceOptions))
return c;
var c = type.GetConstructor (ActivationConstructorBindingFlags, null, JIConstructorSignature, null);
if (c != null) {
var args = new object[] {
reference,
options,
};
var p = (IJavaPeerable) c.Invoke (args);
reference = (JniObjectReference) args [0];
return p;
}
return null;
}
Expand Down
1 change: 1 addition & 0 deletions src/Java.Interop/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ static Java.Interop.JniEnvironment.BeginMarshalMethod(nint jnienv, out Java.Inte
static Java.Interop.JniEnvironment.EndMarshalMethod(ref Java.Interop.JniTransition transition) -> void
virtual Java.Interop.JniRuntime.OnEnterMarshalMethod() -> void
virtual Java.Interop.JniRuntime.OnUserUnhandledException(ref Java.Interop.JniTransition transition, System.Exception! e) -> void
virtual Java.Interop.JniRuntime.JniValueManager.TryCreatePeer(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options, System.Type! type) -> Java.Interop.IJavaPeerable?
Java.Interop.JavaException.JavaException(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions transfer, Java.Interop.JniObjectReference throwableOverride) -> void
Java.Interop.JavaException.SetJavaStackTrace(Java.Interop.JniObjectReference peerReferenceOverride = default(Java.Interop.JniObjectReference)) -> void
Java.Interop.JniRuntime.JniValueManager.GetPeer(Java.Interop.JniObjectReference reference, System.Type? targetType = null) -> Java.Interop.IJavaPeerable?
Expand Down

0 comments on commit ae9346a

Please sign in to comment.