Skip to content

Support the pattern libraries in trimmed and NativeAOT applications #252

Description

The pattern libraries could not be used in an application that is trimmed or compiled ahead of time (NativeAOT). This issue follows #89, which made the aspect framework (PostSharp.dll and the code that the weaver generates) work there.

Measurements before the fix

A net11.0 console application used [NotifyPropertyChanged], [Recordable], [ReaderWriterSynchronized], [Cache] with MemoryCachingBackend, and [Log] with ConsoleLoggingBackend. It was published for win-x64 with the .NET 11 RC1 SDK and the packages of the branch of #89. Under the JIT, every aspect worked.

With NativeAOT:

Aspect Result
[Log] The module initializer failed: MissingMethodException, no parameterless constructor for QueryableFormatter, which FormatterRepository.Register creates by reflection. The application could not start.
[NotifyPropertyChanged], [Recordable], [ReaderWriterSynchronized] The initializer of the aspects failed: PortableSerializationException, no serializer for WeakEventHandler<...>, which relied on the nested Serializer naming convention.
[Cache] Failed with ArgumentException: EnumerableAdapter<T> "has to inherit from IValueAdapter<T>", because the trimmer removed the interfaces of the generic definition.

ILC reported 39 warnings in the Patterns assemblies. With IsAotCompatible, the analyzers reported 107 in the net10.0 builds of Common, Aggregation, Model, Threading, Diagnostics and Caching; most of them in code that only the PostSharp compiler runs.

Fix

  • The net10.0 builds of Common, Aggregation, Model, Threading, Diagnostics and Caching set IsAotCompatible and build without any trimming or AOT warning.
  • Compile-time code is behind a feature switch. CompileTimeValidate, CompileTimeInitialize, the selectors of the pointcuts, the dependency analysis of [NotifyPropertyChanged], ThreadSafetyPolicy and the other code that only the compiler runs test the internal switch PostSharp.Patterns.IsCompileTimeCodeSupported, or require unreferenced code. PostSharp.Patterns.Common.Redist sets the switch to false, with Trim="true", in an application that is trimmed or compiled ahead of time, so the trimmer removes that code.
  • The advice factories of the advisable collections are created without reflection. AdvisableCollection<T>, AdvisableDictionary<TKey, TValue> and AdvisableHashSet<T> pass their generic arguments to a factory provider of each library, so that [Aggregatable], [Disposable], [Recordable], [Freezable] and the threading models no longer call MakeGenericType and Activator.CreateInstance.
  • Formatters and value adapters keep their constructors and interfaces. FormatterRepository<TRole>.Register(Type, Type), ValueAdapterFactory.Register(Type, Type) and the attributes that register type adapters and field rules annotate the registered type with DynamicallyAccessedMembers. Under NativeAOT, a generic formatter or value adapter over reference types is then created by reflection; one over a value type has native code only when the application instantiates it (measured). In that case a formatter is skipped for that type in favor of the next one (a non-generic collection formatter is registered under NativeAOT, so that a List<int> still gets a cache key that depends on its items), and a value adapter throws a NotSupportedException that names the type, because caching the value without its adapter would be wrong.
  • WeakEventHandler names its serializer with [Serializer]. In PostSharp.dll, ReflectionSerializerFactory now creates a generic serializer under NativeAOT instead of refusing it, which works for reference-type arguments.
  • The custom logging API annotates LogEventData.Create<T> and UnknownObjectAccessor.GetFactory<T> so that the properties of T are kept, and marks the overloads that take an object with RequiresUnreferencedCode. Logging an augmented event no longer compiles an expression for each type.
  • Under NativeAOT, two analyses that read metadata at run time are made conservative, because NativeAOT has no metadata for the fields or methods of a type that is not reflected on:
    • The type analysis of Aggregation classifies a type without an aspect as mutable. Before, it saw no field and classified it as immutable, so that [Freezable] did not advise a child AdvisableCollection<int>, and the collection of a frozen object stayed writable (measured).
    • DefaultFormatter always calls ToString, and formats the name of the type when ToString returns it. Otherwise it could miss an override and format every value of a type as the name of the type, so that two values would get the same cache key.
  • A static handler of a weak event ([WeakEvent], and the events of [NotifyPropertyChanged]) was kept through a weak reference to its MethodInfo. NativeAOT creates a new MethodInfo for each access, so the handler was collected (measured). The weak reference is now to the invocation delegate, which the event holds.
  • DefaultFormatter formats a nullable value when NullableFormatter<,> has no native code for its type. A null value threw a NullReferenceException (measured).
  • Other reflection over the types of the application is kept and documented: the properties of an object passed to the custom logging API as an object are not logged when trimming removes them, and the designer detection of Windows Forms and WPF fails silently, as these frameworks do not support trimming.

Tests

  • Tests/Core/Issues/IssueGH252 publishes an application that uses [NotifyPropertyChanged], [Recordable], [ReaderWriterSynchronized], [Freezable], [Disposable], [Cache] and [Log], with advisable collections, dictionaries and hash sets of value types, without trimming, trimmed, and with NativeAOT. It runs each program and fails on any warning IL in the publication output.
  • The unit tests of Common, Aggregation, Model, Threading, Diagnostics and Caching pass under the JIT on net10.0 and net472.
  • -p:PostSharpTestNativeAot=true publishes a Patterns unit test application with NativeAOT, with the .aot packages of xunit.v3 (Documentation/Testing.md). Measured on win-x64, .NET 10: Common 213/217, Aggregation 228/252, Model 576/599, Threading 544/577 (12 skipped), Diagnostics (console backend) 247/253 (6 skipped, no failure). The two defects above (weak static handlers, null nullable values) were found this way. The remaining failures are expected: tests of BinaryFormatter, DataContractSerializer and XmlSerializer, test helpers that use reflection, the formatting of anonymous types and of objects passed as object to the custom logging API, and the type analysis of Aggregation, which is conservative under NativeAOT. It is not a CI cell yet.

Not covered

  • Generic adapters and formatters over a value type that the application registers at run time have no native code under NativeAOT. Those of the libraries are requested by the aspects since Let aspects request the native code of generic instances for NativeAOT #254, so [Cache] on a method that returns IEnumerable<int> works.
  • MemoryCachingBackend uses System.Runtime.Caching, which reads the configuration of the application through System.Configuration, which trimming breaks: a trimmed application fails when it creates the backend. MemoryCacheBackend (PostSharp.Patterns.Caching.IMemoryCache) works.
  • JsonCachingSerializer and BinarySerializer require unreferenced and dynamic code. A JsonCachingSerializer with a source-generated JsonSerializerContext works, but its constructor still reports the warning.
  • The backends and adapters of the logging and caching libraries, PostSharp.Patterns.Diagnostics.Configuration, PostSharp.Patterns.Xaml and the Windows-specific parts of PostSharp.Patterns.Threading are not annotated.
  • Anonymous types: their properties are not formatted or logged under NativeAOT, because they have no metadata.
  • The type analysis of Aggregation classifies a type without an aspect as mutable under NativeAOT, so an immutable type that is not annotated with [Immutable] or [AssumeImmutable] is treated as mutable at run time. Inferred from the code, not measured: such an object, added at run time to a collection of a frozen or immutable object, is rejected where the JIT accepts it.
  • NativeAOT was measured on win-x64 only.

— Claude for Gael

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-Patterns-CommonAdvisable collections, threading primitivesArea-PlatformsTarget frameworks, SDKs, reference packs, languages

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions