Skip to content

Let aspects request the native code of generic instances for NativeAOT #254

Description

An aspect cannot tell the ahead-of-time compiler (NativeAOT) which closed generic types it will create by reflection at run time. A generic instance over a value type that no code instantiates has no native code, so the pattern libraries cannot create it. This issue follows #252.

Problem

[Cache] caches the result of a method that returns IEnumerable<T> or IEnumerator<T> through a value adapter, EnumerableAdapter<T> or EnumeratorAdapter<T>. ValueAdapterFactory creates the adapter at run time with MakeGenericType and Activator.CreateInstance. Measured with NativeAOT on win-x64, .NET 11 RC1:

  • Over a reference type, the generic instance can be created, because all reference types share one canonical code, and the annotation of the registration keeps the constructor.
  • Over a value type, it can be created only when some code instantiates it. Otherwise Activator.CreateInstance throws a NotSupportedException. Since Support the pattern libraries in trimmed and NativeAOT applications #252, the value adapter reports this as an error rather than caching the enumerable itself, so [Cache] on a method that returns IEnumerable<int> is not supported under NativeAOT.

The formatters of [Log] and of the cache keys have the same limit (CollectionFormatter<TRole, T>, NullableFormatter<TRole, T>). Since #252 they fall back to a non-generic formatter, which formats the same text, but is slower.

The return type and the parameter types are known when the method is woven. Only the aspect knows which closed types it needs at run time.

Measured in #89: DynamicDependencyAttribute and an ldtoken of the closed type do not make the ahead-of-time compiler produce the native code of the instance. Only an executable newobj in reachable code does.

Proposal

  • A new advice, InstantiateTypeAdviceInstance( Type closedType ), which an aspect returns from IAdviceProvider.ProvideAdvices. Requesting a closed generic method can come later.
  • The weaver emits a factory for each requested type in the metadata dispenser that it already generates in each assembly: a static method that calls the constructor with newobj, registered in a static Dictionary<Type, Func<object>>, as the dispenser already does for the serializers of the aspects (Support trimming and NativeAOT for woven assemblies, and test ReadyToRun #89). The same accessibility rules apply: a type that the generated code cannot access, or that has no public parameterless constructor, is reported and skipped.
  • A run-time API in PostSharp.dll returns the factory of a type from the dispensers of the loaded assemblies, for instance GeneratedInstanceFactories.TryCreate( Type, out object ).
  • The advice is a no-op unless the project is compiled for NativeAOT. Under the JIT, and in an application that is only trimmed, MakeGenericType works for every type, and the woven assembly must stay as it is today.
    • The condition is an MSBuild property, for instance PostSharpAotCompatibleWeaving, which defaults to true when PublishAot, IsAotCompatible or EnableAotAnalyzer is true, and which the project can set. An application sets PublishAot. A library never does, although its woven assembly is published in a native application, so IsAotCompatible is the signal for a library.
    • The value of the property must be an input of the weaving, so that dotnet build followed by dotnet publish -p:PublishAot=true weaves again. Otherwise the publication reuses the woven assembly of the build, without the factories, and nothing reports it.
  • The pattern libraries use it.
    • TypeExtensionFactory creates an extension with the generated factory, when there is one, before it falls back to Activator.CreateInstance.
    • [Cache] requests the value adapter of its return type, for the generic adapters that Caching registers by default.
    • The [Log] weaver requests the formatters of the types of the parameters and of the return value.

Limits

  • Only the types known at build time are covered. A generic adapter or formatter that the application registers at run time (ValueAdapters.Register( typeof(Foo<>), typeof(FooAdapter<>) )) keeps the current behavior: it works for reference types and throws for value types.
  • A library woven without the property and published in a native application lacks the factories. It fails at run time with the existing NotSupportedException. The documentation must say that a library that targets NativeAOT sets IsAotCompatible.
  • The advice and the run-time API are additions to the public API of PostSharp.dll.

— Claude for Gael

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions