Skip to content

Latest commit

 

History

History
145 lines (103 loc) · 13.6 KB

File metadata and controls

145 lines (103 loc) · 13.6 KB

AOT support matrix

Microsoft.UI.Reactor (Reactor)'s core framework targets .NET native AOT. The repo sets IsAotCompatible=true and treats the IL2*/IL3* analyzer warnings as errors on the core library, so trimming/AOT regressions can't merge silently. The tests/stress_perf/StressPerf.Reactor benchmark harness publishes Reactor with PublishAot=true on every CI build that runs perf — that's the canary for the framework's AOT viability.

But not every subsystem is AOT-clean. Some features rely on reflection in ways that can't yet be expressed without a source generator, and they sit behind unconditional trim suppressions. The suppressions silence the analyzer; at runtime, the affected code paths will throw under AOT if they're invoked.

This page enumerates what works, what doesn't, and what's planned. If you publish your app with PublishAot=true, you're responsible for staying inside the green column.

What works under AOT

  • Core reconciler — virtual element tree, diffing, mount/update, keyed reconciliation, element pooling.
  • DSL & elements — factory methods, fluent modifiers.
  • HooksUseState, UseReducer, UseEffect, UseMemo, UseRef, UseCallback. (See note on UseObservable below.)
  • Flex layout — Yoga port is pure C#, no reflection.
  • Charting (D3) — algorithm port is pure C#.
  • Markdown — md4c-backed parser and renderer.
  • Commanding — command records, focus-scoped accelerators.
  • Animation — compositor-layer transitions, keyframes.
  • ThemingThemeRef tokens, style caching. Brushes resolved through XamlControlsResources work under AOT once the WindowsAppSDK#6394 publish workaround is in place (see Required publish-time workarounds below).
  • Built-in WinUI controlsNavigationView, TabView, Pivot, TreeView, Expander, InfoBar, MenuBar, BreadcrumbBar, RefreshContainer, TeachingTip, ColorPicker, NumberBox, RatingControl, SplitButton, etc. all mount and update under AOT once WindowsAppSDK#6394 is mitigated. {TemplateBinding} against built-in DPs works; private DPs declared in a separate third-party assembly without an IXamlMetadataProvider are still broken (Issue #142).
  • Markdown / Localization (read path)IStringLoc/Loc.X.Y lookups generated by Reactor.Localization.Generator are AOT-clean. Source-generated, no runtime reflection.

What does not work under AOT (today)

These subsystems compile cleanly with IsAotCompatible=true (the warnings are suppressed), but the suppressions cover real reflection that will throw at runtime under PublishAot=true. Don't rely on them in an AOT-published app until they're rebuilt on source generators.

Subsystem What breaks Tracking
Devtools / MCP server Assembly.GetTypes for component discovery, reflection-based hook inspection in DevtoolsStateTool, reflection-based method invocation in DevtoolsFireTool, DependencyProperty enumeration in DevtoolsPropertyTools, JsonSerializer for MCP request/response. The whole devtools surface (/--devtools, /--mcp-stdio) is reflection-driven. Issue #70
PropertyGrid auto-discovery TypeRegistry.Resolve, ReflectionTypeMetadataProvider walk public properties, build init-only setters, and instantiate editors via Activator.CreateInstance. Manually-registered metadata is fine; auto-discovery from a runtime type is not. Issue #70
PropertyGrid array editor Array.CreateInstance is annotated RequiresDynamicCode. Array-typed property editors won't work AOT. Issue #70
DataGrid AutoColumns<T> Reflects over T's public properties via TypeRegistry. The T parameter is annotated [DynamicallyAccessedMembers(PublicProperties)] so the trimmer keeps the members, but AutoColumns ultimately funnels into TypeRegistry.Resolve, which is RequiresUnreferencedCode. Use explicit Column<T,V>(…) definitions instead. Issue #70
UseObservable on POCOs ObservableTreeTracker walks public properties via reflection to subscribe to INPC. Observables built explicitly (Observable<T>, IObservableCollection) are fine; the implicit-INPC path is not. Issue #70
Form validation FormField's default editor resolution goes through TypeRegistry. Same caveat as PropertyGrid. Issue #70
Navigation state JSON NavigationHandle serializes deep-link state via JsonSerializer without a source-generated context. Custom types that ride through navigation state will fail to serialize under AOT. Issue #70
Component discovery (ReactorApp.Run<TApp> reflection paths) The instantiation of TApp itself is annotated and works. The devtools-only --list-components enumeration scans Assembly.GetTypes; that path is gated to non-AOT builds. Issue #70
Theme resource lookup (Theme.X, ThemeRef.Resolve) Works once the WindowsAppSDK#6394 workaround target ships the project .pri into the publish output (see Required publish-time workarounds). Reactor's library itself is AOT-clean here. WindowsAppSDK#6394
Third-party-assembly XAML metadata (Issue #142 fixtures) Built-in WinUI controls work under AOT now (see Required publish-time workarounds). What remains is {TemplateBinding} against private DPs in a third-party assembly that ships no .xaml file: the XAML compiler only emits an IXamlMetadataProvider for projects that have at least one .xaml, and AOT trims any implicit metadata path. Affected fixtures: Issue142_CustomControlPrivateDp_Renders, Issue142_ThirdPartyControlPrivateDp_Renders. The library-author workaround is to register a hand-written IXamlMetadataProvider via RegisterControlAssembly. Issue #142

Conventions

  • The library compiles AOT-clean. Builds of Reactor.csproj produce zero IL2*/IL3* warnings. New code that reaches for reflection must either be source-generated, annotated with DynamicallyAccessedMembers, or — as a last resort — gated behind [RequiresUnreferencedCode] / [RequiresDynamicCode] so consumers see the warning at the call site.
  • Suppressions are temporary. Every [UnconditionalSuppressMessage("Trimming", ...)] or ("AOT", ...) in this repo is a TODO. The justification field names the reflection use; tracking is folded into issue #70.
  • The benchmark canary. tests/stress_perf/StressPerf.Reactor (and the StressPerf.Direct/ReactorGrid siblings) set PublishAot=true. If they stop publishing, an AOT regression has landed in the framework.
  • The runtime canary. CI's AOT Selftests job (.github/workflows/ci.yml) publishes tests/Reactor.AppTests.Host with PublishAotInternal=true and runs the full selftest suite against the NativeAOT binary on every PR. SelfTestRunner.DefaultAotSkipPatterns mutes the known reflection-bound fixtures (Devtools/MCP, PropertyGrid auto-discovery, Issue142 XAML metadata, plus the two framework cases under investigation); any new failure fails the job. If you intentionally need to add a skip, document the bucket in the comment above DefaultAotSkipPatterns and re-probe with tests/Reactor.AppTests.Host/probe-aot-skips.ps1.

Required publish-time workarounds

WindowsAppSDK#6394 — project .pri (and .xbf) missing from publish output

When publishing an unpackaged WinUI 3 app (WindowsPackageType=None) with PublishAot=true, the WindowsAppSDK build pipeline generates $(AssemblyName).pri into the intermediate output but does not copy it into the publish directory. The MakePRI step is conditioned on AppxPackage=true, so unpackaged apps fall through. Same applies to .xbf files generated by the XAML compiler.

The missing .pri has cascading runtime symptoms that look like separate bugs but all trace back to the same cause:

  • TabView's constructor throws FileNotFoundException from ResourceAccessor::GetLocalizedStringResource(SR_TabViewCloseButtonTooltipWithKA).
  • NavigationView mounts but its template-apply throws the same FileNotFoundException (Reactor's error boundary catches it and renders an error TextBlock).
  • Application.Current.Resources loads as an empty dictionary (0 merged, 0 themed, 0 keys) because Application.LoadComponent cascades through MRT for type-info lookups.
  • Every ThemeRef.Resolve(...) returns null; .Foreground(Theme.X) falls back to control defaults.

Upstream tracking: microsoft/WindowsAppSDK#6394 — still OPEN against 1.8 and 2.0.

Workaround (in tests/Reactor.AppTests.Host/Reactor.AppTests.Host.csproj):

<Target Name="_CopyWinUIResourcesForAot" AfterTargets="Publish"
        Condition="'$(PublishAot)' == 'true' and '$(AppxPackage)' != 'true'">
  <ItemGroup>
    <_WinUIResourcesForAot Include="$(OutputPath)**\*.xbf" />
    <_WinUIResourcesForAot Include="$(OutputPath)$(AssemblyName).pri"
                           Condition="Exists('$(OutputPath)$(AssemblyName).pri')" />
  </ItemGroup>
  <Copy SourceFiles="@(_WinUIResourcesForAot)"
        DestinationFiles="@(_WinUIResourcesForAot->'$(PublishDir)%(RecursiveDir)%(Filename)%(Extension)')"
        SkipUnchangedFiles="true" />
</Target>

Any Reactor app that publishes with PublishAot=true and WindowsPackageType=None should copy this target verbatim until WindowsAppSDK ships a fix. Remove the target once #6394 closes.

Debugging an AOT selftest hang

tests/Reactor.AppTests.Host maintains an explicit allow-list of fixtures that hang, crash, or assert-fail under NativeAOT (SelfTestRunner.DefaultAotSkipPatterns). When you remove an entry from that list and the published Host hangs, crashes, or asserts instead of producing output, use the following workflow.

Failure mode at a glance

Probing each DefaultAotSkipPatterns entry in isolation (via the tests/Reactor.AppTests.Host/probe-aot-skips.ps1 helper) reveals three buckets. Pick the matching workflow:

Bucket Symptom Debug step
Hang (dispatcher starvation) Fixture's RunAsync() synchronously blocks the UI thread; the in-band 15 s Task.Delay watchdog cannot fire because the dispatcher isn't pumping. Off-dispatcher hang watchdog (60 s default, configurable via REACTOR_SELFTEST_HANG_TIMEOUT_SECONDS) writes Bail out! HANG_DETECTED: <fixture> … to stdout + stderr, flushes, then Environment.FailFast. With DOTNET_DbgEnableMiniDump=1 set, this produces a Watson minidump.
Native crash Process exits with 0xC0000409 (STATUS_STACK_BUFFER_OVERRUN) — the AOT runtime's FailFast for unhandled managed exceptions. No # Total failures: line because the process terminated abruptly. Set DOTNET_DbgEnableMiniDump=1 (and COMPlus_DbgEnableMiniDump=1 — both, matching DevtoolsStressE2ERunner) before launching. Open the resulting .dmp with dotnet-dump analyze or WinDbg; look at the dispatcher (UI) thread's stack for the throwing call.
Assertion failure TAP output already shows not ok <name> - <reason>; process exits 1 cleanly. No special tooling needed — read the TAP failure line. The fixture and check name are in the message.

Parent attribution

The MSTest harness (Reactor.SelfTests.SelfTestBatch) parses the HANG_DETECTED signal from both stdout and stderr, and on process timeout falls back to the last # Running: line. Either way, the failure surfaces against the named fixture in the failing test's detail with a copy-pasteable repro command. It does not cascade through _initError (which would mark every unrelated fixture failed).

Capturing a dump

Set these env vars before launching the Host:

DOTNET_DbgEnableMiniDump=1
DOTNET_DbgMiniDumpType=2
DOTNET_DbgMiniDumpName=%TEMP%\reactor-selftest-%p.dmp
COMPlus_DbgEnableMiniDump=1
COMPlus_DbgMiniDumpType=2
COMPlus_DbgMiniDumpName=%TEMP%\reactor-selftest-%p.dmp

Both Environment.FailFast (hang path) and the AOT runtime's unhandled-exception fast-fail (crash path) honour these vars.

Isolated repro

Once you have the offending fixture name, repro it standalone against the AOT-published binary:

dotnet publish tests/Reactor.AppTests.Host -p:PublishAotInternal=true -p:Platform=x64 -r win-x64 -c Release
$env:DOTNET_DbgEnableMiniDump=1
$env:DOTNET_DbgMiniDumpName="$env:TEMP\reactor-hang-%p.dmp"
& "<publish-dir>\Reactor.AppTests.Host.exe" --self-test --no-aot-skip --filter <FixtureName>

--no-aot-skip bypasses the entire skip list so the targeted fixture actually runs. To drive the MSTest harness against the AOT-published binary, point it at the publish output:

$env:REACTOR_SELFTEST_HOST_EXE="<publish-dir>\Reactor.AppTests.Host.exe"
dotnet test tests/Reactor.SelfTests

Disabling the watchdog while debugging

When stepping through a fixture in a debugger, set REACTOR_SELFTEST_HANG_TIMEOUT_SECONDS=0 to suppress the hang watchdog entirely. (It also auto-disables whenever Debugger.IsAttached returns true at poll time.)

Categorising the skip list (tests/Reactor.AppTests.Host/probe-aot-skips.ps1)

The repo includes a probe script that runs every DefaultAotSkipPatterns entry in isolation under --no-aot-skip and writes a CSV summarising whether each one passes, hangs, crashes natively, or assert-fails. Use it after AOT framework changes to find stale skips that have started passing, and to triage what's still broken.

pwsh -NoProfile -File tests\Reactor.AppTests.Host\probe-aot-skips.ps1

When in doubt

If you need a feature listed in the "does not work" table and you're publishing AOT, file an issue against #70 with your scenario. The fix in most cases is a source generator pass; what gets prioritized is driven by who's hitting the wall.