Skip to content
 
 

Latest commit

 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WPF Native AOT

A proof-of-concept fork of WPF with changes to support Native AOT deployment, plus a sample calculator app (wpfaot) that validates the AOT-compatible build end-to-end.

Repository: github.com/ibebbs/wpfaot | WPF fork branch: ibebbs/wpf@feature/native-aot

Project overview

WPF was never designed for ahead-of-time compilation. It relies heavily on runtime reflection, dynamic assembly loading, COM interop, and Reflection.Emit -- all of which are incompatible with Native AOT. This project systematically addresses each of those blockers in a fork of WPF, resulting in an AOT-published WPF application that starts in ~136 ms (vs ~650-730 ms with JIT), uses 32% less memory, and ships as a single native executable with no .NET runtime dependency.

Key changes made to the WPF fork

Area What was done
XAML runtime AOT-safe ClrObjectRuntime used instead of DynamicMethodRuntime (no Reflection.Emit)
Assembly resolution BAML/XAML assembly resolution uses already-loaded assemblies only (no Assembly.Load in AOT)
Trimming annotations [DynamicallyAccessedMembers] and [RequiresUnreferencedCode] on key WPF APIs
COM interop Critical COM sites (drag-drop, TSF, UIAutomation focus) gated behind RuntimeFeature.IsDynamicCodeSupported
DirectWrite fonts Custom font loaders ported from C++/CLI to C# with [GeneratedComInterface]/[GeneratedComClass] for AOT-compatible COM
Feature switches [FeatureSwitchDefinition] switches for 5 subsystems (spell check, printing, drag-drop, TSF, accessibility) enabling dead-code elimination
Compiled bindings New {CompiledBinding} markup extension + Roslyn source generator for reflection-free data binding
UI Automation 26 UIA provider interfaces converted to [GeneratedComInterface]; 24+ implementation classes annotated with [GeneratedComClass] (in progress)

Performance (AOT vs JIT)

Metric Framework-dependent Self-contained AOT + trimmed
Startup time (median) 652 ms 728 ms 136 ms
Working set (RSS) 82.4 MB 82.8 MB 56.1 MB
Runtime files 202 files (24 MB) 413 files (118 MB) 6 files (63 MB)

Measured on Intel i7-14700K, Windows 10, .NET 10 Preview. See wpfaot-perf.md for full methodology.


Prerequisites

Required software

  1. .NET 10 SDK (10.0.200-preview or later)

  2. Visual Studio 2022 (17.2 or later) with WPF build workloads

    • Install via the Visual Studio Installer
    • Import the workload configuration file: wpf\Documentation\wpf.vsconfig
      • In the VS Installer, click More > Import configuration and select the file
    • Or manually ensure these workloads/components are installed:
      • .NET desktop development workload
      • Desktop development with C++ workload (required for DirectWriteForwarder C++/CLI project)
      • Individual components:
        • MSVC C++ build tools (v143 or later)
        • C++/CLI support
        • Windows 10 SDK (10.0.19041.0)
        • .NET Framework 4.7.2 targeting pack
        • ATL and MFC libraries
        • Text Template Transformation
  3. Windows 10 (build 19041 or later) or Windows 11

Optional tools

  • PerfView -- for ETW trace analysis (see wpfaot-plan.md section 6.2)
  • Inspect.exe (from Windows SDK) -- for verifying UI Automation tree

Getting the code

git clone https://github.com/ibebbs/wpfaot.git
cd wpfaot

This is a mono-repo containing both the WPF fork and the sample app. The WPF fork is also independently available at ibebbs/wpf@feature/native-aot (based on dotnet/wpf release/10.0).


Repository structure

wpfaot/
├── wpfaot/                     # Sample AOT calculator app
│   ├── wpfaot.csproj           # .NET 10, PublishAot=true, feature switches
│   ├── TrimmerRoots.xml        # Trimmer preservation descriptors
│   ├── MainWindow.xaml         # Calculator UI with {CompiledBinding}
│   ├── CalculatorViewModel.cs  # ViewModel (INPC + ICommand)
│   ├── DelegateCommand.cs      # ICommand implementation
│   └── Fonts/                  # Embedded font resources
├── wpf/                        # WPF fork (modified dotnet/wpf)
│   ├── build.cmd               # WPF build script
│   ├── Microsoft.Dotnet.Wpf.sln
│   └── src/Microsoft.DotNet.Wpf/src/
│       ├── WindowsBase/        # Feature switches, AOT-safe helpers
│       ├── PresentationCore/   # DirectWrite COM source gen, rendering
│       ├── PresentationFramework/  # CompiledBinding, XAML, themes
│       ├── System.Xaml/        # AOT-safe XAML runtime
│       ├── CompiledBinding.Generator/  # Roslyn source generator
│       └── UIAutomation*/      # UIA interfaces ([GeneratedComInterface])
├── scripts/                    # Build and diagnostic scripts
│   ├── collect-perf.ps1        # Performance benchmark script
│   ├── Capture-WpfEtw.ps1     # ETW trace capture
│   └── Run-WpfaotDiagnostics.ps1  # Render diagnostics
├── wpfaot-plan.md              # Detailed implementation plan and phase records
└── wpfaot-perf.md              # Performance comparison results

Building and running

There are three ways to build and run the sample app, depending on whether you want to use the WPF fork or the built-in framework WPF.

Option A: Framework WPF (quickest -- no fork build required)

Uses the built-in .NET 10 WPF assemblies. No fork changes are applied, but this is the fastest way to verify the app builds and publishes with AOT.

cd wpfaot
dotnet publish -c Release -r win-x64 -p:WpfFrameworkOnly=true

The published executable is at:

wpfaot\bin\Release\net10.0-windows\win-x64\publish\wpfaot.exe

Note: With framework WPF, {CompiledBinding} and the source generator are not available (they live in the fork). The app will build but bindings using {CompiledBinding} will not resolve.

Option B: Fork packaging output (recommended)

Build the WPF fork first, then build/publish the sample app against the fork's packaging output. This is the recommended workflow -- it uses all fork changes and does not require Visual Studio's MSBuild for the app build.

Step 1: Build the WPF fork

cd wpf
.\build.cmd -configuration Release -platform x64 -pack

This takes approximately 2-4 minutes on first build. The build may report "Native tools bootstrap failed" for strawberry-perl, net-framework-48-ref-assemblies, and windows-sdk-d3d-redist -- these are optional and the build will continue without them (see wpfaot-plan.md section 2.1 for details).

Build output lands in:

wpf\artifacts\packaging\Release\Microsoft.DotNet.Wpf.GitHub\lib\net10.0\

Step 2: Build and publish the sample app

cd wpfaot
dotnet build -c Release
dotnet publish -r win-x64 -c Release

The wpfaot.csproj auto-detects the fork packaging output. When it exists, the app references the fork DLLs automatically.

Step 3: Run

.\bin\Release\net10.0-windows\win-x64\publish\wpfaot.exe

You should see a dark-themed calculator window with a live clock, display, memory register (TwoWay binding), and a full button grid (ICommand binding).

Option C: ProjectReference (rebuilds fork from wpfaot)

Builds the WPF fork as part of the app build via ProjectReference. Requires Visual Studio's MSBuild (Developer Command Prompt) because the fork includes the C++/CLI DirectWriteForwarder project.

:: Open "Developer Command Prompt for VS 2022"
msbuild wpfaot\wpfaot.csproj /p:Configuration=Release /p:WpfUsePackagingOutput=false

This is slower (rebuilds the fork) but useful if you are iterating on fork changes without running build.cmd separately.


Publishing modes

The sample app can be published in three deployment modes:

Mode Command Description
Framework-dependent dotnet publish -c Release -r win-x64 --no-self-contained Smallest output; requires .NET 10 runtime installed
Self-contained dotnet publish -c Release -r win-x64 --self-contained -p:PublishAot=false Includes full .NET runtime; no install required
Native AOT dotnet publish -c Release -r win-x64 Single native exe; fastest startup; no runtime required

To collect performance comparisons across all three modes:

.\scripts\collect-perf.ps1

Results are written to wpfaot-perf.md.


What's working

The following features are fully functional in the Native AOT published app:

Feature Details
Window with themed content Aero2 theme applied correctly via targeted trimmer roots
Text rendering (system fonts) System font collection via DirectWrite works natively
Text rendering (embedded fonts) Embedded .ttf fonts via pack:// URIs; custom DWrite font loaders use [GeneratedComInterface]/[GeneratedComClass]
Compiled bindings ({CompiledBinding}) Reflection-free data binding via WPF fork markup extension + Roslyn source generator. Supports OneWay, TwoWay, and ICommand/DelegateCommand
Standard bindings ({Binding}) Works in AOT if view model types are preserved in TrimmerRoots.xml (reflection-based; {CompiledBinding} is preferred)
Button / ICommand DelegateCommand pattern with CommandParameter dispatch
TextBox (TwoWay binding) Editable TextBox with TwoWay compiled binding to ViewModel
Keyboard input and focus Keyboard focus events and routed events work; TSF/IME is disabled
Timer-driven UI updates DispatcherTimer-based clock demonstrates dynamic INPC updates

What's not working / limited

Disabled via feature switches (can be re-enabled)

These subsystems are disabled in the sample app via [FeatureSwitchDefinition] feature switches in the .csproj. When disabled, the trimmer eliminates all associated code. They can be re-enabled by setting the switch to true (but some require COM source generation work to function in AOT):

Feature Switch Status
Drag and drop System.Windows.Features.DragDrop Gated; no-op in AOT. COM source generation needed for full support
TSF / IME (input methods) System.Windows.Features.TextServices Gated; no-op in AOT. ~50 COM interfaces would need source generation
Spell checking System.Windows.Features.SpellCheck Gated; returns null. COM source generation needed
Printing System.Windows.Features.Printing Gated; limited. IStream COM source generation needed
Accessibility / UI Automation System.Windows.Features.Accessibility Phase 11 in progress: UIA interfaces converted to [GeneratedComInterface], implementation classes annotated with [GeneratedComClass]. Stages 3-6 remaining (P/Invoke updates, guard removal, testing)

To configure feature switches in your app's .csproj:

<ItemGroup>
  <!-- Disable a feature: trimmer removes all associated code -->
  <RuntimeHostConfigurationOption Include="System.Windows.Features.SpellCheck"
                                  Value="false" Trim="true" />
  <!-- Enable a feature (default): code is preserved -->
  <RuntimeHostConfigurationOption Include="System.Windows.Features.Accessibility"
                                  Value="true" Trim="true" />
</ItemGroup>

Not supported in AOT

Limitation Details
Partial-trust XAML Out of scope. AOT apps must run full trust
Dynamic assembly loading Assembly.Load by name, plugin assemblies, and GAC resolution are not supported. All assemblies must be statically referenced at build time
Taskbar / JumpList Uses COM (Type.GetTypeFromCLSID + Activator.CreateInstance); not yet gated. Will crash if used in AOT
ActiveX / PeoplePicker Uses COM; not yet gated. Will crash if used in AOT
Unbounded type resolution Only XAML types that are statically referenced by the app (or listed in TrimmerRoots.xml) are supported. Types resolved purely by string at runtime may be trimmed

Known constraints

  • Binary size: The AOT executable is ~55 MB. This is larger than ideal because WPF's BAML known types system statically references 759 types via typeof(), preventing the trimmer from removing them. Theme BAML resources also contribute ~10 MB of embedded data.
  • Trimmer roots: New WPF controls used in XAML may require additional entries in TrimmerRoots.xml for their theme style triggers (e.g., TextBox needed BooleanConverter and FrameworkAppContextSwitches).
  • {CompiledBinding} scope: Currently supports simple property paths, INotifyPropertyChanged, DependencyProperty (TwoWay), IValueConverter, OneWay/TwoWay/OneTime modes. Not yet supported: dotted paths (Address.City), collection bindings, MultiBinding, RelativeSource, ElementName, StringFormat, indexers.

Architecture notes

AOT gating pattern

The fork uses RuntimeFeature.IsDynamicCodeSupported to detect AOT at runtime. When false (Native AOT), code paths that use Reflection.Emit, Assembly.Load, or built-in COM interop are bypassed:

if (!RuntimeFeature.IsDynamicCodeSupported)
{
    // AOT path: use statically-safe alternative
    return;
}
// JIT path: original behavior

Feature switches

Feature switches use [FeatureSwitchDefinition] (introduced in .NET 8) so the ILC trimmer can substitute the property to false at compile time and eliminate all guarded code:

[FeatureSwitchDefinition("System.Windows.Features.SpellCheck")]
internal static bool IsSpellCheckSupported =>
    AppContext.TryGetSwitch("System.Windows.Features.SpellCheck", out bool v) ? v : true;

Feature switches are defined in WindowsBase\MS\Internal\WpfFeatureSwitches.cs.

Compiled bindings

The {CompiledBinding} system has two parts:

  1. Runtime (in WPF fork's PresentationFramework):

    • CompiledBindingExtension -- XAML markup extension
    • CompiledBindingRegistry -- static accessor dictionary
    • CompiledBindingInstance -- binding lifecycle manager (DataContext tracking, INPC subscription)
  2. Source generator (in CompiledBinding.Generator):

    • Roslyn IIncrementalGenerator that scans XAML AdditionalFiles
    • Finds {CompiledBinding Type={x:Type ...}, Path=...} expressions
    • Emits [ModuleInitializer] code that registers typed getter/setter lambdas

Usage in XAML (no xmlns prefix needed -- it's in the WPF default namespace):

<TextBlock Text="{CompiledBinding Type={x:Type local:MyViewModel}, Path=Title}" />
<TextBox Text="{CompiledBinding Type={x:Type local:MyViewModel}, Path=Name, Mode=TwoWay}" />
<Button Command="{CompiledBinding Type={x:Type local:MyViewModel}, Path=SaveCommand}" />

TrimmerRoots.xml

The TrimmerRoots.xml file tells the ILC trimmer which types to preserve. It contains targeted entries for:

  • App XAML root types (MainWindow, App, CalculatorViewModel, DelegateCommand)
  • Theme BAML types resolved by string at runtime ({x:Static} targets, stream types not in the known types table, internal converters)
  • Core WPF assemblies (PresentationCore, WindowsBase, DirectWriteForwarder, System.Xaml) -- included as roots to preserve metadata
  • Aero2 theme assembly -- loaded by Assembly.Load at runtime

When adding new control types to your XAML, check if their theme styles use triggers that reference types not in WPF's known types table. If the app crashes with a type resolution error, add the missing type to TrimmerRoots.xml.

Diagnostic logging

The fork includes opt-in file-based diagnostics. Set the environment variable WpfAotDiagnostics=1 before running the app:

$env:WpfAotDiagnostics = "1"
.\wpfaot.exe
# Check output: $env:TEMP\wpfaot-render.log

Or use the diagnostic script:

.\scripts\Run-WpfaotDiagnostics.ps1 -RunSeconds 6

Troubleshooting

App crashes on startup

Check the Windows Event Log for .NET Runtime errors:

Get-WinEvent -FilterHashtable @{
    LogName = 'Application'
    ProviderName = '.NET Runtime'
} -MaxEvents 5 -ErrorAction SilentlyContinue | Format-List TimeCreated, Id, Message

Also check the startup log:

Get-Content "$env:TEMP\wpfaot-startup.log"

Missing type errors at runtime

If the app crashes with ArgumentException mentioning a type that "cannot be resolved", that type needs to be added to TrimmerRoots.xml. The fork logs missing BAML types to %TEMP%\wpfaot-trimmed-types.log in AOT builds.

WPF fork build fails

  • Ensure Visual Studio 2022 is installed with the workloads from wpf\Documentation\wpf.vsconfig
  • The "Native tools bootstrap failed" warnings for strawberry-perl, net-framework-48-ref-assemblies, and windows-sdk-d3d-redist are expected and harmless -- the build continues without them
  • If DirectWriteForwarder fails, ensure the C++/CLI workload and Windows 10 SDK are installed

Binary size

Check binary size after a clean publish:

dotnet publish -r win-x64 -c Release
(Get-Item ".\bin\Release\net10.0-windows\win-x64\publish\wpfaot.exe").Length / 1MB

References

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages