You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
425 lines
16 KiB
425 lines
16 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.Collections.Immutable; |
|
using System.Linq; |
|
using System.Reflection; |
|
using System.Reflection.Emit; |
|
using System.Runtime.CompilerServices; |
|
using gaemstone.ECS; |
|
using GCHandle = System.Runtime.InteropServices.GCHandle; |
|
|
|
namespace gaemstone.Utility.IL; |
|
|
|
// TODO: Support tuple syntax to match relationship pairs. |
|
public unsafe class IterActionGenerator |
|
{ |
|
private static readonly ConstructorInfo _entityRefCtor = typeof(EntityRef).GetConstructors().Single(); |
|
|
|
private static readonly PropertyInfo _iteratorWorldProp = typeof(Iterator).GetProperty(nameof(Iterator.World))!; |
|
private static readonly PropertyInfo _iteratorDeltaTimeProp = typeof(Iterator).GetProperty(nameof(Iterator.DeltaTime))!; |
|
private static readonly PropertyInfo _iteratorCountProp = typeof(Iterator).GetProperty(nameof(Iterator.Count))!; |
|
private static readonly MethodInfo _iteratorEntityMethod = typeof(Iterator).GetMethod(nameof(Iterator.Entity))!; |
|
private static readonly MethodInfo _iteratorFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field))!; |
|
private static readonly MethodInfo _iteratorFieldOrEmptyMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldOrEmpty))!; |
|
|
|
private static readonly MethodInfo _handleFromIntPtrMethod = typeof(GCHandle).GetMethod(nameof(GCHandle.FromIntPtr))!; |
|
private static readonly PropertyInfo _handleTargetProp = typeof(GCHandle).GetProperty(nameof(GCHandle.Target))!; |
|
|
|
private static readonly ConditionalWeakTable<MethodInfo, IterActionGenerator> _cache = new(); |
|
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>>> _globalUniqueParameters = new() { |
|
[typeof(World)] = (IL, iter) => { IL.Load(iter, _iteratorWorldProp); }, |
|
[typeof(Universe)] = (IL, iter) => { IL.Load(iter, _iteratorWorldProp); IL.Cast(typeof(Universe)); }, |
|
[typeof(TimeSpan)] = (IL, iter) => { IL.Load(iter, _iteratorDeltaTimeProp); }, |
|
}; |
|
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>, ILocal<int>>> _uniqueParameters = new() { |
|
[typeof(Iterator)] = (IL, iter, i) => { IL.Load(iter); }, |
|
[typeof(EntityRef)] = (IL, iter, i) => { IL.Load(iter); IL.Load(i); IL.Call(_iteratorEntityMethod); }, |
|
}; |
|
|
|
public World World { get; } |
|
public MethodInfo Method { get; } |
|
public IReadOnlyList<ParamInfo> Parameters { get; } |
|
|
|
public IReadOnlyList<Term> Terms { get; } |
|
public Action<object?, Iterator> GeneratedAction { get; } |
|
public string ReadableString { get; } |
|
|
|
public void RunWithTryCatch(object? instance, Iterator iter) |
|
{ |
|
try { GeneratedAction(instance, iter); } |
|
catch { Console.Error.WriteLine(ReadableString); throw; } |
|
} |
|
|
|
public IterActionGenerator(World world, MethodInfo method) |
|
{ |
|
World = world; |
|
Method = method; |
|
Parameters = method.GetParameters().Select(ParamInfo.Build).ToImmutableArray(); |
|
|
|
var name = "<>Query_" + string.Join("_", method.Name); |
|
var genMethod = new DynamicMethod(name, null, new[] { typeof(object), typeof(Iterator) }); |
|
var IL = new ILGeneratorWrapper(genMethod); |
|
|
|
var instanceArg = IL.Argument<object?>(0); |
|
var iteratorArg = IL.Argument<Iterator>(1); |
|
|
|
var fieldIndex = 0; |
|
var paramData = new List<(ParamInfo Info, Term? Term, ILocal? FieldLocal, ILocal? TempLocal)>(); |
|
foreach (var p in Parameters) { |
|
// If the parameter is unique, we don't create a term for it. |
|
if (p.Kind <= ParamKind.Unique) |
|
{ paramData.Add((p, null, null, null)); continue; } |
|
|
|
// Create a term to add to the query. |
|
var term = new Term(world.LookupByTypeOrThrow(p.UnderlyingType)) { |
|
Source = (p.Source != null) ? (TermId)World.LookupByTypeOrThrow(p.Source) : null, |
|
InOut = p.Kind switch { |
|
ParamKind.In => TermInOutKind.In, |
|
ParamKind.Out => TermInOutKind.Out, |
|
ParamKind.Not or ParamKind.Not => TermInOutKind.None, |
|
_ => default, |
|
}, |
|
Oper = p.Kind switch { |
|
ParamKind.Not => TermOperKind.Not, |
|
ParamKind.Or => TermOperKind.Or, |
|
_ when !p.IsRequired => TermOperKind.Optional, |
|
_ => default, |
|
}, |
|
}; |
|
|
|
// If this and the previous parameter are marked with [Or], do not advance the field index. |
|
if ((fieldIndex == 0) || (p.Kind != ParamKind.Or) || (paramData[^1].Info.Kind != ParamKind.Or)) |
|
fieldIndex++; |
|
|
|
var spanType = typeof(Span<>).MakeGenericType(p.FieldType); |
|
var fieldLocal = (ILocal?)null; |
|
var tempLocal = (ILocal?)null; |
|
|
|
switch (p.Kind) { |
|
// FIXME: Currently would not work with [Or]'d components. |
|
case ParamKind.Has or ParamKind.Not or ParamKind.Or: |
|
if (!p.ParameterType.IsValueType) break; |
|
// If parameter is a struct, we require a temporary local that we can |
|
// later load onto the stack when loading the arguments for the action. |
|
IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});"); |
|
tempLocal = IL.Local(p.ParameterType); |
|
IL.LoadAddr(tempLocal); |
|
IL.Init(tempLocal.LocalType); |
|
break; |
|
|
|
case ParamKind.Nullable or ParamKind.Or: |
|
IL.Comment($"{p.Info.Name}Field = iterator.FieldOrEmpty<{p.FieldType.Name}>({fieldIndex})"); |
|
fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field"); |
|
IL.Load(iteratorArg); |
|
IL.LoadConst(fieldIndex); |
|
IL.Call(_iteratorFieldOrEmptyMethod.MakeGenericMethod(p.FieldType)); |
|
IL.Store(fieldLocal); |
|
|
|
if (p.UnderlyingType.IsValueType) { |
|
IL.Comment($"{p.Info.Name}Temp = default({p.ParameterType});"); |
|
tempLocal = IL.Local(p.ParameterType); |
|
IL.LoadAddr(tempLocal); |
|
IL.Init(tempLocal.LocalType); |
|
} |
|
break; |
|
|
|
default: |
|
IL.Comment($"{p.Info.Name}Field = iterator.Field<{p.FieldType.Name}>({fieldIndex})"); |
|
fieldLocal = IL.Local(spanType, $"{p.Info.Name}Field"); |
|
IL.Load(iteratorArg); |
|
IL.LoadConst(fieldIndex); |
|
IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType)); |
|
IL.Store(fieldLocal); |
|
break; |
|
} |
|
|
|
paramData.Add((p, term, fieldLocal, tempLocal)); |
|
} |
|
|
|
// If there's any reference type parameters, we need to define a GCHandle local. |
|
var hasReferenceType = paramData |
|
.Where(p => p.Info.Kind > ParamKind.Unique) |
|
.Any(p => !p.Info.UnderlyingType.IsValueType); |
|
var handleLocal = hasReferenceType ? IL.Local<GCHandle>() : null; |
|
|
|
var indexLocal = IL.Local<int>("iter_index"); |
|
var countLocal = IL.Local<int>("iter_count"); |
|
|
|
IL.Set(indexLocal, 0); |
|
IL.Load(iteratorArg, _iteratorCountProp); |
|
IL.Store(countLocal); |
|
|
|
// If all parameters are fixed, iterator count will be 0, but since |
|
// the query matched, we want to run the callback at least once. |
|
IL.Comment("if (iter_count == 0) iter_count = 1;"); |
|
var dontIncrementLabel = IL.DefineLabel(); |
|
IL.Load(countLocal); |
|
IL.GotoIfTrue(dontIncrementLabel); |
|
IL.LoadConst(1); |
|
IL.Store(countLocal); |
|
IL.MarkLabel(dontIncrementLabel); |
|
|
|
IL.While("IteratorLoop", (@continue) => { |
|
IL.GotoIf(@continue, indexLocal, Comparison.LessThan, countLocal); |
|
}, (_, _) => { |
|
if (!Method.IsStatic) |
|
IL.Load(instanceArg); |
|
|
|
foreach (var (info, term, fieldLocal, tempLocal) in paramData) { |
|
var isValueType = info.UnderlyingType.IsValueType; |
|
var paramName = info.ParameterType.GetFriendlyName(); |
|
switch (info.Kind) { |
|
|
|
case ParamKind.GlobalUnique: |
|
IL.Comment($"Global unique parameter {paramName}"); |
|
_globalUniqueParameters[info.ParameterType](IL, iteratorArg); |
|
break; |
|
|
|
case ParamKind.Unique: |
|
IL.Comment($"Unique parameter {paramName}"); |
|
_uniqueParameters[info.ParameterType](IL, iteratorArg, indexLocal!); |
|
break; |
|
|
|
// FIXME: Currently would not work with [Or]'d components. |
|
case ParamKind.Has or ParamKind.Not or ParamKind.Or: |
|
IL.Comment($"Has parameter {paramName}"); |
|
if (isValueType) IL.LoadObj(tempLocal!); |
|
else IL.LoadNull(); |
|
break; |
|
|
|
default: |
|
var spanType = typeof(Span<>).MakeGenericType(info.FieldType); |
|
var spanItemMethod = spanType.GetProperty("Item")!.GetMethod!; |
|
var spanLengthMethod = spanType.GetProperty("Length")!.GetMethod!; |
|
|
|
IL.Comment($"Parameter {paramName}"); |
|
if (info.IsByRef) { |
|
IL.LoadAddr(fieldLocal!); |
|
if (info.IsFixed) IL.LoadConst(0); |
|
else IL.Load(indexLocal!); |
|
IL.Call(spanItemMethod); |
|
} else if (info.IsRequired) { |
|
IL.LoadAddr(fieldLocal!); |
|
if (info.IsFixed) IL.LoadConst(0); |
|
else IL.Load(indexLocal!); |
|
IL.Call(spanItemMethod); |
|
IL.LoadObj(info.FieldType); |
|
|
|
if (!isValueType) { |
|
IL.Comment($"Convert nint to {paramName}"); |
|
IL.Call(_handleFromIntPtrMethod); |
|
IL.Store(handleLocal!); |
|
IL.LoadAddr(handleLocal!); |
|
IL.Call(_handleTargetProp.GetMethod!); |
|
IL.Cast(info.UnderlyingType); |
|
} |
|
} else { |
|
var elseLabel = IL.DefineLabel(); |
|
var doneLabel = IL.DefineLabel(); |
|
IL.LoadAddr(fieldLocal!); |
|
IL.Call(spanLengthMethod); |
|
IL.GotoIfFalse(elseLabel); |
|
IL.LoadAddr(fieldLocal!); |
|
if (info.IsFixed) IL.LoadConst(0); |
|
else IL.Load(indexLocal!); |
|
IL.Call(spanItemMethod); |
|
IL.LoadObj(info.FieldType); |
|
if (!isValueType) { |
|
IL.Comment($"Convert nint to {paramName}"); |
|
IL.Call(_handleFromIntPtrMethod); |
|
IL.Store(handleLocal!); |
|
IL.LoadAddr(handleLocal!); |
|
IL.Call(_handleTargetProp.GetMethod!); |
|
IL.Cast(info.UnderlyingType); |
|
} else IL.New(info.ParameterType); |
|
IL.Goto(doneLabel); |
|
IL.MarkLabel(elseLabel); |
|
if (!isValueType) IL.LoadNull(); |
|
else IL.LoadObj(tempLocal!); |
|
IL.MarkLabel(doneLabel); |
|
} |
|
break; |
|
} |
|
} |
|
IL.Call(Method); |
|
|
|
IL.Increment(indexLocal); |
|
}); |
|
|
|
IL.Return(); |
|
|
|
Terms = paramData.Where(p => p.Term != null).Select(p => p.Term!).ToImmutableList(); |
|
GeneratedAction = genMethod.CreateDelegate<Action<object?, Iterator>>(); |
|
ReadableString = IL.ToReadableString(); |
|
} |
|
|
|
public static IterActionGenerator GetOrBuild(World world, MethodInfo method) |
|
=>_cache.GetValue(method, m => new IterActionGenerator(world, m)); |
|
|
|
public class ParamInfo |
|
{ |
|
public ParameterInfo Info { get; } |
|
|
|
public ParamKind Kind { get; } |
|
public Type ParameterType { get; } |
|
public Type UnderlyingType { get; } |
|
public Type FieldType { get; } |
|
|
|
public Type? Source { get; } |
|
|
|
public bool IsRequired => (Kind < ParamKind.Nullable); |
|
public bool IsByRef => (Kind is ParamKind.In or ParamKind.Ref); |
|
public bool IsFixed => (Kind == ParamKind.GlobalUnique) || (Source != null); |
|
|
|
private ParamInfo(ParameterInfo info, ParamKind kind, |
|
Type paramType, Type underlyingType) |
|
{ |
|
Info = info; |
|
Kind = kind; |
|
ParameterType = paramType; |
|
UnderlyingType = underlyingType; |
|
// Reference types have a backing type of nint - they're pointers. |
|
FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint); |
|
|
|
if (UnderlyingType.Has<SingletonAttribute>()) Source = UnderlyingType; |
|
if (Info.Get<SourceAttribute>()?.Type is Type type) Source = type; |
|
} |
|
|
|
public static ParamInfo Build(ParameterInfo info) |
|
{ |
|
if (info.IsOptional) throw new ArgumentException($"Optional parameters are not supported\nParameter: {info}"); |
|
if (info.ParameterType.IsArray) throw new ArgumentException($"Arrays are not supported\nParameter: {info}"); |
|
if (info.ParameterType.IsPointer) throw new ArgumentException($"Pointers are not supported\nParameter: {info}"); |
|
if (info.ParameterType.IsPrimitive) throw new ArgumentException($"Primitives are not supported\nParameter: {info}"); |
|
|
|
// Find out initial parameter kind from provided attribute. |
|
var fromAttributes = new List<ParamKind>(); |
|
if (info.Has< InAttribute>()) fromAttributes.Add(ParamKind.In); |
|
if (info.Has<OutAttribute>()) fromAttributes.Add(ParamKind.Out); |
|
if (info.Has<HasAttribute>()) fromAttributes.Add(ParamKind.Has); |
|
if (info.Has< OrAttribute>()) fromAttributes.Add(ParamKind.Or); |
|
if (info.Has<NotAttribute>()) fromAttributes.Add(ParamKind.Not); |
|
// Throw an error if multiple incompatible attributes were found. |
|
if (fromAttributes.Count > 1) throw new ArgumentException( |
|
"Parameter must not be marked with multiple attributes: " |
|
+ string.Join(", ", fromAttributes.Select(a => $"[{a}]")) |
|
+ $"\nParameter: {info}"); |
|
var kind = fromAttributes.FirstOrNull() ?? ParamKind.Normal; |
|
|
|
// Handle unique parameters such as Universe, EntityRef, ... |
|
var isGlobalUnique = _globalUniqueParameters.ContainsKey(info.ParameterType); |
|
var isUnique = _uniqueParameters.ContainsKey(info.ParameterType); |
|
if (isGlobalUnique || isUnique) { |
|
if (kind != ParamKind.Normal) throw new ArgumentException( |
|
$"Unique parameter {info.ParameterType.Name} does not support [{kind}]\nParameter: {info}"); |
|
kind = isGlobalUnique ? ParamKind.GlobalUnique : ParamKind.Unique; |
|
return new(info, kind, info.ParameterType, info.ParameterType); |
|
} |
|
|
|
var isNullable = info.IsNullable(); |
|
var isByRef = info.ParameterType.IsByRef; |
|
|
|
if (info.ParameterType.Has<TagAttribute>() && (kind is not (ParamKind.Has or ParamKind.Not or ParamKind.Or))) { |
|
if (kind is not ParamKind.Normal) throw new ArgumentException($"Parameter does not support [{kind}]\nParameter: {info}"); |
|
kind = ParamKind.Has; |
|
} |
|
|
|
if (kind is ParamKind.Not or ParamKind.Has) { |
|
if (isNullable) throw new ArgumentException($"Parameter does not support Nullable\nParameter: {info}"); |
|
if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}"); |
|
return new(info, kind, info.ParameterType, info.ParameterType); |
|
} |
|
|
|
var underlyingType = info.ParameterType; |
|
|
|
if (isNullable) { |
|
if (isByRef) throw new ArgumentException($"Parameter does not support ByRef\nParameter: {info}"); |
|
if (info.ParameterType.IsValueType) |
|
underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!; |
|
kind = ParamKind.Nullable; |
|
} |
|
|
|
if (info.ParameterType.IsByRef) { |
|
if (kind != ParamKind.Normal) throw new ArgumentException( |
|
$"Parameter does not support [{kind}]\nParameter: {info}"); |
|
underlyingType = info.ParameterType.GetElementType()!; |
|
if (!underlyingType.IsValueType) throw new ArgumentException( |
|
$"Reference types can't also be ByRef\nParameter: {info}"); |
|
kind = info.IsIn ? ParamKind.In |
|
: info.IsOut ? ParamKind.Out |
|
: ParamKind.Ref; |
|
} |
|
|
|
if (underlyingType.IsPrimitive) throw new ArgumentException( |
|
$"Primitives are not supported\nParameter: {info}"); |
|
|
|
return new(info, kind, info.ParameterType, underlyingType); |
|
} |
|
} |
|
|
|
public enum ParamKind |
|
{ |
|
/// <summary> |
|
/// Not part of the resulting query's terms. |
|
/// Same value across a single invocation of a callback. |
|
/// For example <see cref="ECS.World"/> or <see cref="TimeSpan"/>. |
|
/// </summary> |
|
GlobalUnique, |
|
|
|
/// <summary> |
|
/// Not part of the resulting query's terms. |
|
/// Unique value for each iterated entity. |
|
/// For example <see cref="EntityRef"/>. |
|
/// </summary> |
|
Unique, |
|
|
|
/// <summary> Passed by value. </summary> |
|
Normal, |
|
|
|
/// <summary> |
|
/// Struct passed with the "in" modifier, allowing direct pointer access. |
|
/// Manually applied with <see cref="InAttribute"/>. |
|
/// Marks a component as being read from. |
|
/// </summary> |
|
In, |
|
|
|
/// <summary> |
|
/// Struct passed with the "out" modifier, allowing direct pointer access. |
|
/// Manually applied with <see cref="HasAttribute"/>. |
|
/// Marks a component as being written to. |
|
/// </summary> |
|
Out, |
|
|
|
/// <summary> |
|
/// Struct passed with the "ref" modifier, allowing direct pointer access. |
|
/// Marks a component as being read from and written to. |
|
/// </summary> |
|
Ref, |
|
|
|
/// <summary> |
|
/// Only checks for presence. |
|
/// Manually applied with <see cref="HasAttribute"/>. |
|
/// Automatically applied for types with <see cref="TagAttribute"/>. |
|
/// Marks a component as not being accessed. |
|
/// </summary> |
|
Has, |
|
|
|
/// <summary> |
|
/// Struct or class passed as <see cref="T?"/>. |
|
/// </summary> |
|
Nullable, |
|
|
|
/// <summary> |
|
/// Only checks for absence. |
|
/// Applied with <see cref="NotAttribute"/>. |
|
/// </summary> |
|
Not, |
|
|
|
/// <summary> |
|
/// Matches any terms in a chain of "or" terms. |
|
/// Applied with <see cref="OrAttribute"/>. |
|
/// Implies <see cref="Nullable"/>. |
|
/// </summary> |
|
Or, |
|
} |
|
}
|
|
|