Sorry for this long question but I feel I have to provide a bit more background as my issue is very specific.
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.
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;
}
}
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.
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!
I am now looking into two things to go around this
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.
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 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;
}