dotnet / runtime

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

UnsafeAccessor does not work for generic constructors, .NET 9 #110054

Closed mgravell closed 4 minutes ago

mgravell commented 13 hours ago

Description

This ticket suggests that this should work from 9.0 Preview 4, however this fails in 9.0 GA:

using System.Runtime.CompilerServices;

// this works fine
Console.WriteLine(Hack.CreateFoo());

// this doesn't work
Console.WriteLine(Hack.CreateBar<int>());

// this doesn't work
Bar<int> evil = (Bar<int>)RuntimeHelpers.GetUninitializedObject(typeof(Bar<int>));
Hack.HacketyHackHack(evil);
Console.WriteLine(evil);

static class Hack
{
    [UnsafeAccessor(UnsafeAccessorKind.Constructor)]
    internal static extern Foo CreateFoo();

    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = ".ctor")]
    internal extern static void HacketyHackHack<T>(Bar<T> obj);

    [UnsafeAccessor(UnsafeAccessorKind.Constructor)]
    internal static extern Bar<T> CreateBar<T>();
}
class Foo
{
    private readonly string s = ".ctor has run";
    public override string ToString() => s;
    private Foo() { }
}
class Bar<T>
{
    private readonly string s = ".ctor has run; " + typeof(T).Name;
    public override string ToString() => s;
    private Bar() { }
}

Reproduction Steps

as above

Expected behavior

it works

Actual behavior

Unhandled exception. System.InvalidProgramException: Generic type constraints do not match.
   at Hack.CreateBar[T]()
   at Program.<Main>$(String[] args) in C:\Users\marcg\source\repos\ConsoleApp4\ConsoleApp4\Program.cs:line 7

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

No response

vcsjones commented 13 hours ago

Where T is declared seems to matter. This works:

using System;
using System;
using System.Runtime.CompilerServices;

Hack<int>.CreateBar();

Bar<int> evil = (Bar<int>)RuntimeHelpers.GetUninitializedObject(typeof(Bar<int>));
Console.WriteLine(Hack<short>.CreateBar());
Hack<int>.HacketyHackHack(evil);
Console.WriteLine(evil);

static class Hack<T>
{

    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = ".ctor")]
    internal extern static void HacketyHackHack(Bar<T> obj);

    [UnsafeAccessor(UnsafeAccessorKind.Constructor)]
    internal static extern Bar<T> CreateBar();
}

class Bar<T>
{
    private readonly string s = ".ctor has run; " + typeof(T).Name;
    public override string ToString() => s;
    private Bar() { }
}
mgravell commented 9 hours ago

Useful workaround @vcsjones ! (I haven't verified, but will do tomorrow)

jkotas commented 5 hours ago

cc @AaronRobinsonMSFT

AaronRobinsonMSFT commented 1 hour ago

Useful workaround @vcsjones ! (I haven't verified, but will do tomorrow)

@mgravell This isn't a "workaround", it is how the feature works. See the following comment in the Remarks section of the documentation.

Generic parameters are supported since .NET 9. Generic parameters must match the target in form and index (that is, type parameters must be type parameters and method parameters must be method parameters).

In your example the T for Bar<> is a type parameter, but in HacketyHackHack<T>(Bar<T> obj) and CreateBar<T>(), T is a method parameter. The approach demonstrated in the documentation and in https://github.com/dotnet/runtime/issues/110054#issuecomment-2491839967 makes the T a type parameter and therefore the look-up works.

Please let me know if the documentation could be updated to help clarify things.

mgravell commented 4 minutes ago

Ok, this is reader error. There's even examples. I personally didn't find the exception cear, but I can't say that it is incorrect. Thanks for the assist.