CoreBus and Native AOT: Hands-On Experience Compiling a Cross-Platform Terminal
Compiling .NET applications using Native AOT promises faster startup times and smaller distribution sizes, but in practice, it comes with a number of technical limitations. This article dives into a real-world case of integrating Native AOT into CoreBus—a cross-platform terminal for working with COM ports and Modbus protocols.
Why Native AOT?
The main drivers for the switch were slow cold starts on low-end PCs and the large installer package size. After migrating from WPF to Avalonia UI, the app size shrank by ~60 MB, but hitting the 10 MB target still seemed out of reach. Native AOT looked like the logical next step, especially for an app aimed at industrial equipment and embedded systems where resources are often tight.
However, right from the build stage, it became clear that Native AOT support required overhauling the bindings architecture, serialization, and publishing strategy.
Problem 1: Compiled Bindings in Avalonia
Avalonia uses dynamic bindings via reflection by default, which isn't compatible with Native AOT. The fix is to enable precompiled bindings:
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
This means explicitly specifying the data type in every XAML file using the x:DataType attribute on the root element (Window, UserControl) and inside DataTemplates. Skipping this attribute leads to either a compilation error (Dynamic code generation is not supported on this platform) or a crash right after launch.
In the CoreBus project, this required updating all XAML markup, which took a significant amount of time but eliminated runtime exceptions.
Problem 2: Serialization Without Reflection
CoreBus settings and presets are stored in JSON files using System.Text.Json. The standard serialization methods are marked with RequiresUnreferencedCode and RequiresDynamicCode attributes, triggering IL2026 and IL3050 warnings during Native AOT builds.
The solution is to use source-generated JSON serializers via JsonSerializerContext. Here's an example of a proper implementation:
var options = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = SerializerContext.Default
};
using var stream = new FileStream(correctFilePath, FileMode.Open);
var jsonTypeInfo = options.TypeInfoResolver.GetTypeInfo(typeof(T), options);
if (jsonTypeInfo == null)
{
throw new Exception($"Not succeeded nayti type {typeof(T)} in obyavlenii konteksta serializatsii.");
}
JsonSerializer.Serialize(stream, data, jsonTypeInfo);
For deserialization, use a similar approach:
var data = (T?)JsonSerializer.Deserialize(stream, typeof(T), SerializerContext.Default);
This completely avoids reflection and ensures compatibility with trimming and AOT.
Problem 3: Cross-Platform Builds
Native AOT requires native compilation for the target OS and architecture. The Linux kernel version and glibc are critical for compatibility:
- Builds on Ubuntu 24.10 (kernel 6.11) wouldn't run on Astra Linux CE (kernel 5.15).
- Builds on Ubuntu 18.04 (glibc 2.27) failed on Astra Linux CE (glibc 2.24) due to lack of backward compatibility in the C library.
Final strategy:
- Windows: Build on Windows 11, test on Windows 7/10—successful compatibility.
- Linux: Build directly on Astra Linux CE with kernel 5.4 and glibc 2.24.
This maximizes portability but complicates the CI/CD process.
Problem 4: Trimming and Dependent Assemblies
Trimming (removing unused code) delivers the biggest savings only in the main executable project. Auxiliary libraries yield less than 300 KB total savings.
However, aggressive trimming can strip essential Avalonia resources, especially styles. It's recommended to explicitly exclude themes from trimming:
<ItemGroup>
<TrimmerRootAssembly Include="Avalonia.Themes.Fluent" />
</ItemGroup>
While this might not be necessary in Avalonia 11.3.x, it's best to keep the setting as a safeguard.
Problem 5: False Positives from Antivirus Software
Apps built with Native AOT + trimming are often flagged as malicious. Windows Defender labeled the installer as Trojan:Win32/Bearfoos.B!ml and the portable version as Trojan:Script/Wacatac.B!ml.
This is a known issue tied to changes in the binary structure and lack of digital signing. The fix is to go through Microsoft SmartScreen verification and add a digital signature.
Key Takeaways
- Native AOT requires a complete rejection of reflection and dynamic code generation.
- Compilation must be done on the target platform, accounting for kernel and system library versions.
- Source-generated serialization is mandatory for JSON handling.
- Trimming can damage UI resources—manually exclude key assemblies.
- Antivirus tools often false-positive on unsigned AOT binaries.
— Editorial Team
No comments yet.