dotnet / runtime

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

TypeLoadException in WASM with Covariance #78635

Closed kaleidocore closed 1 year ago

kaleidocore commented 1 year ago

Is there an existing issue for this?

Describe the bug

A covariant class implementing an interface that matches a covariant member cannot be resolved by WASM.

WASM throws a runtime TypeLoadException when covariance is used in certain class/interface hierarchies. The same code works fine in regular desktop .Net 6/7.

Expected Behavior

I expect WASM to handle covariance without crashing...

Steps To Reproduce

Minimal repro here: https://github.com/kaleidocore/CoCrashWASM

public class Program
{
    public static async Task Main()
    {
        try
        {
            CrashMe(); // Can't find .ctor, VTable error
            await CrashMeAsync(); // Generic parameter 0 error
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debugger.Break(); // TypeLoadException
            throw;
        }
    }

    static void CrashMe()
    {
        var x = new DerivedCrash<int>();
    }

    static async Task CrashMeAsync() => await CrashDifferent();
    static Task<DerivedCrash<int>> CrashDifferent() => throw new Exception();
}

public abstract class BaseProp
{
}

public class DerivedProp<T> : BaseProp
{
}

public interface ICrash<T>
{
    BaseProp MyProp { get; }
}

public abstract class BaseCrash
{
    public abstract BaseProp MyProp { get; }
}

public class DerivedCrash<T> : BaseCrash, ICrash<T>
{
    public override DerivedProp<T> MyProp { get; } = new();
}

The code snippet above is all that is needed to repro the issue.

Exceptions (if any)

System.TypeLoadException

---> System.TypeLoadException: Could not find method '.ctor' due to a type load error: VTable setup of type CoCrashWASM.DerivedCrash'1[T] failed assembly:CoCrashWASM.Client.dll type:DerivedCrash'1 member:(null) at CoCrashWASM.Program.Main()

And for the async path:

---> System.TypeLoadException: Failed to load generic parameter 0 at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[d2](d2& stateMachine) at CoCrashWASM.Program.CrashMeAsync() at CoCrashWASM.Program.Main()

.NET Version

7.0.100

Anything else?

.NET SDK: Version: 7.0.100 Commit: e12b7af219

Runtime Environment: OS Name: Windows OS Version: 10.0.22621 OS Platform: Windows RID: win10-x64 Base Path: C:\Program Files\dotnet\sdk\7.0.100\

Host: Version: 7.0.0 Architecture: x64 Commit: d099f075e4

.NET SDKs installed: 6.0.100 [C:\Program Files\dotnet\sdk] 6.0.108 [C:\Program Files\dotnet\sdk] 6.0.303 [C:\Program Files\dotnet\sdk] 7.0.100 [C:\Program Files\dotnet\sdk]

.NET runtimes installed: Microsoft.AspNetCore.App 6.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 6.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 6.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.8 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

Other architectures found: x86 [C:\Program Files (x86)\dotnet] registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation]

Environment variables: Not set

global.json file: Not found


Microsoft Visual Studio Professional 2022 Version 17.4.1 VisualStudio.17.Release/17.4.1+33110.190 Microsoft .NET Framework Version 4.8.09032

Installed Version: Professional

ASP.NET and Web Tools 17.4.326.54890 ASP.NET and Web Tools

Azure App Service Tools v3.0.0 17.4.326.54890 Azure App Service Tools v3.0.0

Azure Functions and Web Jobs Tools 17.4.326.54890 Azure Functions and Web Jobs Tools

C# Tools 4.4.0-6.22559.4+d7e8a398ef479a908e76bded82150c39251d0c9c C# components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Common Azure Tools 1.10 Provides common services for use by Azure Mobile Services and Microsoft Azure Tools.

Microsoft JVM Debugger 1.0 Provides support for connecting the Visual Studio debugger to JDWP compatible Java Virtual Machines

NuGet Package Manager 6.4.0 NuGet Package Manager in Visual Studio. For more information about NuGet, visit https://docs.nuget.org/

Razor (ASP.NET Core) 17.0.0.2246202+61cc048d36a3fc9246d2f04625988b19a18ab8f0 Provides languages services for ASP.NET Core Razor.

TypeScript Tools 17.0.10921.2001 TypeScript Tools for Microsoft Visual Studio

Visual Basic Tools 4.4.0-6.22559.4+d7e8a398ef479a908e76bded82150c39251d0c9c Visual Basic components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Visual F# Tools 17.4.0-beta.22512.4+525d5109e389341bb90b144c24e2ad1ceec91e7b Microsoft Visual F# Tools

Visual Studio IntelliCode 2.2 AI-assisted development for Visual Studio.

dotnet-issue-labeler[bot] commented 1 year ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

radical commented 1 year ago

With a wasmconsole template, I get:

 $ WasmAppHost --runtime-config /private/tmp/wc/bin/Debug/net7.0/browser-wasm/AppBundle/wc.runtimeconfig.json
Running: node main.mjs
Using working directory: /private/tmp/wc/bin/Debug/net7.0/browser-wasm/AppBundle
mono_wasm_runtime_ready fe00e07a-5519-4dfe-b35a-f867dbaf2e28
Hello, World! Greetings from node version: v19.1.0
Error: One or more errors occurred.
(Could not find method '.ctor' due to a type load error: VTable setup of type DerivedCrash`1[T] failed assembly:wc.dll type:DerivedCrash`1 member:(null))
    at jo (file:///private/tmp/wc/bin/Debug/net7.0/browser-wasm/AppBundle/dotnet.js:5:38027)
    at Object.Oo (file:///private/tmp/wc/bin/Debug/net7.0/browser-wasm/AppBundle/dotnet.js:5:37626)
    at _mono_wasm_marshal_promise (file:///private/tmp/wc/bin/Debug/net7.0/browser-wasm/AppBundle/dotnet.js:14:104180)
    at do_icall (wasm://wasm/009924a6:wasm-function[313]:0x1d3b4)
    at do_icall_wrapper (wasm://wasm/009924a6:wasm-function[283]:0x1c8d1)
    at interp_exec_method (wasm://wasm/009924a6:wasm-function[221]:0xdfdd)
    at interp_runtime_invoke (wasm://wasm/009924a6:wasm-function[220]:0xce8f)
    at mono_jit_runtime_invoke (wasm://wasm/009924a6:wasm-function[8112]:0x1a1fcc)
    at do_runtime_invoke (wasm://wasm/009924a6:wasm-function[2053]:0x859fe)
    at mono_runtime_try_invoke (wasm://wasm/009924a6:wasm-function[2058]:0x86066)
exiting due to exception: Error: One or more errors occurred. (Could not find method '.ctor' due to a type load error: VTable setup of type DerivedCrash`1[T] failed assembly:wc.dll type:DerivedCrash`1 member:(null))

cc @pavelsavara I see _mono_wasm_marshal_promise near top of the trace. cc @BrzVlad

ghost commented 1 year ago

Tagging subscribers to 'arch-wasm': @lewing See info in area-owners.md if you want to be subscribed.

Issue Details
### Is there an existing issue for this? - [X] I have searched the existing issues ### Describe the bug A covariant class implementing an interface that matches a covariant member cannot be resolved by WASM. WASM throws a **runtime** `TypeLoadException` when covariance is used in certain class/interface hierarchies. The same code works fine in regular desktop .Net 6/7. ### Expected Behavior I expect WASM to handle covariance without crashing... ### Steps To Reproduce Minimal repro here: https://github.com/kaleidocore/CoCrashWASM ``` public class Program { public static async Task Main() { try { CrashMe(); // Can't find .ctor, VTable error await CrashMeAsync(); // Generic parameter 0 error } catch (Exception ex) { System.Diagnostics.Debugger.Break(); // TypeLoadException throw; } } static void CrashMe() { var x = new DerivedCrash(); } static async Task CrashMeAsync() => await CrashDifferent(); static Task> CrashDifferent() => throw new Exception(); } public abstract class BaseProp { } public class DerivedProp : BaseProp { } public interface ICrash { BaseProp MyProp { get; } } public abstract class BaseCrash { public abstract BaseProp MyProp { get; } } public class DerivedCrash : BaseCrash, ICrash { public override DerivedProp MyProp { get; } = new(); } ``` The code snippet above is all that is needed to repro the issue. ### Exceptions (if any) `System.TypeLoadException ` ---> System.TypeLoadException: Could not find method '.ctor' due to a type load error: VTable setup of type CoCrashWASM.DerivedCrash'1[T] failed assembly:CoCrashWASM.Client.dll type:DerivedCrash'1 member:(null) at CoCrashWASM.Program.Main() **And for the async path:** ---> System.TypeLoadException: Failed to load generic parameter 0 at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[d__2](d__2& stateMachine) at CoCrashWASM.Program.CrashMeAsync() at CoCrashWASM.Program.Main() ### .NET Version 7.0.100 ### Anything else? .NET SDK: Version: 7.0.100 Commit: e12b7af219 Runtime Environment: OS Name: Windows OS Version: 10.0.22621 OS Platform: Windows RID: win10-x64 Base Path: C:\Program Files\dotnet\sdk\7.0.100\ Host: Version: 7.0.0 Architecture: x64 Commit: d099f075e4 .NET SDKs installed: 6.0.100 [C:\Program Files\dotnet\sdk] 6.0.108 [C:\Program Files\dotnet\sdk] 6.0.303 [C:\Program Files\dotnet\sdk] 7.0.100 [C:\Program Files\dotnet\sdk] .NET runtimes installed: Microsoft.AspNetCore.App 6.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.8 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 6.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.8 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 6.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.8 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.11 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 7.0.0 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Other architectures found: x86 [C:\Program Files (x86)\dotnet] registered at [HKLM\SOFTWARE\dotnet\Setup\InstalledVersions\x86\InstallLocation] Environment variables: Not set global.json file: Not found -------- Microsoft Visual Studio Professional 2022 Version 17.4.1 VisualStudio.17.Release/17.4.1+33110.190 Microsoft .NET Framework Version 4.8.09032 Installed Version: Professional ASP.NET and Web Tools 17.4.326.54890 ASP.NET and Web Tools Azure App Service Tools v3.0.0 17.4.326.54890 Azure App Service Tools v3.0.0 Azure Functions and Web Jobs Tools 17.4.326.54890 Azure Functions and Web Jobs Tools C# Tools 4.4.0-6.22559.4+d7e8a398ef479a908e76bded82150c39251d0c9c C# components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used. Common Azure Tools 1.10 Provides common services for use by Azure Mobile Services and Microsoft Azure Tools. Microsoft JVM Debugger 1.0 Provides support for connecting the Visual Studio debugger to JDWP compatible Java Virtual Machines NuGet Package Manager 6.4.0 NuGet Package Manager in Visual Studio. For more information about NuGet, visit https://docs.nuget.org/ Razor (ASP.NET Core) 17.0.0.2246202+61cc048d36a3fc9246d2f04625988b19a18ab8f0 Provides languages services for ASP.NET Core Razor. TypeScript Tools 17.0.10921.2001 TypeScript Tools for Microsoft Visual Studio Visual Basic Tools 4.4.0-6.22559.4+d7e8a398ef479a908e76bded82150c39251d0c9c Visual Basic components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used. Visual F# Tools 17.4.0-beta.22512.4+525d5109e389341bb90b144c24e2ad1ceec91e7b Microsoft Visual F# Tools Visual Studio IntelliCode 2.2 AI-assisted development for Visual Studio.
Author: kaleidocore
Assignees: -
Labels: `arch-wasm`, `untriaged`
Milestone: -
pavelsavara commented 1 year ago

_mono_wasm_marshal_promise

That's correct because it has async Task Main(). Or are you suggesting something else @radical ?

BrzVlad commented 1 year ago

This issue is not WASM related, I can reproduce it just fine with a desktop sample. I know Mono covariance support is not perfect. Maybe @lambdageek has some observations here.

kaleidocore commented 1 year ago

I apologize if I complicated things by bringing in an async path, it probably has nothing to with async per-se but just the fact that the DerivedCrash<T> class resolves differently when wrapped in a Task<T>. I also found I get a MissingFieldException by calling:

static IEnumerable<DerivedCrash<int>> CrashMeToo() => Enumerable.Empty<DerivedCrash<int>>();

Resulting in:

System.MissingFieldException: Field not found: !0[] .EmptyArray`1.Value Due to: Could not find field in class
   at System.Linq.Enumerable.Empty[DerivedCrash`1]()
   at CoCrashWASM.Program.CrashMeToo()
   at CoCrashWASM.Program.Main()
lambdageek commented 1 year ago

Interesting. Thanks for the bug report, @kaleidocore. It doesn't have anything to do with wasm, specifically.

I think these are all the same crash (possibly with the exception of CrashMeToo from https://github.com/dotnet/runtime/issues/78635#issuecomment-1323555329 - I can't actually repro the MissingFieldException in a console app) due to failing to set up the vtable for DerivedCrash<T> which leads to a failed class which then manifests in different ways, depending on how we observe the failed class.

Not sure yet about the underlying issue. This is covariant returns, not interface covariance, btw. Which should be much closer to coreclr's behavior.

lambdageek commented 1 year ago

The ICrash and DerivedCrash types don't have to be generic to get CrashMe to crash. (although DerivedCrash<> has to be generic to get CrashMeAsync and CrashMeToo to crash).

The weird thing that this example does is that BaseCrash doesn't implement ICrash, which is different from most covariant return examples. Changing public abstract class BaseCrash { ... } to public abstract class BaseCrash : ICrash { ... } makes all the cases work.

public class Program
{
    public static async Task Main()
    {
        // Each of these crashes
        CrashMe(); // Can't find .ctor, VTable error
        //await CrashMeAsync(); // Generic parameter 0 error
        //CrashMeToo(); // also Generic parameter 0 error
    }

    static void CrashMe()
    {
        ICrash x = new DerivedCrash<int>();
        x.MyMeth ();
    }

    static async Task CrashMeAsync() => await CrashDifferent();
    static Task<DerivedCrash<int>> CrashDifferent() => throw new Exception();

    static IEnumerable<DerivedCrash<int>> CrashMeToo() => Enumerable.Empty<DerivedCrash<int>>();

}

public abstract class BaseProp
{
}

public class DerivedProp : BaseProp
{
}

public interface ICrash
{
    BaseProp MyMeth();
}

public abstract class BaseCrash
{
    public abstract BaseProp MyMeth ();
}

// The CrashMe crash happens even if this class is just DerivedCrash (without a <T>).
// The CrashMeAsync and CrashMeToo seem to work (because the instantiated generic param of Task<> or IEnumerable<> isn't used?)
public class DerivedCrash<T> : BaseCrash, ICrash
{
    public override DerivedProp MyMeth () => new();
}
lambdageek commented 1 year ago

@davidwrighton why does this example work on coreclr? In the IL I see:

  .class public auto ansi beforefieldinit DerivedCrash`1<T>
    extends BaseCrash
    implements ICrash  {
    .param type T
    .custom instance void class System.Runtime.CompilerServices.NullableAttribute::'.ctor'(unsigned int8) =  (01 00 02 00 00 ) // .....

    // method line 18
    .method public virtual hidebysig newslot 
           instance default class DerivedProp MyMeth ()  cil managed 
    {
        .custom instance void class System.Runtime.CompilerServices.NullableContextAttribute::'.ctor'(unsigned int8) =  (01 00 01 00 00 ) // .....

        .custom instance void [System.Runtime]System.Runtime.CompilerServices.PreserveBaseOverridesAttribute::.ctor() =  (01 00 00 00 ) // ....

        // Method begins at RVA 0x2186
    .override class BaseCrash::MyMeth
    // Code size 6 (0x6)
    .maxstack 8
    IL_0000:  newobj instance void class DerivedProp::'.ctor'()
    IL_0005:  ret 
    } // end of method DerivedCrash`1::MyMeth

    // method line 19
    .method public hidebysig specialname rtspecialname 
           instance default void '.ctor' ()  cil managed 
    {
        // Method begins at RVA 0x218d
    // Code size 8 (0x8)
    .maxstack 8
    IL_0000:  ldarg.0 
    IL_0001:  call instance void class BaseCrash::'.ctor'()
    IL_0006:  nop 
    IL_0007:  ret 
    } // end of method DerivedCrash`1::.ctor

  } // end of class DerivedCrash`1

which looks like DerivedCrash<T> implements MyMeth of ICrash implicitly, since the explicit override is just for BaseCrash (which doesn't implement the interface ICrash at all). which doesn't match my intuition about covariant returns based on https://github.com/davidwrighton/runtime/blob/main/docs/design/specs/Ecma-335-Augments.md#covariant-return-types

davidwrighton commented 1 year ago

I'll take a look on Monday

davidwrighton commented 1 year ago

@lambdageek, the behavior of CoreCLR in this case is correct.

The relevant facts as I see them.

  1. BaseCrash defines a abstract method BaseType MyMeth().
  2. DerivedCrash<T> declares that it implements ICrash This is an "explicit interface implementation"
  3. DerivedCrash<T> overrides the virtual method BaseType BaseCrash.MyMeth() with DerivedProp DerivedCrash<T>.MyMeth()

One of the somewhat obscure rules of how interface methods are invoked is that an interface method maps to a virtual method declaration, and then that virtual method is resolved to find an actual implementation. These are separate steps in the logical sense, and in the implementation of CoreCLR are actually implemented as a 2 pass algorithm first considering the interface method to virtual method declaration, followed by the virtual method declaration to virtual method implementation steps.

Another of the obscure rules is that implicit interface implementation will walk through to parent types to find implicit implementations. This is a backwards walk through the list of virtual methods implemented on the types.

So, what happens here is that when searching for implicit interface implementations of ICrash on DerivedCrash<T> , we find BaseType BaseCrash.MyMeth(). Then when actually calling the interface method BaseType ICrash.MyMeth() on some instance of DerivedCrash<T>, it is resolved to BaseType BaseCrash.MyMeth(), then we resolve that method to find its implementation, and find DerivedProp DerivedCrash<T>.MyMeth()

lambdageek commented 1 year ago

One of the somewhat obscure rules of how interface methods are invoked is that an interface method maps to a virtual method declaration, and then that virtual method is resolved to find an actual implementation. These are separate steps in the logical sense, and in the implementation of CoreCLR are actually implemented as a 2 pass algorithm first considering the interface method to virtual method declaration, followed by the virtual method declaration to virtual method implementation steps.

Got it. Thanks!

lambdageek commented 1 year ago

Update these notes are wrong. I misunderstood where the problem was in Mono. The issue is not that mono wasn't considering covariant returns during the first pass. it's that we weren't considering parent type's (abstract, but also non-abstract virtual) methods during the first pass.


(Just taking notes). Mono actually does two passes to resolve interface methods, too. I just didn't understand that this was a correctness requirement, not an implementation choice. It looks like the issue is that we only consider covariant returns in the first pass (matching an interface method to a virtual method) only when there is an explicit override. otherwise the second pass (matching a virtual method implementation to a virtual method declaration) that considers covariant returns. The first pass for explicit interface implementations (class explicitly declares it implements an interface) that have an implicit method implementation (ie without an overrride table row) only does exact signature matches. This is likely an oversight on my part from when we did the initial covariant returns support in Mono.

lambdageek commented 1 year ago

/cc @naricc