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?
At this point I'm also open for any other alternative that provides the desired flexibility while maintaining the allocation restrictions.
To call a delegate, you need to call its
Invokemethod. This means you need theconverteritself. You can't use the.Methodproperty as you don't know what type of method it is (instance/static) or whether it involves any shuffling due to an extra or missingthis.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:But obviously this makes no sense to do. If all you wanted was to return a
Func<T, TOutput>as aDelegate, you don't need dynamic methods, you could just return the delegate you already have.So just cast use your original delegate.
And then you can just declare your dictionaries like this:
dotnetfiddle