BeanCheeseBurrito / Flecs.NET

A C# wrapper for flecs
MIT License
145 stars 18 forks source link

Managed Component Garbage Collection #33

Closed xentripetal closed 3 months ago

xentripetal commented 3 months ago

When testing managed component behavior I noticed that class finalizers are never called for destructed components.

public class FlecsTests
{
    public class FinalizerComponent(Action cb)
    {
        protected Action _cb = cb;

        ~FinalizerComponent()
        {
            _cb();
        }
    }

    [Fact]
    public void TestFinalizer()
    {
        bool finalizerCalled = false;
        using var world = World.Create();
        var entity = world.Entity().Set(new FinalizerComponent(() => finalizerCalled = true));
        entity.Destruct();
        for (int i = 0; i < 10; i++)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
        Assert.True(finalizerCalled);
    }
}

Checking the managed component behavior, it seems a GCHandle is allocated for it but its never freed. This means managed components will always be leaked.

It is slightly counter to being a direct Flecs wrapper, but it might make sense when a component is first registered to also automatically register an observer that will listen to OnRemove and free the handle.

xentripetal commented 3 months ago

Actually it looks like that's exactly what you do with the hooks. I'll investigate further to why this finalizer isn't called.

BeanCheeseBurrito commented 3 months ago

I think the class is still in scope in your example. If you move it into a separate function the finalizer gets called.

public class FinalizerComponent
{
    public static bool FinalizerCalled;
    ~FinalizerComponent()
    {
        FinalizerCalled = true;
    }
}

public static void CreateDelete()
{
    using World world = World.Create();
    world.Entity()
        .Set(new FinalizerComponent())
        .Destruct();
}

public static void TestFinalizer()
{
    CreateDelete();
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Console.WriteLine(FinalizerComponent.FinalizerCalled); // True
}