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.
 
 

327 lines
12 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using gaemstone.ECS;
using static flecs_hub.flecs;
namespace gaemstone.Utility.IL;
public unsafe class QueryActionGenerator
{
private static readonly PropertyInfo _iteratorUniverseProp = typeof(Iterator).GetProperty(nameof(Iterator.Universe))!;
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 _iteratorFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field))!;
private static readonly MethodInfo _iteratorFieldIsSetMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldIsSet))!;
private static readonly MethodInfo _iteratorEntityMethod = typeof(Iterator).GetMethod(nameof(Iterator.Entity))!;
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, QueryActionGenerator> _cache = new();
private static readonly Dictionary<Type, Action<ILGeneratorWrapper, IArgument<Iterator>, ILocal<int>>> _uniqueParameters = new() {
[typeof(Iterator)] = (IL, iter, i) => { IL.Load(iter); },
[typeof(Universe)] = (IL, iter, i) => { IL.Load(iter, _iteratorUniverseProp); },
[typeof(TimeSpan)] = (IL, iter, i) => { IL.Load(iter, _iteratorDeltaTimeProp); },
[typeof(Entity)] = (IL, iter, i) => { IL.Load(iter); IL.Load(i); IL.Call(_iteratorEntityMethod); },
};
public Universe Universe { get; }
public MethodInfo Method { get; }
public ParamInfo[] Parameters { get; }
public ecs_filter_desc_t Filter { get; }
public Action<object?, Iterator> GeneratedAction { get; }
public string ReadableString { get; }
public void RunWithTryCatch(object? instance, Iterator iter)
{
try { GeneratedAction(instance, iter); } catch {
Console.WriteLine("Exception occured while running:");
Console.WriteLine(" " + Method);
Console.WriteLine();
Console.WriteLine("Method's IL code:");
Console.WriteLine(ReadableString);
Console.WriteLine();
throw;
}
}
public QueryActionGenerator(Universe universe, MethodInfo method)
{
Universe = universe;
Method = method;
Parameters = method.GetParameters().Select(ParamInfo.Build).ToArray();
if (!Parameters.Any(c => c.IsRequired && (c.Kind != ParamKind.Unique)))
throw new ArgumentException($"At least one parameter in {method} is required");
var filter = default(ecs_filter_desc_t);
var name = "<>Query_" + string.Join("_", Parameters.Select(p => p.UnderlyingType.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 counter = 0; // Counter for fields actually part of the filter terms.
var fieldLocals = new ILocal[Parameters.Length];
var tempLocals = new ILocal[Parameters.Length];
for (var i = 0; i < Parameters.Length; i++) {
var p = Parameters[i];
if (p.Kind == ParamKind.Unique) continue;
// Update the flecs filter to look for this type.
// Term index is 0-based and field index (used below) is 1-based, so increasing counter here works out.
ref var term = ref filter.terms[counter++];
term.id = Universe.Lookup(p.UnderlyingType);
term.inout = p.Kind switch {
ParamKind.In => ecs_inout_kind_t.EcsIn,
ParamKind.Out => ecs_inout_kind_t.EcsOut,
ParamKind.Has or ParamKind.Not => ecs_inout_kind_t.EcsInOutNone,
_ => ecs_inout_kind_t.EcsInOut,
};
term.oper = p.Kind switch {
ParamKind.Not => ecs_oper_kind_t.EcsNot,
_ when !p.IsRequired => ecs_oper_kind_t.EcsOptional,
_ => ecs_oper_kind_t.EcsAnd,
};
if (p.Source != null) term.src = new() { id = Universe.Lookup(p.Source) };
// Create a Span<T> local and initialize it to iterator.Field<T>(i).
var spanType = typeof(Span<>).MakeGenericType(p.FieldType);
fieldLocals[i] = IL.Local(spanType, $"field_{counter}");
if (p.Kind is ParamKind.Has or ParamKind.Not) {
// If a "has" or "not" parameter is a struct, we require a temporary local that
// we can later load onto the stack when loading the arguments for the action.
if (p.ParameterType.IsValueType) {
IL.Comment($"temp_{counter} = default({p.ParameterType});");
tempLocals[i] = IL.Local(p.ParameterType);
IL.LoadAddr(tempLocals[i]);
IL.Init(tempLocals[i].LocalType);
}
} else if (p.IsRequired) {
IL.Comment($"field_{counter} = iterator.Field<{p.FieldType.Name}>({counter})");
IL.Load(iteratorArg);
IL.LoadConst(counter);
IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType));
IL.Store(fieldLocals[i]);
} else {
IL.Comment($"field_{counter} = iterator.FieldIsSet({counter}) ? iterator.Field<{p.FieldType.Name}>({counter}) : default");
var elseLabel = IL.DefineLabel();
var doneLabel = IL.DefineLabel();
IL.Load(iteratorArg);
IL.LoadConst(counter);
IL.Call(_iteratorFieldIsSetMethod);
IL.GotoIfFalse(elseLabel);
IL.Load(iteratorArg);
IL.LoadConst(counter);
IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType));
IL.Store(fieldLocals[i]);
IL.Goto(doneLabel);
IL.MarkLabel(elseLabel);
IL.LoadAddr(fieldLocals[i]);
IL.Init(spanType);
IL.MarkLabel(doneLabel);
}
if (p.Kind == ParamKind.Nullable) {
IL.Comment($"temp_{counter} = default({p.ParameterType});");
tempLocals[i] = IL.Local(p.ParameterType);
IL.LoadAddr(tempLocals[i]);
IL.Init(tempLocals[i].LocalType);
}
}
// If there's any reference type parameters, we need to define a GCHandle local.
var hasReferenceType = Parameters
.Where(p => p.Kind != ParamKind.Unique)
.Any(p => !p.UnderlyingType.IsValueType);
var handleLocal = hasReferenceType ? IL.Local<GCHandle>() : null;
using (IL.For(() => IL.Load(iteratorArg, _iteratorCountProp), out var currentLocal)) {
if (!Method.IsStatic)
IL.Load(instanceArg);
for (var i = 0; i < Parameters.Length; i++) {
var p = Parameters[i];
if (p.Kind == ParamKind.Unique) {
IL.Comment($"Unique parameter {p.ParameterType}");
_uniqueParameters[p.ParameterType](IL, iteratorArg, currentLocal);
} else if (p.Kind is ParamKind.Has or ParamKind.Not) {
if (p.ParameterType.IsValueType)
IL.LoadObj(tempLocals[i]!);
else IL.LoadNull();
} else {
var spanType = typeof(Span<>).MakeGenericType(p.FieldType);
var spanItemMethod = spanType.GetProperty("Item")!.GetMethod!;
var spanLengthMethod = spanType.GetProperty("Length")!.GetMethod!;
IL.Comment($"Parameter {p.ParameterType}");
if (p.IsByRef) {
IL.LoadAddr(fieldLocals[i]!);
IL.Load(currentLocal);
IL.Call(spanItemMethod);
} else if (p.IsRequired) {
IL.LoadAddr(fieldLocals[i]!);
IL.Load(currentLocal);
IL.Call(spanItemMethod);
IL.LoadObj(p.FieldType);
} else {
var elseLabel = IL.DefineLabel();
var doneLabel = IL.DefineLabel();
IL.LoadAddr(fieldLocals[i]!);
IL.Call(spanLengthMethod);
IL.GotoIfFalse(elseLabel);
IL.LoadAddr(fieldLocals[i]!);
IL.Load(currentLocal);
IL.Call(spanItemMethod);
IL.LoadObj(p.FieldType);
if (p.Kind == ParamKind.Nullable)
IL.New(p.ParameterType);
IL.Goto(doneLabel);
IL.MarkLabel(elseLabel);
if (p.Kind == ParamKind.Nullable)
IL.LoadObj(tempLocals[i]!);
else IL.LoadNull();
IL.MarkLabel(doneLabel);
}
if (!p.UnderlyingType.IsValueType) {
IL.Comment($"Convert nint to {p.UnderlyingType}");
IL.Call(_handleFromIntPtrMethod);
IL.Store(handleLocal!);
IL.LoadAddr(handleLocal!);
IL.Call(_handleTargetProp.GetMethod!);
IL.Cast(p.UnderlyingType);
}
}
}
IL.Call(Method);
}
IL.Return();
Filter = filter;
GeneratedAction = genMethod.CreateDelegate<Action<object?, Iterator>>();
ReadableString = IL.ToReadableString();
}
public static QueryActionGenerator GetOrBuild(Universe universe, MethodInfo method)
=>_cache.GetValue(method, m => new QueryActionGenerator(universe, m));
public class ParamInfo
{
public ParameterInfo Info { get; }
public int Index { 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 >= ParamKind.In) && (Kind <= ParamKind.Ref);
private ParamInfo(
ParameterInfo info, int index, ParamKind kind,
Type paramType, Type underlyingType)
{
Info = info;
Index = index;
Kind = kind;
ParameterType = paramType;
UnderlyingType = underlyingType;
// Reference types have a backing type of nint - they're pointers.
FieldType = underlyingType.IsValueType ? underlyingType : typeof(nint);
// If the underlying type has EntityAttribute, it's a singleton.
if (UnderlyingType.Has<EntityAttribute>()) Source = underlyingType;
if (Info.Get<SourceAttribute>() is SourceAttribute attr) Source = attr.Type;
}
public static ParamInfo Build(ParameterInfo info, int index)
{
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 (_uniqueParameters.ContainsKey(info.ParameterType))
return new(info, index, ParamKind.Unique, info.ParameterType, info.ParameterType);
var isByRef = info.ParameterType.IsByRef;
var isNullable = info.IsNullable();
if (info.Has<NotAttribute>()) {
if (isByRef || isNullable) throw new ArgumentException(
"Parameter with NotAttribute must not be ByRef or nullable\nParameter: " + info);
return new(info, index, ParamKind.Not, info.ParameterType, info.ParameterType);
}
if (info.Has<HasAttribute>() || info.ParameterType.Has<TagAttribute>()) {
if (isByRef || isNullable) throw new ArgumentException(
"Parameter with HasAttribute / TagAttribute must not be ByRef or nullable\nParameter: " + info);
return new(info, index, ParamKind.Has, info.ParameterType, info.ParameterType);
}
var kind = ParamKind.Normal;
var underlyingType = info.ParameterType;
if (info.IsNullable()) {
if (info.ParameterType.IsValueType)
underlyingType = Nullable.GetUnderlyingType(info.ParameterType)!;
kind = ParamKind.Nullable;
}
if (info.ParameterType.IsByRef) {
if (kind == ParamKind.Nullable) throw new ArgumentException(
"ByRef and Nullable are not supported together\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, index, kind, info.ParameterType, underlyingType);
}
}
public enum ParamKind
{
/// <summary> Parameter is not part of terms, handled uniquely, such as Universe and Entity. </summary>
Unique,
/// <summary> Passed by value. </summary>
Normal,
/// <summary> Struct passed with the "in" modifier. </summary>
In,
/// <summary> Struct passed with the "out" modifier. </summary>
Out,
/// <summary> Struct passed with the "ref" modifier. </summary>
Ref,
/// <summary>
/// Only checks for presence.
/// Manually applied with <see cref="HasAttribute"/>.
/// Automatically applied for types with <see cref="TagAttribute"/>.
/// </summary>
Has,
/// <summary> Struct passed as <c>Nullable&lt;T&gt;</c>. </summary>
Nullable,
/// <summary>
/// Only checks for absence.
/// Applied with <see cref="NotAttribute"/>.
/// </summary>
Not,
}
}