dotnet / runtime

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

System.TypeLoadException When Interface Type Contains Static Field/Property for Derived Struct Type #104511

Open AndrewDRX opened 2 months ago

AndrewDRX commented 2 months ago

Description

A System.TypeLoadException exception is thrown when an interface contains either a static field or property with an initial value for a derived struct type. This works for derived record and class types.

Reproduction Steps

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
</Project>
public interface IExample
{
  public static Example DefaultExample { get; } = new();
}
public struct Example : IExample { }
var example = IExample.DefaultExample;
Unhandled exception. System.TypeLoadException: Could not load type 'Example' from assembly 'test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.
   at Program.<Main>$(String[] args)

Expected behavior

Behavior should be consistent for where an interface can be statically aware of derived types.

Actual behavior

Behavior is inconsistent for where an interface can be statically aware of derived types.

Regression?

No response

Known Workarounds

See "Other Information" section.

Configuration

No response

Other information

If the struct type is changed to a record or class type then the error is not seen.

E.g.

public record Example : IExample { }

Or

public class Example : IExample { }

If the static property or field is changed from having an initial value to instead be a property with a getter or an expression-bodied property then the error is not seen.

E.g.

public static Example DefaultExample { get => new(); }

Or

public static Example DefaultExample => new();
steveisok commented 2 months ago

@EgorBo it seems an exception is being thrown from https://github.com/dotnet/runtime/blob/e733c2f22710b6382c9aadb8139833fbf395c332/src/coreclr/vm/prestub.cpp#L939

Perhaps this is a JIT issue?

steveisok commented 2 months ago

likely related to https://github.com/dotnet/runtime/issues/100077, https://github.com/dotnet/runtime/issues/88030, and https://github.com/dotnet/runtime/issues/100950

lambdageek commented 2 months ago

Mono does something funny here too. If you run with the JIT it prints:

Unhandled Exception:
System.TypeLoadException: Recursive type definition detected .IExample
[ERROR] FATAL UNHANDLED EXCEPTION: System.TypeLoadException: Recursive type definition detected .IExample

If you run with the interpreter, it works.

using System;

public interface IExample
{
  public static Example DefaultExample { get; } = new();
}

public struct Example : IExample { }

public class Program
{
        public static void Main()
        {
                var example = IExample.DefaultExample;
                Console.WriteLine(example.GetType());
        }
}
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <UseMonoRuntime>true</UseMonoRuntime>
    <SelfContained>true</SelfContained>
  </PropertyGroup>

</Project>
% dotnet run

Unhandled Exception:
System.TypeLoadException: Recursive type definition detected .IExample
[ERROR] FATAL UNHANDLED EXCEPTION: System.TypeLoadException: Recursive type definition detected .IExample

% MONO_ENV_OPTIONS=--interp dotnet run
Example
lambdageek commented 2 months ago

Actually if you change it to

public interface IExample
{
        public static Example DefaultExample { get; } = default(Example);
}

That's a TLE in CoreCLR (and Mono JIT) too - and in that case we don't even have a cctor in IExample

AndrewDRX commented 2 months ago

likely related to #100077, #88030, and #100950

It does look similar. I didn't find those beforehand when searching for existing issues to comment on because my search was focused on interface-derived types, whereas those are for a self-referencing struct.

I will leave this issue open for the time being until there is further confirmation that this is indeed a duplicate.

steveisok commented 1 month ago

The good news is that we know the problem is a recursive type loading issue and that if we defer the type being fully loaded to a second pass, then it'll likely start working. The bad news is that it's too late in the cycle to take such a change as it has the potential to create other type loading issues. That's pretty risky and so we'll try to fix this early on in the .NET 10 cycle.

@AndrewDRX my advice is to continue to use the workaround that you posted. Thanks for raising this issue 👍

JakeYallop commented 1 month ago

Just wanted to flag one of the cases raised in one of the duplicate issues, in case its helpful. This isn't unique to interfaces:

sharplab

using System;
using System.Collections.Immutable;

Console.WriteLine(new MyStruct());

public struct MyStruct
{
    static ImmutableArray<MyStruct> One;
}

For this case, the workaround isn't really suitable (as there could be a fair bit of data that we don't want to recreate every time). Specifically, ImmutableArray<MyStruct> causes the issue. Replacing that with other collection types (e.g MyStruct[], HashSet<MyStruct>) allows the code to work (which is the workaround to use for this situation).

steveisok commented 1 month ago

@JakeYallop thanks! We won't lose sight of what's in the duplicate issues as test cases to help validate the fixes.