Skip to content

AOT optimizer emits unqualified generic type arguments in GlobalVtableLookup, so the entries never match Type.ToString() #2536

Description

@FrayxRulez

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions