Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[nativeaot] support for Application subclasses #9716

Merged
merged 16 commits into from
Feb 3, 2025

Conversation

jonathanpeppers
Copy link
Member

@jonathanpeppers jonathanpeppers commented Jan 28, 2025

This a step toward testing a .NET MAUI app, as it uses a custom Android.App.Application subclass.

jonpryor added a commit to dotnet/java-interop that referenced this pull request Jan 29, 2025
Context: https://github.com/dotnet/android/pull/9630/files#r1891090085
Context: dotnet/android#9716

As part of NativeAOT prototyping within dotnet/android, we need to
update `Java.Lang.Object.GetObject<T>()` so that it uses
`Java.Interop.JniRuntime.JniValueManager` APIs instead of its own
`TypeManager.CreateInstance()` invocation, as
`TypeManager.CreateInstance()` hits P/Invokes which don't currently
work in the NativeAOT sample environment.

However, updated it to *what*?  The obvious thing to do would be to
use `JniRuntime.JniValueManager.GetValue()`:

	partial class Object {
	    internal static IJavaPeerable? GetObject (IntPtr handle, JniHandleOwnershipt ransfer, Type? type = null)
	    {
	        var r = PeekObject (handle, type);
	        if (r != null) {
	            JNIEnv.DeleteRef (handle, transfer);
	            return r;
	        }
	        var reference = new JniObjectReference (handle);
	        r = (IJavaPeerable) JNIEnvInit.ValueManager.GetValue (
	                ref reference,
	                JniObjectReferenceOptions.Copy,
	                type);
	        JNIEnv.DeleteRef (handle, transfer);
	        return r;
	    }
	}

The problem is that this blows up good:

	<System.InvalidCastException: Arg_InvalidCastException
	   at Java.Lang.Object.GetObject(IntPtr , JniHandleOwnership , Type )
	   at Android.Runtime.JNIEnv.<CreateNativeArrayElementToManaged>g__GetObject|74_11(IntPtr , Type )
	   at Android.Runtime.JNIEnv.<>c.<CreateNativeArrayElementToManaged>b__74_9(Type type, IntPtr source, Int32 index)
	   at Android.Runtime.JNIEnv.GetObjectArray(IntPtr , Type[] )
	   at Java.InteropTests.JnienvTest.<>c__DisplayClass26_0.<MoarThreadingTests>b__1()>

because somewhere in that stack trace we have a `java.lang.Integer`
instance, and `.GetValue(Integer_ref, …)` returns a `System.Int32`
containing the underling value, *not* an `IJavaPeerable` value for
the `java.lang.Integer` instance.  Consider:

	var i_class = new JniType ("java/lang/Integer");
	var i_ctor  = i_class.GetConstructor ("(I)V");
	JniArgumentValue* i_args = stackalloc JniArgumentValue [1];
	i_args [0]  = new JniArgumentValue (42);
	var i_value = i_class.NewObject (i_ctor, i_args);
	var v       = JniEnvironment.Runtime.ValueManager.GetValue (ref i_value, JniObjectReferenceOptions.CopyAndDispose, null);
	Console.WriteLine ($"v? {v} {v?.GetType ()}");

which prints `v? 42 System.Int32`.

This was expected and desirable, until we try to use `GetValue()` for
`Object.GetObject<T>()`; the semantics don't match.

Add a new `JniRuntime.JniValueManager.GetPeer()` method, which better
matches the semantics that `Object.GetObject<T>()` requires, allowing:

	partial class Object {
	    internal static IJavaPeerable? GetObject (IntPtr handle, JniHandleOwnershipt ransfer, Type? type = null)
	    {
	        var r = JNIEnvInit.ValueManager.GetPeer (new JniObjectReference (handle));
	        JNIEnv.DeleteRef (handle, transfer);
	        return r;
	    }
	}

Finally, add a new `JniRuntimeJniValueManagerContract` unit test,
so that we have "more formalized" semantic requirements on
`JniRuntime.JniValueManager` implementations.
jonpryor added a commit to dotnet/java-interop that referenced this pull request Jan 30, 2025
Context: https://github.com/dotnet/android/pull/9630/files#r1891090085
Context: dotnet/android#9716

As part of NativeAOT prototyping within dotnet/android, we need to
update [`Java.Lang.Object.GetObject<T>()`][0] so that it uses
`Java.Interop.JniRuntime.JniValueManager` APIs instead of its own
`TypeManager.CreateInstance()` invocation, as
`TypeManager.CreateInstance()` hits P/Invokes which don't currently
work in the NativeAOT sample environment.

However, update it to *what*?  The obvious thing to do would be to
use `JniRuntime.JniValueManager.GetValue()`:

	partial class Object {
	    internal static IJavaPeerable? GetObject (IntPtr handle, JniHandleOwnership transfer, Type? type = null)
	    {
	        var r = PeekObject (handle, type);
	        if (r != null) {
	            JNIEnv.DeleteRef (handle, transfer);
	            return r;
	        }
	        var reference = new JniObjectReference (handle);
	        r = (IJavaPeerable) JNIEnvInit.ValueManager.GetValue (
	                ref reference,
	                JniObjectReferenceOptions.Copy,
	                type);
	        JNIEnv.DeleteRef (handle, transfer);
	        return r;
	    }
	}

The problem is that this blows up good:

	<System.InvalidCastException: Arg_InvalidCastException
	   at Java.Lang.Object.GetObject(IntPtr , JniHandleOwnership , Type )
	   at Android.Runtime.JNIEnv.<CreateNativeArrayElementToManaged>g__GetObject|74_11(IntPtr , Type )
	   at Android.Runtime.JNIEnv.<>c.<CreateNativeArrayElementToManaged>b__74_9(Type type, IntPtr source, Int32 index)
	   at Android.Runtime.JNIEnv.GetObjectArray(IntPtr , Type[] )
	   at Java.InteropTests.JnienvTest.<>c__DisplayClass26_0.<MoarThreadingTests>b__1()>

because somewhere in that stack trace we have a `java.lang.Integer`
instance, and `.GetValue(Integer_ref, …)` returns a `System.Int32`
containing the underling value, *not* an `IJavaPeerable` value for
the `java.lang.Integer` instance.  Consider:

	var i_class = new JniType ("java/lang/Integer");
	var i_ctor  = i_class.GetConstructor ("(I)V");
	JniArgumentValue* i_args = stackalloc JniArgumentValue [1];
	i_args [0]  = new JniArgumentValue (42);
	var i_value = i_class.NewObject (i_ctor, i_args);
	var v       = JniEnvironment.Runtime.ValueManager.GetValue (ref i_value, JniObjectReferenceOptions.CopyAndDispose, null);
	Console.WriteLine ($"v? {v} {v?.GetType ()}");

which prints `v? 42 System.Int32`.

This was expected and desirable, until we try to use `GetValue()` for
`Object.GetObject<T>()`; the semantics don't match.

Add a new `JniRuntime.JniValueManager.GetPeer()` method, which better
matches the semantics that `Object.GetObject<T>()` requires, allowing:

	partial class Object {
	    internal static IJavaPeerable? GetObject (IntPtr handle, JniHandleOwnership transfer, Type? type = null)
	    {
	        var r = JNIEnvInit.ValueManager.GetPeer (new JniObjectReference (handle));
	        JNIEnv.DeleteRef (handle, transfer);
	        return r;
	    }
	}

Finally, add a new `JniRuntimeJniValueManagerContract` unit test,
so that we have "more formalized" semantic requirements on
`JniRuntime.JniValueManager` implementations.

[0]: https://github.com/dotnet/android/blob/cc35a263e046444c2123e7a7dba106b18e2bbebb/src/Mono.Android/Java.Lang/Object.cs#L133-L166
Should fix the unexpected P/Invoke which is causing the NativeAOT
sample to crash:

	net.dot.jni.internal.JavaProxyThrowable: System.DllNotFoundException: DllNotFound_Linux, xa-internal-api,
	dlopen failed: library "xa-internal-api.so" not found
	dlopen failed: library "libxa-internal-api.so" not found
	dlopen failed: library "xa-internal-api" not found
	dlopen failed: library "libxa-internal-api" not found
	01-30 02:19:25.570  3348  3348 E AndroidRuntime:
	   at System.Runtime.InteropServices.NativeLibrary.LoadLibErrorTracker.Throw(String) + 0x47
	   at Internal.Runtime.CompilerHelpers.InteropHelpers.FixupModuleCell(InteropHelpers.ModuleFixupCell*) + 0xe2
	   at Internal.Runtime.CompilerHelpers.InteropHelpers.ResolvePInvokeSlow(InteropHelpers.MethodFixupCell*) + 0x35
	   at Android.Runtime.RuntimeNativeMethods.monodroid_TypeManager_get_java_class_name(IntPtr) + 0x22
	   at Java.Interop.TypeManager.GetClassName(IntPtr) + 0xe
	   at Java.Interop.TypeManager.CreateInstance(IntPtr, JniHandleOwnership, Type) + 0x69
	   at Java.Lang.Object._GetObject[T](IntPtr, JniHandleOwnership) + 0x4e
	   at Android.App.Application.n_OnCreate(IntPtr jnienv, IntPtr native__this) + 0x89
		at my.MainApplication.n_onCreate(Native Method)
		at my.MainApplication.onCreate(MainApplication.java:24)
		at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1182)
		at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6460)
		at android.app.ActivityThread.access$1300(ActivityThread.java:219)
		at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1859)
		at android.os.Handler.dispatchMessage(Handler.java:107)
		at android.os.Looper.loop(Looper.java:214)
		at android.app.ActivityThread.main(ActivityThread.java:7356)
		at java.lang.reflect.Method.invoke(Native Method)
		at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
		at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
This reverts commit f3c122e.

This doens't build, because d3cde47 turns all warnings into errors,
and this commit introduces a warning.

PR #9728 is the proper fix.
jonpryor pushed a commit that referenced this pull request Jan 30, 2025
Context: #9630
Context: #9716
Context: dotnet/java-interop@e288589

@jonathanpeppers attempted to prototype MAUI startup in a NativeAOT
environment, which promptly crashes similar to:

	E AndroidRuntime: net.dot.jni.internal.JavaProxyThrowable: System.DllNotFoundException: DllNotFound_Linux, xa-internal-api, 
	E AndroidRuntime: dlopen failed: library "xa-internal-api" not found
	E AndroidRuntime: dlopen failed: library "libxa-internal-api" not found
	E AndroidRuntime: 
	E AndroidRuntime:    at System.Runtime.InteropServices.NativeLibrary.LoadLibErrorTracker.Throw(String) + 0x47
	E AndroidRuntime:    at Internal.Runtime.CompilerHelpers.InteropHelpers.FixupModuleCell(InteropHelpers.ModuleFixupCell*) + 0xe2
	E AndroidRuntime:    at Internal.Runtime.CompilerHelpers.InteropHelpers.ResolvePInvokeSlow(InteropHelpers.MethodFixupCell*) + 0x35
	E AndroidRuntime:    at Android.Runtime.RuntimeNativeMethods.monodroid_TypeManager_get_java_class_name(IntPtr) + 0x22
	E AndroidRuntime:    at Java.Interop.TypeManager.GetClassName(IntPtr) + 0xe
	E AndroidRuntime:    at Java.Interop.TypeManager.CreateInstance(IntPtr, JniHandleOwnership, Type) + 0x69
	E AndroidRuntime:    at Java.Lang.Object._GetObject[T](IntPtr, JniHandleOwnership) + 0x4e
	E AndroidRuntime:    at Android.App.Application.n_OnCreate(IntPtr jnienv, IntPtr native__this) + 0x89
	E AndroidRuntime: 	at my.MainApplication.n_onCreate(Native Method)
	E AndroidRuntime: 	at my.MainApplication.onCreate(MainApplication.java:24)
	E AndroidRuntime: 	at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1182)
	E AndroidRuntime: 	at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6460)
	E AndroidRuntime: 	at android.app.ActivityThread.access$1300(ActivityThread.java:219)
	E AndroidRuntime: 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1859)
	E AndroidRuntime: 	at android.os.Handler.dispatchMessage(Handler.java:107)
	E AndroidRuntime: 	at android.os.Looper.loop(Looper.java:214)
	E AndroidRuntime: 	at android.app.ActivityThread.main(ActivityThread.java:7356)
	E AndroidRuntime: 	at java.lang.reflect.Method.invoke(Native Method)
	E AndroidRuntime: 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
	E AndroidRuntime: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

because:

 1. MAUI apps provide an `Android.App.Application` sublass, and

 2. [`Application` is "special"][0], and

 3. When `Java.Lang.Object.GetObject<T>()` is hit for an instance
    which doesn't already have an instance mapping -- i.e.
    `Object.PeekObject()` returns `null` -- then we'd hit
    `TypeManager.CreateInstance()`, which is a codepath full of
    P/Invokes, and P/Invokes don't currently work on NativeAOT [^1].

The solution is to partially resuscitate PR #9630, but this time have
`Java.Lang.Object.GetObject<T>()` call
`Java.Interop.JniRuntime.JniValueManager.GetPeer()` instead of
`Java.Interop.JniRuntime.JniValueManager.GetValue()`; see also
dotnet/java-interop@e288589d.

This allows `Application` subclasses to be used (along with other
build system changes present in #9716).

This also cleans up a few things if `Java.Lang.Object` introduces an
internal `DynamicallyAccessedMemberTypes Constructors` field.

[0]: https://learn.microsoft.com/en-us/previous-versions/xamarin/android/internals/architecture#java-activation

[^1]: It's *not* that NativeAOT doesn't support P/Invokes; it does.
      The problem is that for a P/Invoke to work, we need to know the
      name of the native library we're P/Invoking into, and our
      NativeAOT sample environment doesn't include any `.so` files
      other than the one for the app, i.e. whatever we'd P/Invoke
      into doesn't exist.
jonpryor and others added 2 commits January 30, 2025 14:40

public NativeAotValueManager(NativeAotTypeManager typeManager) =>
Copy link
Member

Choose a reason for hiding this comment

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

If this doesn't need to know about NativeAotTypeManager, then this can instead provide a default constructor and override OnSetRuntime() to cache the TypeManager instance:

public override void OnSetRuntime (JniRuntime runtime)
{
    base.OnSetRuntime (runtime);
    TypeManager = runtime.TypeManager;
}

The "benefit" is that you don't need to change the the init code in JavaInteropRuntime.init(). (That might be a silly reason…)

Copy link
Member Author

Choose a reason for hiding this comment

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

It doesn't look like this works because of ordering:

runtime.TypeManager is null at this point.

I changed the field to JniRuntime.JniTypeManager, though.

jonathanpeppers and others added 2 commits January 31, 2025 10:26
Context: https://discord.com/channels/732297728826277939/732297837953679412/1334614545871929345

PR #9716 was crashing with a stack overflow:

	I NativeAotFromAndroid:    at Java.Interop.JniEnvironment.InstanceMethods.CallVoidMethod(JniObjectReference, JniMethodInfo, JniArgumentValue*) + 0xa8
	I NativeAotFromAndroid:    at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualVoidMethod(String, IJavaPeerable, JniArgumentValue*) + 0x184
	I NativeAotFromAndroid:    at Android.App.Application.n_OnCreate(IntPtr jnienv, IntPtr native__this) + 0xa8
	I NativeAotFromAndroid:    at libNativeAOT!<BaseAddress>+0x4f3e44

The cause was the topmost frame: `CallVoidMethod()`, which performs a
*virtual* method invocation.  The stack overflow was that
Java `MainApplication.onCreate()` called C# `Application.n_OnCreate()`,
which called `InvokeVirtualVoidMethod()`, which did a *virtual*
invocation back on `MainApplication.onCreate()`, …

`InvokeVirtualVoidMethod()` should have been calling
`CallNonvirtualVoidMethod()`; why wasn't it?

Further investigation showed:

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

which at a glance seems correct, but isn't: the `Instance.Type` for
a `Java.Type` of `my/MainApplication` should be `MainApplication`,
*not* `Android.App.Application`!

Because the runtime type of this value was `Application`,
it was warranted and expected that `InvokeVirtualVoidMethod()`
would do a virtual invocation!

So, why did the avove `Created PeerReference …` line show the wrong
type?  Because `NativeAotTypeManager.CreatePeer()` needs to check
for bindings of the the runtime type of the Java handle *before*
using the `targetType` parameter, because the targetType parameter
will *never* be for that of a custom subclass.

Copy *lots* of code from dotnet/java-interop -- showing that this
needs some major cleanup & refactoring -- so that we properly check
the runtime type of `reference` + base classes when trying to determine
the type of the proxy to create.

This fixes the stack overflow.
@jonathanpeppers jonathanpeppers marked this pull request as ready for review January 31, 2025 20:13
@jonathanpeppers
Copy link
Member Author

The relevant tests here passed:

image

NativeAOTSample runs the app on an emulator.

jonpryor added a commit to dotnet/java-interop that referenced this pull request Feb 1, 2025
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.
jonpryor added a commit to dotnet/java-interop that referenced this pull request Feb 3, 2025
Context: 78d5937
Context: be6cc8f
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:

	[Application (Name = "my.MainApplication")]
	public class MainApplication : Application
	{
	    public MainApplication (IntPtr handle, JniHandleOwnership transfer)
	        : base (handle, transfer)
	    {
	    }

	    public override void OnCreate ()
	    {
	        base.OnCreate ();
	    }
	}


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 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.

Additionally, add `JniRuntime.JniTypeManager.GetInvokerType()`:

	partial class JniRuntime {
	  partial class JniTypeManager {
	    public Type? GetInvokerType (Type type);
	    protected virtual Type? GetInvokerTypeCore (Type type);
	  }
	}

`GetInvokerType()` calls `GetInvokerTypeCore()`, allowing flexibility
for subclasses to lookup the "string-based" the .NET for Android -style
Invoker types that JavaInterop1-style bindings used before commit
be6cc8f.
@jonpryor
Copy link
Member

jonpryor commented Feb 3, 2025

Draft commit message:

[NativeAOT] Add support for `Application` subclasses

Context: https://github.com/xamarin/monodroid/commit/1a931e276ad38c28b944b83157573a55044c1be3

`android.app.Application` and `android.app.Instrumentation` are
["special"][0], in terms of app startup and type registration,
because MonoVM is initialized via a `ContentProvider`, which is
constructed *after* `Application` instance is constructed.

In broad terms:

 1. `Application` instance is created.
    (Specific type is via [`//application/@android:name`][1].)

 2. `ContentProvider`s are created, including
    `MonoRuntimeProvider` (MonoVM) or
    `NativeAotRuntimeProvider` (NativeAOT).

 3. `*RuntimeProvider.attachInfo()` invoked, provided (1).

 4. `*RuntimeProvider.attachInfo()` does whatever runtime init is
    required, e.g. MonoVM's `MonoRuntimeProvider` calls
    `MonoPackageManager.LoadApplication()`.

We cannot dispatch Java `native` methods into managed code until
*after* (4) finishes.  Meanwhile, we allow C# to subclass
`Android.App.Application` and override methods like
`Application.OnCreate()`:

	[Application (Name = "my.MainApplication")]
	public partial class MainApplication : Application
	{
	    public override void OnCreate ()
	    {
	        base.OnCreate ();
	    }
	}

How does that work?

It works via a leaky abstraction: unlike other types, the
Java Callable Wrapper (JCW) for `Application` and `Instrumentation`
subclasses lacks:

 1. A `Runtime.register()` invocation in the static constructor, and
 2. A call to `TypeManager.Activate()` in the instance constructor.

Compare an `Activity` JCW:

	public /* partial */ class MainActivity extends android.app.Activity {
	  static {
	    __md_methods = "…";
	    mono.android.Runtime.register ("….MainActivity, …", MainActivity.class, __md_methods);
	  }
	
	  public MainActivity ()
	  {
	    super ();
	    if (getClass () == MainActivity.class) {
	      mono.android.TypeManager.Activate ("….MainActivity, …", "", this, new java.lang.Object[] {  });
	    }
	  }
	}

to an `Application` JCW:

	public /* partial */ class MainApplication extends android.app.Application {
	{
	  static {
	    // mostly empty
	  }}

	  public MainApplication ()
	  {
	    // Used to provide `Android.App.Application.Context` property
	    mono.MonoPackageManager.setContext (this);

	    // No `TypeManager.Activate(…)` call
	  }
	}

Instead of a `Runtime.register()` invocation in the e.g.
`MainApplication` static constructor, `MainApplication` is instead
registered via `ApplicationRegistration.registerApplications()`, which is:

 1. Generated via the `<GenerateJavaStubs/>` task, and
 2. Invoked from `MonoPackageManager.LoadApplication()`.

"Later", when the Java `Application.onCreate()` method is invoked,
the `Application.n_OnCreate()` marshal method will call
`Java.Lang.Object.GetObject<Application>(…)`.  This will look for
the `(IntPtr, JniHandleOwnership)` "activation constructor", which
must be present on the C# type:

	public partial class MainApplication : Application
	{
	    public MainApplication (IntPtr handle, JniHandleOwnership transfer)
	        : base (handle, transfer)
	    {
	    }
	}

To support `Application` subclasses under NativeAOT, we need to
ensure that the `<GenerateJavaStubs/>` task creates
`ApplicationRegistration.java`, and update
`NativeAotRuntimeProvider` to invoke
`ApplicationRegistration.registerApplications()`.  We also need
various changes to `NativeAotValueManager` so that we can create the
`MainApplication` "proxy" instance via the activation constructor.

Other changes:

  * Change the package name for `ApplicationRegistration` from
    `mono.android` to `net.dot.android`.

  * Stop using `AndroidValueManager` and instead copy
    [`ManagedValueManager`][2] into `NativeAotValueManager`.

  * Allow `CodeGenerationTarget` to be explicitly provided to
    `<GenerateJavaStubs/>`, instead of inferring it based on
    `$(_AndroidRuntime)`.

[0]: https://learn.microsoft.com/en-us/previous-versions/xamarin/android/internals/architecture#java-activation
[1]: https://developer.android.com/guide/topics/manifest/application-element#nm
[2]: https://github.com/dotnet/java-interop/blob/dd3c1d0514addfe379f050627b3e97493e985da6/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs

@jonpryor jonpryor merged commit dbb0b92 into main Feb 3, 2025
58 checks passed
@jonpryor jonpryor deleted the dev/peppers/application/nativeaot branch February 3, 2025 19:09
grendello added a commit that referenced this pull request Feb 3, 2025
* main:
  [NativeAOT] Add support for `Application` subclasses (#9716)
  Bump to dotnet/sdk@d6bc7918c0 10.0.100-preview.2.25102.3 (#9726)
  [xa-prep-tasks] fix build errors for long `darc-` branch names (#9740)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants