-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Background
As part of CsWinRT 2.1.1, we introduced AOT support in WinRT scenarios. In order to make this happen, CsWinRT now also generates a fair amount (read: a metric ton) of code to make things such as WinRT generics work. This support comes with several drawbacks though. The first one is that it is viral: we need every single library to reference CsWinRT in order to make types AOT compatible when used for WinRT interop. This doesn't just affect libraries that are specific to Windows, but any library which contains types that will be used in WinRT scenarios. For instance, I've had to add the Windows TFM to the MVVM Toolkit, so that the types in the MVVM Toolkit could get the necessary supporting code generated. This isn't really fixable, it's just the state of things now that WinRT support is lifted.
On top of this however, we have two main problems that we'd like to solve:
- Versioning: right now CsWinRT generates all this code in all of those libraries, which won't get any new updates no matter what CsWinRT version the final application is using, until every single dependency goes and also updates CsWinRT to regenerate that code, and ships an update. This is completely not scalable. It might take months for a large project to get every single dependency to publish updated versions to gain the codegen improvements.
- Binary size: there is a ton of duplicate code across every single library you reference, because each library will generate supporting code for every possible generic type instantiation it might need, and there's no way to share this code.
For instance, here's just some of these supporting types generated in the MVVM Toolkit:
It's very easy for a large app to have even hundreds of these spread across all your dependencies and projects. And it's not uncommon that a lot of these will just be copies and copies of the same combinations of type arguments (eg. string).
We'd like improve things and move as much code generation as possible down to the published projects.
API proposal
The proposal is not an API, but an extension on top of #90081. We'd like to add a new "*App*" wildcard that can be used in place of assembly names for [UnsafeAccessor] methods. The exact semantics would be controlled via a new feature switch that can be configured via the new UnsafeAccessorOutputAssemblyResolutionType property. There would be two modes:
- "Static": in this configuration, "*App*" means:
"When using NAOT or self-contained publishing, it refers to the project being published. Otherwise, it refers to the same assembly that
Assembly.GetEntryAssemblywould return. If it would returnnull,NotSupportedExceptionis thrown."
- "Dynamic": in this configuration, "*App*" means:
"Always invoke
AssemblyLoadContext.Resolvingand use the returned assembly"
Example use
Consider the current CsWinRT code generation for some enumerable type, such as ObservableGroup<TKey, TElement>:
Generated CCW vtable (click to expand):
internal sealed class ObservableGroupedCollection_TKey__TElement_WinRTTypeDetails : IWinRTExposedTypeDetails
{
public ComWrappers.ComInterfaceEntry[] GetExposedInterfaces()
{
_ = IReadOnlyList_System_Collections_IList.Initialized;
_ = IReadOnlyList_System_ComponentModel_INotifyPropertyChanged.Initialized;
_ = IReadOnlyList_System_Collections_Specialized_INotifyCollectionChanged.Initialized;
_ = IReadOnlyList_System_Collections_IEnumerable.Initialized;
_ = IReadOnlyList_object.Initialized;
_ = IEnumerable_System_Collections_IList.Initialized;
_ = IEnumerable_System_ComponentModel_INotifyPropertyChanged.Initialized;
_ = IEnumerable_System_Collections_Specialized_INotifyCollectionChanged.Initialized;
_ = IEnumerable_System_Collections_IEnumerable.Initialized;
_ = IEnumerable_object.Initialized;
return new ComWrappers.ComInterfaceEntry[14]
{
new ComWrappers.ComInterfaceEntry
{
IID = IReadOnlyListMethods<IList>.IID,
Vtable = IReadOnlyListMethods<IList>.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = IReadOnlyListMethods<INotifyPropertyChanged>.IID,
Vtable = IReadOnlyListMethods<INotifyPropertyChanged>.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = IReadOnlyListMethods<INotifyCollectionChanged>.IID,
Vtable = IReadOnlyListMethods<INotifyCollectionChanged>.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = IReadOnlyListMethods<IEnumerable>.IID,
Vtable = IReadOnlyListMethods<IEnumerable>.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = IReadOnlyListMethods<object>.IID,
Vtable = IReadOnlyListMethods<object>.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = IEnumerableMethods<IList>.IID,
Vtable = IEnumerableMethods<IList>.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = IEnumerableMethods<INotifyPropertyChanged>.IID,
Vtable = IEnumerableMethods<INotifyPropertyChanged>.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = IEnumerableMethods<INotifyCollectionChanged>.IID,
Vtable = IEnumerableMethods<INotifyCollectionChanged>.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = IEnumerableMethods<IEnumerable>.IID,
Vtable = IEnumerableMethods<IEnumerable>.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = IEnumerableMethods<object>.IID,
Vtable = IEnumerableMethods<object>.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = IListMethods.IID,
Vtable = IListMethods.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = INotifyCollectionChangedMethods.IID,
Vtable = INotifyCollectionChangedMethods.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = INotifyPropertyChangedMethods.IID,
Vtable = INotifyPropertyChangedMethods.AbiToProjectionVftablePtr
},
new ComWrappers.ComInterfaceEntry
{
IID = IEnumerableMethods.IID,
Vtable = IEnumerableMethods.AbiToProjectionVftablePtr
}
};
}
}This, along with all of those IReadOnlyList_System_Collections_IList etc. implementations (each of those is and lot of code as well). With this proposed feature, we'd be able to not generate any of that, and simply do this instead:
[assembly: WinRTGenericTypeInstantiation(typeof(IReadOnlyList<IList>))]
[assembly: WinRTGenericTypeInstantiation(typeof(IReadOnlyList<INotifyPropertyChanged>))]
// etc.For any generic type instantiation we need. Then, replace those Initialized accesses with:
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "get_Initialized")]
[UnsafeAccessorType("WinRT.GenericHelpers.IReadOnlyList_System_Collections_IList, *App*")]
static extern bool IReadOnlyList_System_Collections_IList();And just call those, which would call the shared generated generic type instantiation in the final assembly. This support could technically us to go even further and potentially just have the entire vtable implementation call to some well known generated method in the final assembly, if we found that method to be even better for CsWinRT (either would work).
Essentially, this allows us to:
- Share all these instantiations
- Minimize the generated code that has to be updated in all dependencies
- Keep everything fully trimmable when in self-contained mode
- Still work in ALC scenarios
- Be predictable (the behavior depends on the feature switch, so it's consistent)
Additional information
This design includes suggestions from @jkoritzinsky, who pointed out that in several scenarios around ALC, there is no "entry assembly", or you might have a given dependency library being loaded in one ALC and then referenced by some other code in another ALC. In those cases, the "Dynamic" mode would allow eg. CsWinRT to implement an appropriate resolver callback.
