sebas
sebas

Reputation: 1085

C# Casting T where T: struct to an interface without boxing

I need to write an allocation free code that avoids boxing when casting from a generic parameter T with struct contraints to an interface to access one implemented property.

I previously solved this problem using dynamically generated code and the lambda expression tree like:

        public delegate void SetEGIDWithoutBoxingActionCast<T>(ref T target, EGID egid) where T : struct, IEntityComponent;

        static SetEGIDWithoutBoxingActionCast<T> MakeSetter()
        {
            if (ComponentBuilder<T>.HAS_EGID)
            {
                Type         myTypeA     = typeof(T);
                PropertyInfo myFieldInfo = myTypeA.GetProperty("ID");

                ParameterExpression targetExp = Expression.Parameter(typeof(T).MakeByRefType(), "target");
                ParameterExpression valueExp  = Expression.Parameter(typeof(EGID), "value");
                MemberExpression    fieldExp  = Expression.Property(targetExp, myFieldInfo);
                BinaryExpression    assignExp = Expression.Assign(fieldExp, valueExp);

                var setter = Expression.Lambda<SetEGIDWithoutBoxingActionCast<T>>(assignExp, targetExp, valueExp).Compile();

                return setter;
            }

The resulting "setter" delegate would allow to set the property value to any structure implementing an ID property without any boxing.

However now I need to solve the same problem without generating dynamic code. I did some quick experiments, but it seems that Unsafe class fails to convert a struct to an interface using any of the As methods, probably because a struct cannot be cast directly to an interface as it is seen as an object and hence cannot be handle directly with a pointer.

Any pointer (no pun intended)?

Edit: I don't necessarily need to cast the struct to the interface, I just need to be able to write the value in the property ID. The fact that it is a property and not a field doesn't help for sure (as I found some code that is able to compute the offset of a field in a struct, but can't be used for a property of course)

Answer accepted, the final code is this and it works fine:

public delegate void SetEGIDWithoutBoxingActionCast<T>(ref T target, EGID egid) where T : struct, IEntityComponent;

static class SetEGIDWithoutBoxing<T> where T : struct, IEntityComponent
{
    public static readonly SetEGIDWithoutBoxingActionCast<T> SetIDWithoutBoxing = MakeSetter();

    public static void Warmup() { }

    static SetEGIDWithoutBoxingActionCast<T> MakeSetter()
    {
        if (ComponentBuilder<T>.HAS_EGID)
        {
            var method = typeof(Trick).GetMethod(nameof(Trick.SetEGIDImpl)).MakeGenericMethod(typeof(T));
            return (SetEGIDWithoutBoxingActionCast<T>) Delegate.CreateDelegate(
                typeof(SetEGIDWithoutBoxingActionCast<T>), method);
        }

        return null;
    }

    static class Trick
    {    
        public static void SetEGIDImpl<U>(ref U target, EGID egid) where U : struct, INeedEGID
        {
            target.ID = egid;
        }
    }
}

Upvotes: 4

Views: 3007

Answers (1)

kalimag
kalimag

Reputation: 1196

You can't cast a struct to an interface without boxing. Interface calls work through the vtable found in an object's header, which unboxed structs do not have.

However, I believe what you are trying to do can be accomplished by creating an unconstrained delegate for a constrained generic method by using Delegate.CreateDelegate.

// No constraints needed on the delegate type
public delegate void SetEGIDWithoutBoxingActionCast<T>(ref T target, EGID egid);

public static SetEGIDWithoutBoxingActionCast<T> MakeSetter<T>()
{
    var method = typeof(Foo).GetMethod(nameof(Foo.SetEGIDImpl)).MakeGenericMethod(typeof(T));
    return (SetEGIDWithoutBoxingActionCast<T>)Delegate.CreateDelegate(typeof(SetEGIDWithoutBoxingActionCast<T>), method);
}

public static class Foo
{    
    public static void SetEGIDImpl<T>(ref T target, EGID egid) where T : INeedEGID
    {
        target.ID = egid;
    }
}

Upvotes: 3

Related Questions