Search code examples
c#generics.net-core

Is it possible to mask a reference with an interface without heap allocating?


Having the following two classes

interface IMask
{
    string GetName();
}

readonly struct StructSource : IMask
{
    string IMask.GetName()
    {
        throw new NotImplementedException();
    }
}

How could we pass a reference to the ValueType casted to the interface? So I don't have to propagate the generic argument down the line. Is it possible to achieve without heap allocating?

Suppose the following perfect world.

using System.Runtime.CompilerServices;

void UseValue<T>(ref T value) where T : IMask
{
    ref IMask ptr = ref Unsafe.As<T, IMask>(ref value);
    UseInterfaceReference(in ptr);
}
void UseInterfaceReference(in IMask target)
{
    target.GetName();
}

but that does not work properly, the reference gets nullified possibly due to type inconsistency.


Solution

  • How could we pass a reference to the ValueType casted to the interface? So I don't have to propagate the generic argument down the line. Is it possible to achieve without heap allocating?

    I think what you mean is:

    Can I just use one generic method (UseValue<T>) which is constrained to T:IMask as entry point, so that I can make nonboxing IMask interface calls in other nonconstrained nongeneric methods down the line?

    The answer is NO.

    You can see why really if you had two versions of UseValue that just printed the result (without ref for simplification)

    void UseValueNonGeneric(IMask value){
        var toPrint = value.GetName();
        Console.WriteLine(toPrint);
    }
    
    void UseValue<T>(T value)
    where T : IMask {
        var toPrint = value.GetName();
        Console.WriteLine(toPrint);
    }
    

    They do effectively the exact same thing, but differ in one thing - how the compiler emits the code for the actual interface call.

    In the nongeneric case we have

    callvirt instance string IMask::GetName()
    

    in the generic case we have:

    constrained. !!T
    callvirt instance string IMask::GetName()
    

    The second case is what you want to avoid boxing (allocation) as stated in the docs about the constrained prefix:

    When a callvirt method instruction has been prefixed by constrained thisType, the instruction is executed as follows:

    • ...
    • If thisType is a value type and thisType implements method then ptr is passed unmodified as the 'this' pointer to a call method
    • ...

    So, no matter if you were able to cast or not, you'd be unsuccessful because for the non-allocaiton (non-boxing) to even be able to happen you have to have a constrained generic method.

    And this simply won't work:

    void UseInterfaceReference(in IMask target)
    {
        target.GetName();
    }