Skip to content
  • Marek Habersack's avatar
    Enable marshal methods support by default (#7351) · 8bc7a3e8
    Marek Habersack authored
    Context: 5271f3e1
    Context: e1af9587
    Context: 186a9fcf
    Context: 903ba37c
    Context: a760281b
    Context: https://github.com/xamarin/xamarin-android/wiki/Blueprint#java-type-registration
    
    Complete the LLVM Marshal Methods effort sketched out in e1af9587.
    
    LLVM Marshal Methods are only supported in .NET Android, *not*
    Xamarin.Android.
    
    A *Marshal Method* is a JNI Callable C function (pointer) which has
    [parameter types and return types which comply with the JNI ABI][0].
    [`generator`][1] emits marshal methods as part of the binding, which
    are turned into Delegate instances at runtime as part of
    [Java Type Registration][2].
    
    *LLVM Marshal Methods* turn this runtime operation -- looking up
    `generator`-emitted marshal methods and registering those methods
    with Java -- into a *build-time* operation, using LLVM-IR to generate
    [JNI Native Method Names][3] which will then be contained within
    `libxamarin-app.so`.  LLVM Marshal Methods will also *remove* the
    previous Reflection-based infrastructure from relevant types.
    
    LLVM Marshal Methods are *enabled by default* for ***Release***
    configuration builds in .NET 8, and disabled by default for Debug
    builds. The new `$(AndroidEnableMarshalMethods)` MSBuild property
    explicitly controls whether or not LLVM Marshal Methods are used.
    
    LLVM Marshal Methods are *not* available in Classic Xamarin.Android.
    
    
    ~~ Build Phase: Scanning for Compatible Types ~~
    
    During the application build, all `Java.Lang.Object` and
    `Java.Lang.Throwable` subclasses are scanned as part of
    [Java Callable Wrapper generation][4], looking for "un-bound"
    (user-written) types which override `abstract` or `virtual`
    methods, or implement interface members.  This is done to emit
    Java Callable Wrappers, Java code which "mirrors" the C# code with
    an appropriate base class, interface implementation list, and
    Java `native` method declarations for "virtual" member overrides.
    
    This scanning process is updated for LLVM Marshal Methods to classify
    each type to see if it requires the legacy Delegate-based
    registration mechanism, as constructs such as
    `[Java.Interop.ExportAttribute]` cannot (yet) be used with
    LLVM Marshal Methods.
    
    
    ~~ Build Phase: Java Callable Wrapper Generation ~~
    
    For example, given the C# type:
    
        // C#
        public partial class MainActivity : Activity {
            protected override void OnCreate (Bundle? state) => …
        }
    
    Then the resulting Java Callable Wrapper *without* LLVM Marshal
    Methods enabled will be:
    
        // Java + No LLVM Marshal Methods
        public /* partial */ class MainActivity extends Activity {
            static {
                String __md_methods =
                    "n_onCreate:(Landroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n";
                mono.android.Runtime.register ("Example.MainActivity, ExampleAssembly", MainActivity.class, __md_methods);
            }
            public void onCreate (android.os.Bundle p0) {n_onCreate(p0);}
            private native void n_onCreate (android.os.Bundle p0);
        }
    
    When LLVM Marshal Methods are enabled, the Java Callable Wrapper
    has no static constructor, nor any call to `Runtime.register()`.
    
    
    ~~ Build Phase: Marshal Method Wrapper ~~
    
    Consider the binding infrastructure code that `generator` emits for
    `Android.App.Activity.OnCreate()`:
    
    	namespace Android.App {
    	    public partial class Activity {
    	        static Delegate? cb_onCreate_Landroid_os_Bundle_;
    	#pragma warning disable 0169
    	        static Delegate GetOnCreate_Landroid_os_Bundle_Handler ()
    	        {
    	            if (cb_onCreate_Landroid_os_Bundle_ == null)
    	                cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPL_V) n_OnCreate_Landroid_os_Bundle_);
    	            return cb_onCreate_Landroid_os_Bundle_;
    	        }
    
    	        static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState)
    	        {
    	            var __this = global::Java.Lang.Object.GetObject<Android.App.Activity> (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
    	            var savedInstanceState = global::Java.Lang.Object.GetObject<Android.OS.Bundle> (native_savedInstanceState, JniHandleOwnership.DoNotTransfer);
    	            __this.OnCreate (savedInstanceState);
    	        }
    	#pragma warning restore 0169
    
    	        [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]
    	        protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState)
    	        {
    	            const string __id = "onCreate.(Landroid/os/Bundle;)V";
    	            try {
    	                JniArgumentValue* __args = stackalloc JniArgumentValue [1];
    	                __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle);
    	                _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args);
    	            } finally {
    	                global::System.GC.KeepAlive (savedInstanceState);
    	            }
    	        }
    	    }
    	}
    
    When LLVM Marshal Methods are enabled, the following IL
    transformations are performed:
    
      * The `static Delegate? cb_…` field is removed.
      * The `static Delegate Get…Handler()` method is removed.
      * A new `static … n_…_mm_wrapper()` method is added.
    
    The `n_…_mm_wrapper()` method is responsible for exception marshaling
    and for `bool` marshaling.  The `n_…_mm_wrapper()` method has the
    [`UnmanagedCallersOnlyAttribute`][5], and works by calling the
    existing `n_…()` method:
    
    	namespace Android.App {
    	    public partial class Activity {
    
    	        // Added
    	        [UnmanagedCallersOnly]
    	        static void n_OnCreate_Landroid_os_Bundle__mm_wrapper (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState)
    	        {
    	            try {
    	                n_OnCreate_Landroid_os_Bundle_ (jnienv, native__this, native_savedInstanceState);
    	            }
    	            catch (Exception __e) {
    	                Android.Runtime.AndroidEnvironmentInternal.UnhandledException (__e);
    	            }
    	        }
    	    }
    	}
    
    
    ~~ Build Phase: LLVM-IR Marshal Method Generation ~~
    
    For each Java `native` method declaration contained in Java Callable
    Wrappers which support LLVM Marshal Methods, LLVM-IR is used to
    generate the JNI Native Method with the `Java_…` symbol name:
    
    	using android_app_activity_on_create_bundle_fn = void (*) (JNIEnv *env, jclass klass, jobject savedInstanceState);
    	static android_app_activity_on_create_bundle_fn android_app_activity_on_create_bundle = nullptr;
    
    	extern "C" JNIEXPORT void
    	JNICALL Java_helloandroid_MainActivity_n_1onCreate__Landroid_os_Bundle_2 (JNIEnv *env, jclass klass, jobject savedInstanceState) noexcept
    	{
    	  if (android_app_activity_on_create_bundle == nullptr) {
    	    get_function_pointer (
    	        16,                                                               // mono image index; computed at build time
    	        0,                                                                // class index; computed at build time
    	        0x0600055B,                                                       // method token; computed at build time
    	        reinterpret_cast<void*&>(android_app_activity_on_create_bundle)   // target pointer
    	    );
    	  }
    
    	  android_app_activity_on_create_bundle (env, klass, savedInstanceState);
    	}
    
    
    ~~ Other Changes ~~
    
    The new `Android.Runtime.JNIEnvInit` type was split out of the
    `Android.Runtime.JNIEnv` type to further reduce startup overhead, as
    there are fewer fields to initialize.
    
    The `Mono.Android.Runtime.dll` assembly is added because the
    Marshal Method Wrapper needs to be able to invoke what *was*
    `AndroidEnvironment.UnhandledException()`, *while also* updating
    `Mono.Android.dll`!  `Mono.Android.Runtime.dll` allows the marshal
    method wrappers to reliably use
    `Android.Runtime.AndroidEnvironmentInternal.UnhandledException()`,
    which will *never* be changed by the marshal method wrapper
    infrastructure.
    
    
    ~~ Results ~~
    
    Marshal methods make application startup around 3.2% faster (the
    bigger the app the  more performance gains), with a bit room for
    future improvements (by eliminating wrapper methods and other
    optimizations):
    
    [.NET Podcasts][6] app test results:
    
    | Before  | After   | Δ        | Notes                                          |
    | ------- | ------- | -------- | ---------------------------------------------- |
    | 868.500 | 840.400 | -3.24% ✓ | preload disabled; 32-bit build; no compression |
    | 863.700 | 837.600 | -3.02% ✓ | preload disabled; 64-bit build; no compression |
    | 872.500 | 850.100 | -2.57% ✓ | preload enabled; 64-bit build                  |
    | 877.000 | 854.800 | -2.53% ✓ | preload disabled; 64-bit build                 |
    | 859.300 | 839.800 | -2.27% ✓ | preload enabled; 64-bit build; no compression  |
    | 871.700 | 853.100 | -2.13% ✓ | preload enabled; 32-bit build                  |
    | 860.600 | 842.300 | -2.13% ✓ | preload enabled; 32-bit build; no compression  |
    | 869.500 | 852.500 | -1.96% ✓ | preload disabled; 32-bit build                 |
    
    Maui Hello World app test results:
    
    | Before  | After   | Δ        | Notes                                          |
    | ------- | ------- | -------- | ---------------------------------------------- |
    | 374.800 | 365.500 | -2.48% ✓ | preload disabled; 64-bit build                 |
    | 374.100 | 365.600 | -2.27% ✓ | preload disabled; 32-bit build                 |
    | 369.100 | 364.400 | -1.27% ✓ | preload enabled; 32-bit build                  |
    | 364.300 | 360.600 | -1.02% ✓ | preload enabled; 32-bit build; no compression  |
    | 368.900 | 365.400 | -0.95% ✓ | preload enabled; 64-bit build                  |
    | 362.500 | 359.400 | -0.86% ✓ | preload disabled; 32-bit build; no compression |
    | 361.100 | 361.600 | +0.14% ✗ | preload enabled; 64-bit build; no compression  |
    | 359.200 | 368.000 | +2.39% ✗ | preload disabled; 64-bit build; no compression |
    
    
    [0]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#native_method_arguments
    [1]: https://github.com/xamarin/xamarin-android/wiki/Blueprint#generator
    [2]: https://github.com/xamarin/xamarin-android/wiki/Blueprint#java-type-registration
    [3]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names
    [4]: https://github.com/xamarin/xamarin-android/wiki/Blueprint#java-callable-wrapper-generator
    [5]: https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-7.0
    [6]: https://github.com/microsoft/dotnet-podcasts/tree/net7.0
    8bc7a3e8