dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.47k stars 4.76k forks source link

Expand ILLink/ILC support for 'eliminate dead branches around typeof comparisons' to work on internal types too #110300

Open Sergio0694 opened 11 hours ago

Sergio0694 commented 11 hours ago

Overview

This is a follow up to #102248 which primarily affects CsWinRT. In order to support marshalling types we don't own (meaning we cannot attach a vtable to them via an attribute), the AOT generators in CsWinRT also generate a global lookup table with all necessary vtables for types it has seen as possibly used across the ABI boundary, in a given project. This looks something like this:

internal static class GlobalVtableLookup
{
    [ModuleInitializer]
    internal static void InitializeGlobalVtableLookup()
    {
        ComWrappersSupport.RegisterTypeComInterfaceEntriesLookup(LookupVtableEntries);
        ComWrappersSupport.RegisterTypeRuntimeClassNameLookup(LookupRuntimeClassName);
    }

    private static ComWrappers.ComInterfaceEntry[] LookupVtableEntries(System.Type type)
    {
        switch (type.ToString())
        {
        case "System.Collections.Generic.Dictionary`2[System.String,System.String]":
        case "System.Collections.ObjectModel.ReadOnlyDictionary`2[System.String,System.String]":
                // Bunch of initialization...
                return TheVTableForThisType();
        case "System.ComponentModel.DataAnnotations.ValidationResult[]":
                // Bunch of initialization...
                return TheVTableForThisType();
        case "ABI.System.Collections.Generic.ToAbiEnumeratorAdapter`1[System.Collections.IList]":
                // Bunch of initialization...
                return TheVTableForThisType();
        // ...
        default:
               return null;
        }
    }

    // 'LookupRuntimeClassName' here, which is kinda similar but returns type names instead
}

This works just fine, but we noticed the linker isn't able to remove all branches for types that are not constructed.

Repro steps

  1. Create a blank .NET 9 project like this:
    <Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0-windows10.0.17763.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>true</InvariantGlobalization>
    <CsWinRTAotWarningLevel>2</CsWinRTAotWarningLevel>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <IlcGenerateMstatFile>true</IlcGenerateMstatFile>
    <IlcGenerateDgmlFile>true</IlcGenerateDgmlFile>
    </PropertyGroup>
    <ItemGroup>
    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0-preview3" />
    </ItemGroup>
    </Project>
  2. Paste this code:
    
    using CommunityToolkit.Mvvm.ComponentModel;

Console.WriteLine(new MyViewModel());

public sealed partial class MyViewModel : ObservableObject { }


3. Publish with `dotnet publish -r win-x64 .\ConsoleApp13.csproj`

Here's what we see in sizoscope:

![Image](https://github.com/user-attachments/assets/66e7c9cd-9bc5-4056-9a98-bcb61d6a4151)

That is, sizoscope isn't able to trim branches for types not constructed when checked via the type name.

> [!NOTE]
> We cannot do `GetType() == typeof()` checks, because that would not work with internal types (which we also need to handle).

### Proposal

We need a generalized way for this to work in such a way that we can also leverage this for internal types. For instance, either:
- Making ILLink/ILC also trim branches just comparing the fully qualified type name
- Making ILLink/ILC support this via some magic intrinsic (eg. `GetType() == Type.GetType("The.Type.Name")`
- Something else..?

To clarify, the issue is internal types _from other assemblies_, ie. such that we can't just do `typeof(TheType)` in code.

cc. @MichalStrehovsky
dotnet-policy-service[bot] commented 11 hours ago

Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas See info in area-owners.md if you want to be subscribed.

dotnet-policy-service[bot] commented 11 hours ago

Tagging subscribers to this area: @dotnet/illink See info in area-owners.md if you want to be subscribed.

dotnet-policy-service[bot] commented 11 hours ago

Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas See info in area-owners.md if you want to be subscribed.

dotnet-policy-service[bot] commented 11 hours ago

Tagging subscribers to this area: @dotnet/illink See info in area-owners.md if you want to be subscribed.

MichalStrehovsky commented 3 hours ago

Making ILLink/ILC also trim branches just comparing the fully qualified type name

Since anyone can have their own System.Type descendants that may return anything as their ToString or FullName, we'd need guarantees that this is the real deal (RuntimeType). So it would have to be something like someInstance.GetType().FullName (it cannot be a System.Type that we obtained from who-knows-where). Would such restriction work?

Making ILLink/ILC support this via some magic intrinsic (eg. GetType() == Type.GetType("The.Type.Name")

We'd probably not want to introduce patterns that only perform well when rewritten. I wonder if it would be feasible to introduce an UnsafeAccessor that can do the equivalent for Type.GetType at compile time (e.g. [UnsafeAccessor(Kind.Type, Name = "The.Type.Name, Assembly")] extern Type GetTheType(); - runtime magic would rewrite this to a method that returns a typeof basically)