0
\$\begingroup\$

I'm building a game engine from scratch and trying to implement a basic plugin system within my engine.

Using AssemblyLoadContext to load the target dll dynamicly and unloading it with AssemblyLoadContext.Unload()

However it seems even if there is a one class instance created from this assembly hanging somewhere in the whole app domain AssemblyLoadContext.Unload() does not unload it until the all the instances are dereferenced.

This is where my problem begins.

There are infinite number of scenarios whether a class which belongs to the plugin can be instantiated and used across the whole appdomain right?.

So how do i keep track of the plugin instances across the whole AppDomain.

(How unity unloads the user scripts and cleans every last one of the instances i wonder)

My engine system is nearly identical to both unreal and unity.

I have entities and the components just like unity.

My current unload workflow is like this.

  1. Loop through every component via reflection and check if one the assembly classes instantiated then set that field/property null
  2. Loop through every assembly and check for static fields/properties which might contain some instance
  3. Now unload it.

However the approach above seems well idiotic and sluggish not to mention it should be slow as hell.

It seems i have a desing problem within my plugin system and i fear if i overcomplicated things.

Are there better way to cleanup assembly instances or am i missing something? (Please tell me im missing something obvious)

\$\endgroup\$
4
  • \$\begingroup\$ Do you want to unload at any moment or at certain checkpoints only? \$\endgroup\$ Apr 13, 2021 at 14:04
  • \$\begingroup\$ Certain checkups only, like at the start of the game session and at the end \$\endgroup\$
    – Roveldo
    Apr 13, 2021 at 17:42
  • \$\begingroup\$ What runtime version are you using? \$\endgroup\$ Apr 13, 2021 at 18:15
  • \$\begingroup\$ im using .net core 3.1 \$\endgroup\$
    – Roveldo
    Apr 13, 2021 at 18:16

1 Answer 1

1
\$\begingroup\$

AppDomain

The most established and bulletproof method for plugin isolation is to use AppDomain. Anything allocated withing one AppDomain will be disposed together when AppDomain unloads or reloads.

As a downside, communication between domains (the default and isolated one) is tricky, because it requires serialization and deserialization, which may lead to performance issues and coding inconvenience bottleneck.

More importantly for your case, AppDomain is not fully supported in .Net Core — only events will work, but not a domain management. Although, the support is back in .NET 5 (and later).

AssemblyLoadContext and disposable proxies

AssemblyLoadContext is the second preferable option. It is much easier for communication, but it also makes to easy to cause a memory leak.

In my knowledge, absolute most leaks are caused by registering references inside a long-lived object, the most common example being events such as in singletons or application services.

The straightforward solution is to not allow any short-lived code to register itself with a long-lived object. Instead, short-lived code should be provided with a reference to a short-lived proxy that allows same registrations. The proxy subscribes to actual underlying service and redirects (or dispatches differently) the events to the short-lived clients.

When game session is finished, the proxy must be disposed, making whole "island" of objects to go away at once. The role of the proxy is to ensure that all the registration is happening in one place, so there is no need to rely on plugin developers not forgetting to unsubscribe (and not skipping it due to bug).

    public class EngineServices : IDisposable
    {
        public event Action<Foo> FooEvent(Foo foo);
        public IEnumerable<Baz> BazRegistry => _bazRegistry;

        public EngineServices(ActualEngineServices actual)
        {
            _actual = actual;
            _actual.FooEvent += RelayFoo;
        }

        public void Dispose()
        {
            _actual.FooEvent -= RelayFoo;
        }

        public void Register(Baz baz) => _bazRegistry.Add(baz);

        ActualEngineServices _actual;
        List<Baz> _bazRegistry = new(); // come to the darkside, we have C#9

        void RelayFoo(Foo foo) => FooEvent?.Invoke(foo);
    }

You still may want to scan the object graph to find possible escape routes.

Sandbox process

As a last resort, it is possible to use dedicated process for plugin isolation. Setting it up would be slightly harder, communication could be slower (and not without a lag), but otherwise it almost the same as AppDomain.

One interesting feature of a separate process is that it can crash itself without crashing the parent application.

Gameplay process

If you are feeling adventurous, design the main process to be a fancy-looking launcher for a game session. Same as the previous method, but allows gameplay objects to be coupled with the engine. Bonus points for making process switching seamless. Looking at you, Noita.

\$\endgroup\$
7
  • \$\begingroup\$ Thank you for the extensive answer you posted.I'll take my time to digest this.However separate process took my attention the most.It seems familliar with how unity do things(when unity script crashes in editor play mode whole application wont shutdown).I feel this the way i'll go.I have my editor project separate from the actual engine thus it should be easy to implement such a desing. \$\endgroup\$
    – Roveldo
    Apr 13, 2021 at 20:33
  • \$\begingroup\$ Any idea on using Mono.Cecil ? \$\endgroup\$
    – Roveldo
    Apr 14, 2021 at 5:50
  • \$\begingroup\$ @Roveldo Forgot to add that Unity is using AddDomain, it's also was part of a reason why Unity was stuck on .NET Framework — forum.unity.com/threads/… \$\endgroup\$ Apr 14, 2021 at 9:04
  • \$\begingroup\$ @Roveldo Mono.Cecil is for manipulating assembly files, I don't see how can it help with isolation. \$\endgroup\$ Apr 14, 2021 at 9:06
  • \$\begingroup\$ yeah it seems 'Mono.Cecil' is not suitable for my purposes(yet).I found this link docs.unity3d.com/Manual/ConfigurableEnterPlayModeDetails.html and its look like a basic 'AppDomain' load&unload process.I guess i will .net instead of .net core for the time being. \$\endgroup\$
    – Roveldo
    Apr 14, 2021 at 10:10

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .