返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
Desktop

AOT Compilation for Avalonia Applications: StarBlog Publisher Project Practice and Challenges

This article introduces the cross-platform article publishing tool StarBlog Publisher developed by the author using the Avalonia framework, and details the process of publishing an Avalonia application using AOT (Ahead-Of-Time) technology along with the issues encountered. The article discusses the pros and cons of AOT, configuration methods, and provides solutions for issues such as JSON serialization and type registration. The author shares techniques such as using the Newtonsoft.Json library and the rd.xml file to optimize AOT publishing performance and compatibility.

曦远 (DealiAxy)约 38 分钟
目录14 个章节

Preface

Recently, I developed an article publishing tool using Avalonia, named StarBlog Publisher.

Avalonia is a cross-platform UI framework that can run on Windows, Linux, and macOS. Its features include high performance, cross-platform compatibility, and ease of use.

Avalonia has many advantages, such as high performance, cross-platform support, and user-friendliness. However, it also has some drawbacks, like a steeper learning curve and documentation that can be harder to find.

But Avalonia is built on the .NET Core framework. The final packaged executable, if published as framework-dependent, requires the .NET Core runtime environment to be installed on the client machine, which can be a significant burden for users. If published as self-contained, the file size becomes larger.

Additionally, it is susceptible to decompilation, which is not allowed in some commercial software. (Though this is not an issue for my project since it is open source.)

This article uses the StarBlog Publisher project as an example to document the process and challenges encountered when publishing an Avalonia application using AOT.

The new version 1.1 has been released. Feel free to download and try it out: https://github.com/star-blog/starblog-publisher/releases

About AOT

Starting from .Net7, support for AOT publishing has been gradually introduced, which is a very important feature. AOT publishing can compile .Net applications into machine code that does not depend on the runtime library. It has a smaller footprint and is less susceptible to decompilation.

Native AOT = At compile time, directly precompile the IL (Intermediate Language) of a .NET program into machine code, generating a truly standalone executable file that does not depend on the CLR.

Compared to traditional .NET applications that dynamically compile at runtime using the JIT (Just-In-Time compiler), Native AOT handles all compilation work Ahead of Time.

Here is a simple Native AOT process flowchart:

C# Code
   ↓
Roslyn Compilation (generates IL)
   ↓
Native AOT Compiler (ilc)
   ↓
- Static Analysis
- Tree Shaking
- IL to Machine Code Conversion
- Linker synthesizes executable file
   ↓
Final Single .exe (No JIT, No Large CLR)

The current LTS version is .Net8, which already has fairly comprehensive support for AOT. This time, I will try to publish an Avalonia application using the AOT method.

PS: It is said that .Net9 provides many optimizations and improvements for the AOT method, and I will try it out next.

Potential Issues When Using AOT

  • Compatibility Issues: AOT compilation may not be compatible with certain dependency libraries, especially those that rely on reflection, dynamic code generation, or JIT compilation. If you encounter issues, you may need to add more configurations in rd.xml.
  • Package Size: AOT compilation generates larger executable files (compared to framework-dependent mode), but with faster startup times.
  • Debugging Difficulty: Debugging AOT-compiled applications may be more challenging.
  • Third-Party Libraries: Check whether the third-party libraries used in your project support AOT compilation. For example, Microsoft.Extensions.AI and Microsoft.Extensions.AI.OpenAI are in preview and may require special attention to their AOT compatibility.
  • Avalonia-Specific Configuration: For Avalonia applications, you may need to ensure that XAML-related type information is correctly preserved.

Modifying the Project File

First, you need to add AOT-related configuration to the project file:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <Nullable>enable</Nullable>
        <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
        <ApplicationManifest>app.manifest</ApplicationManifest>
        <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
        
        <!-- AOT related configuration -->
        <PublishAot>true</PublishAot>
        <TrimMode>full</TrimMode>
        <InvariantGlobalization>true</InvariantGlobalization>
        <IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
        <IlcOptimizationPreference>Size</IlcOptimizationPreference>
        <IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
        
        <JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
    </PropertyGroup>

    <!-- Keep the remaining parts unchanged -->
</Project>

JSON Serialization Issues

In an AOT (Ahead-of-Time) compilation environment, JSON serialization is a common issue because it typically relies on runtime reflection.

This project uses JSON in several places:

One is for application settings, and another is for network requests.

In conclusion: Newtonsoft.Json offers better AOT support compared to System.Text.Json. If you plan to use AOT, it is recommended to prioritize the Newtonsoft.Json library.

Modify AppSettings.cs to Support AOT

If you absolutely must use System.Text.Json, then you need to make some modifications. If you are using Newtonsoft.Json, you can skip this section.

using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using StarBlogPublisher.Services.Security;
using System.Text.Json.Serialization.Metadata; // Add this namespace

namespace StarBlogPublisher.Services;

// Add the JsonSerializable attribute to generate serialization code for AOT
[JsonSerializable(typeof(AppSettings))]
internal partial class AppSettingsContext : JsonSerializerContext
{
}

public class AppSettings {
    private static readonly string ConfigPath = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
        "StarBlogPublisher",
        "settings.json"
    );

    // ... existing code ...

    private static AppSettings Load() {
        try {
            if (File.Exists(ConfigPath)) {
                var json = File.ReadAllText(ConfigPath);
                // Use an AOT-friendly serialization approach
                var settings = JsonSerializer.Deserialize(json, AppSettingsContext.Default.AppSettings);
                return settings ?? new AppSettings();
            }
        }
        catch (Exception ex) {
            // If loading fails, return default settings
            Console.WriteLine($"Failed to load app settings. {ex}");
        }

        return new AppSettings();
    }

    public void Save() {
        try {
            var directory = Path.GetDirectoryName(ConfigPath);
            if (!string.IsNullOrEmpty(directory)) {
                Directory.CreateDirectory(directory);
            }

            // Use an AOT-friendly serialization approach
            var json = JsonSerializer.Serialize(this, AppSettingsContext.Default.AppSettings, new JsonSerializerOptions {
                WriteIndented = true
            });
            File.WriteAllText(ConfigPath, json);

            // Trigger the settings changed event
            SettingsChanged?.Invoke(this, EventArgs.Empty);
        }
        catch (Exception) {
            // todo: Handle save failure scenario
        }
    }
}

Explanation

  1. Added the [JsonSerializable] attribute and the derived JsonSerializerContext class. This is the key to supporting AOT for JSON serialization in .NET. It generates the serialization code at compile time, rather than relying on runtime reflection.
  2. Modified the Load() and Save() methods to use AppSettingsContext.Default.AppSettings as the type information, instead of relying on runtime type inference.
  3. This approach ensures that all necessary serialization code is generated at compile time in an AOT environment, eliminating the need for runtime reflection.

Additionally, you need to ensure the JSON source generator for AOT compilation is enabled in your project file:

<PropertyGroup>
    <!-- Other properties -->
    <JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>

These modifications will ensure that the AppSettings class can correctly serialize and deserialize JSON in an AOT compilation environment.

Refit JSON Serialization Issue in AOT Mode

In AOT mode, the Refit library's JSON processing can also use Newtonsoft.Json.

First, install the Refit.Newtonsoft.Json library, and additional configuration is required to handle type information.

Adding Type Pre-registration

A new class needs to be created to pre-register all types used in the API interfaces:

using Newtonsoft.Json;
using StarBlogPublisher.Models;
using System.Collections.Generic;
using CodeLab.Share.ViewModels.Response;

namespace StarBlogPublisher.Services;

/// <summary>
/// Pre-register types used by Refit for AOT compilation.
/// </summary>
public static class RefitTypeRegistration
{
    /// <summary>
    /// Call this method at application startup to ensure all types are pre-registered.
    /// </summary>
    public static void RegisterTypes()
    {
        // Register common response types.
        JsonConvert.DefaultSettings = () => new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.Auto,
            // Add custom converters if needed.
            Converters = new List<JsonConverter>
            {
                // Custom converters can be added here.
            }
        };

        // Pre-warm types - ensure these types are included during AOT compilation.
        var types = new[]
        {
            typeof(ApiResponse<>),
            typeof(ApiResponse<List<Category>>),
            typeof(ApiResponse<List<WordCloud>>),
            // Add other API response types here.
            typeof(List<Category>),
            typeof(Category),
            typeof(WordCloud),
            // Add all model types here.
        };

        // Trigger type loading.
        foreach (var type in types)
        {
            var _ = type.FullName;
        }
    }
}

Modifying the ApiService Class

Modify the ApiService class to ensure types are registered during initialization:

using Refit;
using StarBlogPublisher.Services.StarBlogApi;
using System;
using System.Net;
using System.Net.Http;
using Newtonsoft.Json;

namespace StarBlogPublisher.Services;

public class ApiService {
    private static ApiService? _instance;

    public static ApiService Instance {
        get {
            _instance ??= new ApiService();
            return _instance;
        }
    }

    private readonly RefitSettings _refitSettings;

    private ApiService() {
        // Ensure types are registered.
        RefitTypeRegistration.RegisterTypes();
        
        // Configure Refit settings.
        _refitSettings = new RefitSettings(new NewtonsoftJsonContentSerializer(
            new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.Auto,
                // Disable reflection optimization, which is important in an AOT environment.
                TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
            }
        ));
    }

    // ... The rest of the code remains unchanged ...
}

Initializing Type Registration in App.axaml.cs

Ensure type registration is called at application startup:

public override void OnFrameworkInitializationCompleted()
{
    // Ensure Refit types are registered.
    RefitTypeRegistration.RegisterTypes();
    
    // Other initialization code...
    
    base.OnFrameworkInitializationCompleted();
}

Adding AOT Compatibility Configuration

Due to AOT compilation's restrictions on reflection and dynamic code generation, an rd.xml file needs to be added to specify the types that must be preserved:

<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
    <Application>
        <!-- Add assemblies and types to be preserved in AOT -->
        <Assembly Name="StarBlogPublisher" Dynamic="Required All" />
        <Assembly Name="Avalonia.Markup.Xaml" Dynamic="Required All" />
        <Assembly Name="Avalonia" Dynamic="Required All" />

        <!-- Refit related assemblies -->
        <Assembly Name="Refit" Dynamic="Required All" />
        <Assembly Name="Newtonsoft.Json" Dynamic="Required All" />

        <!-- Add API interfaces and model types -->
        <Assembly Name="StarBlogPublisher">
            <Type Name="StarBlogPublisher.Services.StarBlogApi.IAuth" Dynamic="Required All" />
            <Type Name="StarBlogPublisher.Services.StarBlogApi.ICategory" Dynamic="Required All" />
            <Type Name="StarBlogPublisher.Services.StarBlogApi.IBlogPost" Dynamic="Required All" />
        </Assembly>
        
        <Assembly Name="CodeLab.Share">
            <Type Name="CodeLab.Share.ViewModels.Response.ApiResponse`1" Dynamic="Required All" />
        </Assembly>
    </Application>
</Directives>

Then, reference this rd.xml file in the project file:

<ItemGroup>
    <RdXmlFile Include="rd.xml" />
</ItemGroup>

Publishing

Use the following command to publish the AOT version of the application:

dotnet publish -c Release -r win-x64 -p:PublishAot=true

For other platforms, you can replace the corresponding RID (runtime identifier):

  • Windows: win-x64
  • macOS: osx-x64
  • Linux: linux-x64

Summary

AOT publishing is an important feature of the .NET platform. It compiles applications into machine code that does not depend on the runtime, which not only reduces the deployment package size but also improves startup speed and increases the difficulty of decompilation.

However, publishing Avalonia applications using the AOT method still has some pitfalls: JSON serialization issues, type registration problems, and AOT compatibility issues. The following solutions can address these problems:

  1. For JSON serialization, the Newtonsoft.Json library, which has better AOT support, is preferred, and serialization correctness is ensured through type pre-registration.
  2. For functionalities requiring reflection, the types that need to be retained are explicitly declared in the rd.xml file, solving the type trimming issue during AOT compilation.
  3. In terms of project configuration, performance and package size are balanced by reasonably setting AOT-related compilation options.

Although AOT publishing still has some limitations, such as relatively difficult debugging and potential incompatibility with some third-party libraries, AOT support will become more and more complete as the .NET platform develops (especially versions after .NET 9). For Avalonia applications that require high performance, small size, and decompilation protection, AOT publishing is a worthwhile option to consider.

_EOF_

AOT Compilation for Avalonia Applications: StarBlog Publisher Project Practice and Challenges

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

聊聊几个相见恨晚的WinForm界面库

前阵子在逛知乎的时候看到了Gitee发的文章,里面介绍了一些WinForm开源项目,其中就有涉及到界面库,我一下子就来了兴趣,然后顺瓜摸藤又找到了几个类似的界面库,看着熟悉的界面和代码,有一种「爷的青春回来了」的感觉~ 可惜属于WinForm的短暂时代早已结束,当今Web统治一切,怕是WinForm以后只能用来追忆往事了吧…… SunnyUI 看看作者的截图: 对话框: 看这界面颇有年代感,很惊讶

2025-03-27 · Desktop · 约 1 分钟

提升Avalonia UI质感,跨平台图标库选型实践

本文介绍了在Avalonia框架下使用两种图标库(Icon.Avalonia和IconPacks.Avalonia)的详细方法。作者通过对比不同设备间emoji显示不一致的问题,引出使用FontAwesome图标的优势。文章重点讲解了两种库的安装步骤、XAML配置方法及实际应用场景,包括独立图标显示、控件附加、动画效果实现等。其中IconPacks.Avalonia支持35种图标集,功能更为丰富,还提供了图标浏览器工具。全文通过具体代码示例,为开发者提供了实用的技术参考。

2025-06-12 · Desktop · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…

AOT Compilation for Avalonia Applications: StarBlog Publisher Project Practice and Challenges · StarBlog