Flecs.NET icon indicating copy to clipboard operation
Flecs.NET copied to clipboard

[v4] Implement component id lookup support for System.Type

Open BeanCheeseBurrito opened this issue 1 year ago • 6 comments

It would be useful in reflection scenarios to allow the user to lookup component ids and execute ECS operations using System.Type. This functionality is required for automatic member registration/serialization/deserialization support. Needs to also support NativeAOT with trimming which might not be possible with the way component registration currently works.

// Example of registering all types in an assembly through reflection.
foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
    world.Component(type);

BeanCheeseBurrito avatar Jun 21 '24 01:06 BeanCheeseBurrito

I messed around with trying to implement this and it seems like there's no way to get the size of the component without breaking NativeAOT.

You can construct an instance of T using Activator as long as you annotate the type properly. However, there's no way to size it directly since you can't access Marshal.SizeOf in AoT.

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)]

You might be able to hack some sort size estimator by walking the fields and using known sizes for base types but that would get very messy.

Might be out of scope but you could consider splitting into a Flecs.Core package with AoT support and a Flecs package without that adds support for dynamic registration and types. Alternatively you could use defines for gating off non-aot compatible code.

xentripetal avatar Aug 01 '24 04:08 xentripetal

Worth noting, it seems Type<T> currently isn't AoT compatible. Its usage of GetEnumValues throws an AoT warning.

Adding <PublishAoT>true</PublishAoT> to the Examples project and running

dotnet publish --property:Example=Reflection_BasicsEnum -r osx-arm64
./bin/Release/net8.0/osx-arm64/publish/Flecs.NET.Examples
Unhandled Exception: System.TypeInitializationException: A type initializer threw an exception. To determine which type, inspect the InnerException's StackTrace property.
 ---> System.NotSupportedException: '<BasicsEnum>FD8BE5548CB1ACE8A8CBC7E39E2FB23FDF52B13BA421B001B7B83140A7A1DF813__Color[]' is missing native code or metadata. This can happen for code that is not compatible with trimming or AOT. Inspect and fix trimming and AOT related warnings that were generated when the app was published. For more information see https://aka.ms/nativeaot-compatibility
   at System.Reflection.Runtime.General.TypeUnifier.WithVerifiedTypeHandle(RuntimeArrayTypeInfo, RuntimeTypeInfo) + 0x70
   at System.Array.InternalCreate(RuntimeType, Int32, Int32*, Int32*) + 0x78
   at System.Array.CreateInstance(Type, Int32) + 0x48
   at System.RuntimeType.GetEnumValues() + 0x64
   at Flecs.NET.Core.Type`1.InitEnumCacheIndexes() + 0x64
   at Flecs.NET.Core.Type`1..cctor() + 0x128
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0xbc
   --- End of inner exception stack trace ---
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0x15c
   at System.Runtime.CompilerServices.ClassConstructorRunner.CheckStaticClassConstructionReturnNonGCStaticBase(StaticClassConstructionContext*, IntPtr) + 0x14
   at Flecs.NET.Core.Component`1..ctor(flecs.ecs_world_t*) + 0xa4
   at Reflection_BasicsEnum.Main() + 0x44
   at Flecs.NET!<BaseAddress>+0x171fb4

xentripetal avatar Aug 03 '24 02:08 xentripetal

Worth noting, it seems Type currently isn't AoT compatible. Its usage of GetEnumValues throws an AoT warning.

I wonder if switching to Enum.GetValuesAsUnderlyingType fixes AOT compatibility. I dropped support for versions below .NET 8 in the type-safe-queries branch so we have access to this function now.

BeanCheeseBurrito avatar Aug 03 '24 02:08 BeanCheeseBurrito

Yup that fixes it! AoT still gives off a warning about the

            if (RuntimeFeature.IsDynamicCodeSupported)
            {
                FieldInfo[] fields =
                    type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
                size = fields.Length == 0 ? 0 : size;
                alignment = size == 0 ? 0 : alignment;
            }

but seems fine since its gated. You could annotate it but it looks like you'd have to bubble up that annotation to 50+ other generic methods that call Type<T>

xentripetal avatar Aug 03 '24 03:08 xentripetal

In the meantime, would you have any qualms with storing the reflection Type on the component entity? I have a usecase for going from the component entity Id to knowing its type for runtime reflection.

I figure easiest solution would be to just do

world.Entity(component).Set(typeof(T));

in Type<T>::RegisterComponent

xentripetal avatar Sep 03 '24 23:09 xentripetal

See https://github.com/BeanCheeseBurrito/Flecs.NET/pull/41

xentripetal avatar Sep 04 '24 01:09 xentripetal