BeanCheeseBurrito / Flecs.NET

A C# wrapper for flecs
MIT License
121 stars 16 forks source link

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

Open BeanCheeseBurrito opened 2 months ago

BeanCheeseBurrito commented 2 months ago

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);
xentripetal commented 1 month ago

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 commented 1 month ago

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

Adding \true\ 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
BeanCheeseBurrito commented 1 month ago

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.

xentripetal commented 1 month ago

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\

xentripetal commented 1 week ago

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::RegisterComponent

xentripetal commented 1 week ago

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