Summary
The AOT optimizer's generated GlobalVtableLookup compares type.ToString() against string literals, but it emits those literals using a display name for generic type arguments rather than the metadata name. When the argument's namespace is imported with a using at the instantiation site, the emitted key is unqualified and can therefore never match — the lookup silently misses, the object gets no vtable entries, and the call fails at runtime.
Configuration
Microsoft.Windows.CsWinRT 2.3.1 (generated header says cswinrt.exe version 2.3.1.260716.1)
- .NET 10,
UseUwp=true, NativeAOT, CsWinRTAotWarningLevel=3
What is generated
private static ComWrappers.ComInterfaceEntry[] LookupVtableEntries(Type type)
{
string typeName = type.ToString();
if (typeName == "..."
Type.ToString() for a constructed generic always yields fully-qualified arguments, e.g.
Telegram.Collections.SortedObservableCollection1[Telegram.Td.Api.ConnectedWebsite]`.
But the emitted literals are not consistently qualified. From one build of our app:
"Telegram.Collections.SortedObservableCollection`1[ChatMember]"
"Telegram.Collections.SortedObservableCollection`1[ConnectedWebsite]"
"Telegram.Collections.SortedObservableCollection`1[GroupCallMessage]"
"Telegram.Collections.SortedObservableCollection`1[Telegram.Td.Api.GroupCallMessage]"
"Telegram.Collections.SortedObservableCollection`1[Telegram.Td.Api.User]"
The GroupCallMessage pair is the whole bug in two lines: the same constructed type registered twice,
unqualified by the optimizer's own discovery and qualified by an explicit
[assembly: GeneratedWinRTExposedExternalType(typeof(...))]. Only the second one ever matches.
A single entry can even mix the two, which shows the string is a display name rather than a metadata name:
"System.Collections.Generic.Dictionary`2[System.Type,Animation]"
The instantiation site is new SortedObservableCollection<ConnectedWebsite>(...) in a file that has
using Telegram.Td.Api;. Writing the argument fully qualified at that site, or declaring the type via
GeneratedWinRTExposedExternalType, produces a matching entry.
Impact
The failure is silent at build time and only shows up when the object crosses the ABI — for us, assigning
such a collection to ItemsSource, which surfaces as E_INVALIDARG (and on a DispatcherQueue, as a
fail-fast rather than a catchable exception). Nothing in the build output indicates the entry is dead.
In our app 38 of 804 generic registrations are in this state; after discarding open generics and tuple
artefacts, 10 are real instantiations that are reachable at runtime.
Suggested fix
Emit the type-argument names the same way Type.ToString() renders them — the metadata name, unaffected
by using directives at the instantiation site — so that the generated comparison can match. Comparing on
something less brittle than a formatted string (a RuntimeTypeHandle, or Type equality) would remove the
class of bug entirely.
Detecting it in an existing app
Scanning the generated WinRTGlobalVtableLookup.g.cs for registrations that exist only with an
unqualified type argument — i.e. no fully-qualified sibling for the same constructed type — finds them
mechanically, and every hit is a real one.
Summary
The AOT optimizer's generated
GlobalVtableLookupcomparestype.ToString()against string literals, but it emits those literals using a display name for generic type arguments rather than the metadata name. When the argument's namespace is imported with ausingat the instantiation site, the emitted key is unqualified and can therefore never match — the lookup silently misses, the object gets no vtable entries, and the call fails at runtime.Configuration
Microsoft.Windows.CsWinRT2.3.1 (generated header sayscswinrt.exe version 2.3.1.260716.1)UseUwp=true, NativeAOT,CsWinRTAotWarningLevel=3What is generated
Type.ToString()for a constructed generic always yields fully-qualified arguments, e.g.Telegram.Collections.SortedObservableCollection1[Telegram.Td.Api.ConnectedWebsite]`.But the emitted literals are not consistently qualified. From one build of our app:
The
GroupCallMessagepair is the whole bug in two lines: the same constructed type registered twice,unqualified by the optimizer's own discovery and qualified by an explicit
[assembly: GeneratedWinRTExposedExternalType(typeof(...))]. Only the second one ever matches.A single entry can even mix the two, which shows the string is a display name rather than a metadata name:
The instantiation site is
new SortedObservableCollection<ConnectedWebsite>(...)in a file that hasusing Telegram.Td.Api;. Writing the argument fully qualified at that site, or declaring the type viaGeneratedWinRTExposedExternalType, produces a matching entry.Impact
The failure is silent at build time and only shows up when the object crosses the ABI — for us, assigning
such a collection to
ItemsSource, which surfaces asE_INVALIDARG(and on a DispatcherQueue, as afail-fast rather than a catchable exception). Nothing in the build output indicates the entry is dead.
In our app 38 of 804 generic registrations are in this state; after discarding open generics and tuple
artefacts, 10 are real instantiations that are reachable at runtime.
Suggested fix
Emit the type-argument names the same way
Type.ToString()renders them — the metadata name, unaffectedby
usingdirectives at the instantiation site — so that the generated comparison can match. Comparing onsomething less brittle than a formatted string (a
RuntimeTypeHandle, orTypeequality) would remove theclass of bug entirely.
Detecting it in an existing app
Scanning the generated
WinRTGlobalVtableLookup.g.csfor registrations that exist only with anunqualified type argument — i.e. no fully-qualified sibling for the same constructed type — finds them
mechanically, and every hit is a real one.