Native AOT Support

Uno Platform 6.6 introduces support for .NET Native AOT deployment across Android, iOS, Linux, macOS, and Windows.

Enabling Native AOT enables faster app startup and improves performance, typically at the cost of larger app sizes. Consider the Uno.Chefs sample app:

Sample Environment Runtime Publish Size Startup Times (s)
Uno.Chefs Android, .NET 10 MonoVM 93M 0.895s
Uno.Chefs Android, .NET 10 NativeAOT 109M
(117% MonoVM)
0.348s
(39% MonoVM)
Uno.Chefs iOS, .NET 10 MonoVM 138M 0.940s
Uno.Chefs iOS, .NET 10 NativeAOT 122M
(88% MonoVM)
0.742s
(79% MonoVM)
Uno.Chefs Linux, .NET 10 CoreCLR 541M 0.87s
Uno.Chefs Linux, .NET 10 NativeAOT 625M
(118% CoreCLR)
0.35s
(40% CoreCLR)
Uno.Chefs macOS, .NET 10 CoreCLR 547M 1.347s
Uno.Chefs macOS, .NET 10 NativeAOT 720M
(141% CoreCLR)
0.555s
(41% CoreCLR)
Uno.Chefs Windows, .NET 10 CoreCLR 725M 1.605s
Uno.Chefs Windows, .NET 10 NativeAOT 970M
(139% CoreCLR)
0.824s
(51% CoreCLR)
Note

App startup times are provided for comparison purposes. Actual startup times will vary depending on hardware.

Note

Native AOT on Android will emit an XA1040 warning.

Prerequisites

Please see the .NET SDK Prerequisites documentation.

Some platforms have additional prerequisites.

Publishing Android apps with Native AOT requires NDK r27 or later, in addition to the normal .NET for Android SDK requirements.

Publish Native AOT using the CLI

Publishing apps mirrors the .NET documentation, and requires setting the $(PublishAot) MSBuild property within App.csproj to true:

<PropertyGroup>
  <PublishAot>true</PublishAot>
</PropertyGroup>

However, simply setting $(PublishAot) will cause issues for iOS; see dotnet/sdk#21877. Consequently, if your app multi-targets multiple target frameworks and iOS is one of those multiple frameworks, then you need to either:

  1. Set $(PublishAot) for each target framework separately, as per Native AOT deployment on iOS and Mac Catalyst > Publish using Native AOT:

    <PropertyGroup>
      <PublishAot Condition=" $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios' ">true</PublishAot>
      <PublishAot Condition=" $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst' ">true</PublishAot>
      <!-- repeat for any other platforms that Native AOT should be enabled on… -->
    </PropertyGroup>
    
  2. Exclude $(PublishAot) for target frameworks which cause the error described in dotnet/sdk#21877. At the time of this writing, excluding only browserwasm from Native AOT allows dotnet publish to work reliably.

    <PropertyGroup>
      <PublishAot Condition=" $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'browserwasm' ">true</PublishAot>
    </PropertyGroup>
    

It is also recommended to set the $(IsAotCompatible) MSBuild property in projects, which enables additional trimmer warnings:

<PropertyGroup>
  <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

Producing the Native AOT package requires a dotnet publish invocation that provides Runtime Identifier (-r) option, and if the project is multi-targeted, then the Target Framework (-f) option is also required:

dotnet publish -f net10.0-ios -r ios-arm64 App.csproj

During testing, it is often desirable to change $(PublishAot) on a per-build basis, so that NativeAOT and non-NativeAOT packages can be produced without editing the .csproj file. Use an intermediate MSBuild property which sets $(PublishAot) in only App projects:

<PropertyGroup>
  <PublishAot Condition=" '$(PublishAot)' == ''
      And '$(TestPublishAot)' != ''
      And $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'browserwasm' ">$(TestPublishAot)</PublishAot>
  <IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

This enables:

dotnet publish -f net10.0-ios -r ios-arm64 -p:TestPublishAot=true App.csproj

The intermediate MSBuild property is useful when you have an App.csproj which contains a project reference to a netstandard2.0 project, as using dotnet publish -p:PublishAot=true App.csproj … will generate build failures:

…/Microsoft.NET.Sdk.FrameworkReferenceResolution.targets(120,5): error NETSDK1207: Ahead-of-time compilation is not supported for the target framework.

Uno Platform Feature Support

Uno Platform Features work with Native AOT, but some adaptations may be required.

Animations

Animations on Attached Properties may require reflection adaptations.

Binding expressions to Attached properties (Storyboard setters) may require reflection adaptations.

Styling

Attached Property Style binding may require reflection adaptations.

Data Binding

Attached Properties binding may require reflection adaptations.

Runtime Performance

Expando Binding and DynamicObject Binding likely will not work under Native AOT, as it involves dynamic.

Microsoft.Web.WebView2.Core.CoreWebView2

The CoreWebView2 type does not work on Linux or Windows when using Native AOT. It does work on macOS.

See unoplatform/uno#23142 and unoplatform/uno#23159 for more details.

Others

AttachedProperty Binding and AttachedProperty Styling may require reflection adaptations.

Limitations of Native AOT deployment

Please refer to the Limitations of Native AOT deployment documentation. In particular:

  • No dynamic loading, for example, Assembly.LoadFile.
  • No runtime code generation, for example, System.Reflection.Emit.
  • Requires trimming, which has limitations.
  • Implies compilation into a single file, which has known incompatibilities.

Some System.Reflection facilities may not work either, such as Type.MakeGenericType() and MethodInfo.MakeGenericMethod().

Many Uno Platform features such as XAML Binding expressions may rely upon System.Reflection, often in "fallback" code paths. Native AOT supports System.Reflection, in that methods such as object.GetType() and Type.GetProperties() work. However, Native AOT is a trimming environment, meaning that types and properties (and more!) may be removed as part of the build process.

Consequently, when a Uno Platform app is built for Native AOT and runs, it is possible that types and members that are required are not present, which often presents as UI bugs such as missing content. The easiest way to obtain actionable information about missing types and members is via log messages.

Log Everything

Broadly speaking, Uno Platform apps write diagnostic log messages via two separate mechanisms:

  1. via an ILogger instance obtained from a .Log() extension method; see Logging.
  2. via an ILogger instance obtained via Dependency Injection, when using Uno.Extensions.

You need to see messages from both sources in order to track down, understand, and fix runtime errors. This may require changes to your App startup code: if you have an App.InitializeLogging() method, then:

  1. Ensure that it is called from your relevant Main() or startup code, and
  2. Ensure it emits output in Release configuration builds, at least during testing. Native AOT is only enabled in Release configuration builds.

If your app does not have an App.InitializeLogging() method, then add one and call it from your startup code. See unoplatform/uno.extensions#3008.

Messages will be written to your app's console output.

Messages will be written to adb logcat.

Reflection metadata

While Native AOT supports System.Reflection, there are a number of constraints when using Reflection, and XAML often uses Reflection in "fallback" code paths. Consequently, apps may not work properly when running in a Native AOT environment.

Native AOT compiles managed code into a single native binary. In order to support System.Reflection, a Reflection metadata "database" is generated at build time. To gain insight into what is retained from the trimming process and what is kept in Reflection metadata, add the following options to the dotnet publish command:

  • -p:TrimmerSingleWarn=false -p:_ExtraTrimmerArgs=--verbose: Show all warning messages from the trimmer during the build. Often times you will see "Assembly X has trimmer warnings" with no specifics; these options show all the warnings.

  • -p:IlcGenerateMetadataLog=true: Emit $(MSBuildProjectName).metadata.csv within $(IntermediateOutputPath). This allows viewing the "reflection metadata" that will be accessible to the app at runtime.

  • -p:EmitCompilerGeneratedFiles=true "-p:CompilerGeneratedFilesOutputPath=PATH": Emit C# source generator output into PATH. It is frequently useful to separately review and search generated output.

  • -p:IlcGenerateMstatFile=true: Emit $(MSBuildProjectName).mstat within $(IntermediateOutputPath). This is an assembly that references all types, fields, non-inlined methods, etc., which make up the resulting native binary after trimming.

    The .mstat can be disassembled with ildasm.

    The output of monodis may be easier to understand, such as monodis --typeref App.mstat to view all referenced types, and monodis --memberref App.mstat to view all referenced members.

    Note

    Just because the .mstat file references a type or property does not mean that the member is available via Reflection. Only information within the .metadata.csv file generated by -p:IlcGenerateMetadataLog=true is accessible via Reflection.

For example, to build an App project which prints all trimmer messages, retains all generated C# Source generator output, and dumps Reflection metadata:

dotnet publish -f net10.0-ios -r ios-arm64 -p:TestPublishAot=true -bl App.csproj `
  -p:TrimmerSingleWarn=false -p:_ExtraTrimmerArgs=--verbose -p:IlcGenerateMetadataLog=true -p:IlcGenerateMstatFile=true `
  -p:EmitCompilerGeneratedFiles=true "-p:CompilerGeneratedFilesOutputPath=$(Join-Path -Path $pwd -ChildPath ../_gen)"

There are three ways to add types and members to Reflection metadata:

  1. The DynamicallyAccessedMembersAttribute custom attribute
  2. The DynamicDependencyAttribute custom attribute.
  3. With Linker Descriptor XML files via the @(TrimmerRootDescriptor) item group and other locations.

Adapt an app to Native AOT deployment

An app may require changes in order to run under Native AOT. (It is also possible that Native AOT cannot be used at all, depending on the app dependencies.)

See the Fixing trim warnings Microsoft documentation.

Additional common problems and their workarounds follow.

Use of Type.GetType(string)

Type.GetType(string) and related overloads only work reliably when given a string constant:

var t = Type.GetType("System.Int32, System.Runtime");

Any other use may mean that the specified type isn't available at runtime, resulting in an exception.

If Type.GetType(string) must be used, always use a string constant containing an assembly-qualified name.

JSON serialization

Many apps (and templates) rely on reflection-based serialization behavior (for example, System.Text.Json default runtime contract discovery). Under Native AOT, this can fail because required members and metadata can be removed.

Prefer System.Text.Json source generators for DTOs and commonly serialized types.

When using Uno.Extensions.Serialization, use the .AddJsonTypeInfo() extension method to register the Json Source Generator output:

using System.Text.Json.Serialization;

public record Person(string name, int age, double height, double weight);

[JsonSerializable(typeof(Person))]
public partial class PersonJsonContext : JsonSerializerContext
{
}

partial class App
{
    protected override void OnLaunched(LaunchActivatedEventArgs e)
    {
        var appBuilder = this.CreateBuilder(args)
            .Configure(host => {
                host
                .UseSerialization(services =>
                {
                    services
                        .AddJsonTypeInfo(PersonJsonContext.Default);
                });
            });
        …
    }
}

Properties

Properties may be referenced via XAML Binding Expressions, and if not preserved by Native AOT then using them will result in messages such as:

fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
      The [ListenButtonContent] property getter does not exist on type [Uno.Gallery.Views.Samples.ClipboardSamplePageViewModel]
fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
      The [Message] property getter does not exist on type [Uno.Gallery.Views.Samples.ClipboardSamplePageViewModel]

There are two general ways to fix this.

The easiest way is to add Microsoft.UI.Xaml.Data.BindableAttribute to the declaring type:

[Microsoft.UI.Xaml.Data.Bindable]
partial class ClipboardSamplePageViewModel {
}

However, this will not work if the type has init-only properties and also WinRT support is required, as this will hit microsoft/microsoft-ui-xaml#8723.

If WinRT support is required, then [DynamicDependency] must be used:

partial class ClipboardSamplePageViewModel {
    [DynamicDependency(nameof(ListenButtonContent))]
    [DynamicDependency(nameof(Message))]
    public ClipboardSamplePageViewModel() {…}
}

XAML Attached Properties

XAML binding can involve Reflection, which can include use of attached properties. If Reflection Metadata doesn't include a required attached property, then the app log will contain messages such as:

fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
      The [ShowMeTheXAML:XamlDisplayExtensions.Header] property getter does not exist on type [ShowMeTheXAML.XamlDisplay]
fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
      The [ShowMeTheXAML:XamlDisplayExtensions.Description] property getter does not exist on type [ShowMeTheXAML.XamlDisplay]
fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
      The [ShowMeTheXAML:XamlDisplayExtensions.Options] property getter does not exist on type [ShowMeTheXAML.XamlDisplay]
fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
      The [ShowMeTheXAML:XamlDisplayExtensions.PrettyXaml] property getter does not exist on type [ShowMeTheXAML.XamlDisplay]
fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
      The [ShowMeTheXAML:XamlDisplayExtensions.ShowXaml] property getter does not exist on type [ShowMeTheXAML.XamlDisplay]

Fix this by using [DynamicDependency] on the attached property so that the associated Get and Set methods are preserved:

diff --git a/Uno.Gallery/Extensions/XamlDisplayExtensions.cs b/Uno.Gallery/Extensions/XamlDisplayExtensions.cs
index 764886bb..f146a16f 100644
--- a/Uno.Gallery/Extensions/XamlDisplayExtensions.cs
+++ b/Uno.Gallery/Extensions/XamlDisplayExtensions.cs
@@ -1,6 +1,7 @@
 #pragma warning disable
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
 using System.Text;
@@ -28,7 +29,12 @@ namespace ShowMeTheXAML
 
                #region Property: Header
 
-               public static DependencyProperty HeaderProperty { get; } = DependencyProperty.RegisterAttached(
+               public static DependencyProperty HeaderProperty
+               {
+                       [DynamicDependency(nameof(GetHeader))]
+                       [DynamicDependency(nameof(SetHeader))]
+                       get;
+               } = DependencyProperty.RegisterAttached(
                        "Header",
                        typeof(string),
                        typeof(XamlDisplayExtensions),

All use of DependencyProperty.RegisterAttached() is potentially suspect and should be reviewed.