Search code examples
c#garbage-collectiondynamic-memory-allocationreflection.emitlinq-expressions

What is wrong with this Reflection.Emit for value conversion delegates?


Sorry for this long question but I feel I have to provide a bit more background as my issue is very specific.

Bigger picture

I am developing on a Unity tool to be used specifically for Embedded Linux platforms.

This environment has certain restrictions such as in specific being quite sensible with runtime allocations (GC causing hickups etc) -> we won't to avoid any form of runtime allocation or better said de-allocation.

Where I come from

In this tool one main feature is the conversion of types.

The original (POC) implementation broken down looked somewhat like e.g.

public interface IValue
{
    bool AsInt(out int result);
    bool AsFloat(out float result);
    bool AsDouble(out double result);
    //...
    // explicit conversions for all supported value types
}

public abstract Value<T> : IValue
{
    [SerializeField] private T m_Value;

    public T TypedValue
    {
        get => m_Value;
        set => m_Value = value;
    }

    private bool TryCast<TTarget>(out TTarget result)
    {
        if (m_Value is TTarget target)
        {
            result = target;
            return true;
        }

        result = default;
        return false;
    }

    public virtual bool AsInt(out int result)
    {
        return TryCast(out result);
    }

    public virtual bool AsFloat(out float result)
    {
        return TryCast(out result);
    }

    public virtual bool AsDouble(out double result)
    {
        return TryCast(out result);
    }

    //...
}

and as implementation example (there are more complex conversions and many types - this is just for understanding the concept)

[Serializable]
public class BoolOriginal : Value<bool>
{
    public override bool AsInt(out int result)
    {
        result = TypedValue ? 1 : 0;
        return true;
    }

    public override bool AsFloat(out float result)
    {
        result = TypedValue ? 1.0f : 0.0f;
        return true;
    }

    public override bool AsDouble(out double result)
    {
        result = TypedValue ? 1.0 : 0.0;
        return true;
    }
}

Original issue/limitation

Now the big "issue" with this is - as soon as you want to add support for a new type you have to hard code it into the interface and at least the base class. As this tool is supposed to be a readonly package this is something we can not do later on.

My first "solution" attempt

So I thought I could solve this by instead introducing generics and do something like e.g.

public interface IValue
{
    bool As<TTarget>(out TTarget result);
}

public abstract class Value<T> : IValue
{
    [SerializeField] private T m_Value;

    public T TypedValue
    {
        get => m_Value;
        set => m_Value = value;
    }

    public bool As<TTarget>(out TTarget result)
    {
        if (TypedValue is TTarget target)
        {
            result = target;
            return true;
        }

        if (Conversions.TryGetValue(typeof(TTarget), out var conversion))
        {
            result = (TTarget)conversion(TypedValue);
            return true;
        }

        if (k_DefaultConversions.TryGetValue(typeof(TTarget), out var defaultConversion))
        {
            result = (TTarget)defaultConversion(TypedValue);
            return true;
        }

        result = default;
        return false;
    }

    private static readonly IReadOnlyDictionary<Type, Func<T, object>> k_DefaultConversions = new Dictionary<Type, Func<T, object>>
    {
        { typeof(string), value => value.ToString() },
    };

    protected abstract IReadOnlyDictionary<Type, Func<T, object>> Conversions { get; }
}

and then simply let inheritors implement their own conversions

public class BoolPlain : Value<bool>
{
    private static readonly IReadOnlyDictionary<Type, Func<bool, object>> k_Conversions = new Dictionary<Type, Func<bool, object>>
    {
        { typeof(int), value => value ? 1 : 0 },
        { typeof(float), value => value ? 1.0f : 0.0f },
        { typeof(double), value => value ? 1.0 : 0.0 }
    };

    protected override IReadOnlyDictionary<Type, Func<bool, object>> Conversions => k_Conversions;
}

This works fine - but introduced boxing allocations! Again this doesn't seem a huge thing but in this specific embedded environment it kinda is!

So what now?

I am now looking into two things to go around this

Linq Expressions

public abstract class Value<T> : IValue
{
    private T m_Value;

    public T TypedValue
    {
        get => m_Value;
        set => m_Value = value;
    }

    public bool As<TTarget>(out TTarget result)
    {
        if (TypedValue is TTarget target)
        {
            result = target;
            return true;
        }

        if (Conversions.TryGetValue(typeof(TTarget), out var conversion) && conversion is Func<T, TTarget> converter)
        {
            result = converter(TypedValue);
            return true;
        }

        if (k_DefaultConversions.TryGetValue(typeof(TTarget), out var defaultConversion) && defaultConversion is Func<T, TTarget> defaultConverter)
        {
            result = defaultConverter(TypedValue);
            return true;
        }

        result = default;
        return false;
    }

    protected static Delegate CreateConverter<TOutput>(Func<T, TOutput> converter)
    {
        var input = Expression.Parameter(typeof(T), "input");
        var body = Expression.Invoke(Expression.Constant(converter), input);
        var lambda = Expression.Lambda(body, input);
        return lambda.Compile();
    }

    private static readonly IReadOnlyDictionary<Type, Delegate> k_DefaultConversions = new Dictionary<Type, Delegate>
    {
        { typeof(string), CreateConverter<string>(v => v.ToString()) },
    };

    protected abstract IReadOnlyDictionary<Type, Delegate> Conversions { get; }
}

and implementation

public class BoolValue : Value<bool>
{
    private static readonly IReadOnlyDictionary<Type, Delegate> k_Conversions = new Dictionary<Type, Delegate>
    {
        { typeof(int), CreateConverter<int>(v => v ? 1 : 0) },
        { typeof(float), CreateConverter<float>(v => v ? 1.0f : 0.0f) },
        { typeof(double), CreateConverter<double>(v => v ? 1.0 : 0.0) }
    };

    protected override IReadOnlyDictionary<Type, Delegate> Conversions => k_Conversions;
}

This at least halves the allocation compared to the plain generic way above and seems very slightly more efficient.

Question

So as alternative I wanted to try Reflection.Emit instead and do the following (with some ChatGPT and research help not gonna lie ^^)

private static readonly valueType = typeof(T);

protected static Delegate CreateConverter<TOutput>(Func<T, TOutput> converter)
{
    var outType = typeof(TOutput);

    var method = new DynamicMethod(
        "Convert_" + valueType.Name + "_To_" + outType.Name,
        outType,
        new[] { valueType },
        typeof(Value<>).Module,
        true);

    var il = method.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.EmitCall(OpCodes.Call, converter.Method, null);
    il.Emit(OpCodes.Ret);

    return method.CreateDelegate(typeof(Func<T, TOutput>));
}

But this always gives me an

InvalidProgramException: Invalid IL code in (wrapper dynamic-method) object:Convert_Boolean_To_String (bool): IL_0001: call      0x00000001

I tried a lot of different iterations - also using OpCodes.Callvirt instead - but this issue persists.

What am I doing wrong here?

Is it maybe an issue that the base class used in typeof(Value<>).Module is generic?

Fiddle here


At this point I'm also open for any other alternative that provides the desired flexibility while maintaining the allocation restrictions.


Solution

  • To call a delegate, you need to call its Invoke method. This means you need the converter itself. You can't use the .Method property as you don't know what type of method it is (instance/static) or whether it involves any shuffling due to an extra or missing this.

    So if you really wanted to use this method (don't, see below), then you would need to pass in the original Func, something like:

    protected static Delegate CreateConverter<TOutput>(Func<T, TOutput> converter)
    {
        var outType = typeof(TOutput);
    
        var method = new DynamicMethod(
            "Convert_" + valueType.Name + "_To_" + outType.Name,
            outType,
            new[] { typeof(Func<T, TOutput>), valueType, },
            typeof(Value<>).Module,
            true);
    
        var il = method.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.EmitCall(OpCodes.Call, typeof(Func<T, TOutput>).GetMethod("Invoke"), null);
        il.Emit(OpCodes.Ret);
    
        return method.CreateDelegate(typeof(Func<Func<T, TOutput>, T, TOutput>));
    }
    

    But obviously this makes no sense to do. If all you wanted was to return a Func<T, TOutput> as a Delegate, you don't need dynamic methods, you could just return the delegate you already have.

    So just cast use your original delegate.

    // we expect each Delegate to be a Func<T, Type> where Type is the key in the dictionay
    protected abstract IReadOnlyDictionary<Type, Delegate> Conversions { get; }
    
    public bool As<TTarget>(out TTarget result)
    {
        if (TypedValue is TTarget result)
        {
            return true;
        }
    
        if (Conversions.TryGetValue(typeof(TTarget), out var conversion) && conversion is Func<T, TTarget> converter)
        {
            result = converter(TypedValue);
            return true;
        }
    
        if (k_DefaultConversions.TryGetValue(typeof(TTarget), out var defaultConversion) && defaultConversion is Func<T, TTarget> defaultConverter)
        {
            result = defaultConverter(TypedValue);
            return true;
        }
    
        result = default;
        return false;
    }
    

    And then you can just declare your dictionaries like this:

    public class BoolPlain : Value<bool>
    {
        private static readonly IReadOnlyDictionary<Type, Delegate> k_Conversions = new Dictionary<Type, Delegate>
        {
            { typeof(int),    new Func<bool, int>   (value => value ? 1 : 0) },
            { typeof(float),  new Func<bool, float> (value => value ? 1.0f : 0.0f) },
            { typeof(double), new Func<bool, double>(value => value ? 1.0 : 0.0) }
        };
    
        protected override IReadOnlyDictionary<Type, Delegate> Conversions => k_Conversions;
    }
    

    dotnetfiddle