Skip to content

[API Proposal]: Adding unsafe methods that bypass IDisposable constraints #120753

@2A5F

Description

@2A5F

Background and motivation

For structures, making a virtual call to the boxing interface will produce a copy, which is incorrect behavior.
Introducing these unsafe APIs can bypass boxing when needed and ensure the correct behavior of the structure

public struct WithSideEffect : IDisposable
{
    public int a;

    public void Dispose()
    {
        a = 456;
        Console.WriteLine("123");
    }
}

void SomeMethodCallDispose<T>(ref T a)
{
    if (a is IDisposable b) b.Dispose();
}
// or
void SomeMethodCallDispose<T>(ref T a) where T : struct
{
    if (a is IDisposable b) b.Dispose();
}
// or
void SomeMethodCallDispose<T>(ref T a)  where T : struct
{
    ((IDisposable)a).Dispose();
}
...

var a = new WithSideEffect(); 

SomeMethodCallDispose(ref a);
if (a is IDisposable b) b.Dispose(); // or
((IDisposable)a).Dispose(); // or

// all same, copied
Console.WriteLine(a.a); // 0 

https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.constrained?view=net-9.0

The constrained prefix can also be used for invocation of interface methods on value types, because the value type method implementing the interface method can be changed using a MethodImpl. If the constrained prefix is not used, the compiler is forced to choose which of the value type's methods to bind to at compile time. Using the constrained prefix allows the MSIL to bind to the method that implements the interface method at run time, rather than at compile time.

MSIL has the ability to call interfaces directly without boxing, which C# does not expose.

API Proposal

public static partial class RuntimeHelpers
{
    // possible impl
    internal class IsDisposable <T>
    {
        public static bool Value{ get; } = typeof(IDisposable).IsAssignableFrom(typeof(T));
    }

    [Intrinsic]
    public static bool IsDisposable<T>() => IsDisposable<T>.Value;
}

public static unsafe partial class Unsafe
{
    public static void TryDispose<T>(ref T value)
    {
        if (!RuntimeHelpers.IsDisposable<T>) return;
        Dispose(ref value);
    }

    [Intrinsic]
    public static void Dispose<T>(ref T value)
    {
        // ldarg.0
        // constrained. !T
        // callvirt  instance void [System.Runtime]System.IDisposable::Dispose()
        // ret
    }
}

API Usage

var a = new WithSideEffect(); 
Unsafe.TryDispose(ref a); // console: 123
Console.WrteLine(a.a); // 456

Alternative Designs

No response

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions