Unloadable plugins

by adrian vintu 5/15/2008 10:13:00 PM

Creating a plugin architecture in .net is pretty easy. Unless you need to unload the plugins - AppDomain.Unload just does not work. Unfortunately I have found little help on this issue ~ google ~ so I decided to write this article.

A few basics:

1. .net uses application domains - kind of VM isolation in Java 

2. multiple app domains can exist into a single process 

3. code running in on app domain cannot directly access code/resources in another app domain - they are isolated.

4. code failures in one app domain cannot affect other app domains

5 . individual assemblies cannot be unloaded, only whole app domains


In our case we will use a process containing a main domain - the main application - and another one where to load/unload plugins. It should look like this:


The main domain contains standard assemblies - mscorlib, System, etc - plus our plugins.dll module - this is the main application.



We now create another app domain in our code using:

AppDomain appDomainPluginA = AppDomain.CreateDomain("appDomainPluginA");

The domains should look like this:



We now add the assembly pluginA.dll to the newly created app domain.

Assembly pluginAassembly = appDomainPluginA.Load("pluginA"); 

 


The result of this is somehow bizarre. You would expect that appDomainPluginA would load the pluginA and that's all. Well, the result is a little bit different. The assembly pluginA also gets loaded into the main appdomain, the plugins main app domain.


This (somehow unexpected) behavior is documented. You can check it out on the MSDN at http://msdn.microsoft.com/en-us/library/36az8x58.aspx

Here is the quote from MSDN: "If the current AppDomain object represents application domain A, and the Load method is called from application domain B, the assembly is loaded into both application domains."

Well, this is all ok until we need this very important functionality: to be able to unload the plugin.

Thing is, once you load the assembly in an app domain, you cannot unload it until you unload the whole app domain. In our case it means that if we want to unload pluginA we have to unload the plugins app domain i.e. our main application - in other words, we have to kill the application. But this is certainly not what we want.

Using AppDomain.Unload in this case will not work:

//The app domain cannot be unloaded. This is by .net design.
//This is because pluginA has been loaded into AppDomain.CurrentDomain and cannot be released
//until AppDomain.CurrentDomain it's self is unloaded - i.e. at end of program.

AppDomain.Unload(appDomainPluginA);

So we need to find a way to load and unload an appdomain and not let the assemblies inside leak into the main app domain.

 

This is where MarshalByRef comes into the scene. It allows us to cross app domain boundaries. So, instead of us calling the load of the plugin from the main domain, we execute the code in the second domain.

Here is how it looks like: 

AppDomain appDomainPluginB = AppDomain.CreateDomain("appDomainPluginB");


We add a class (RemoteLoader) in the appDomainPluginB that will handle the Assembly.Load, mark it as MarshalByRefObject so that we can break the domain boundaries so we can execute the code in the context of appDomainPluginB and not plugins domain.

RemoteLoader loader = (RemoteLoader)appDomainPluginB.CreateInstanceAndUnwrap("plugins", "plugins.RemoteLoader");


This basically means we have an instance of RemoteLoader in appDomainPluginB but we can access it in plugins app domain.

Now we let the instance from appDomainPluginB load the pluginB.dll (into appDomainPluginB, of course):

loader.LoadAndExecute("pluginB");


Now the code in RemoteLoader in appDomainPluginB loads the assembly pluginB.dll and the assembly does not leak anymore into the plugins domain.



The sample in this article uses reflection to scan for and execute the classes/types that implement the IPlugin interface defined in pluginsCommon.dll.

 public interface IPlugin
 {
    string Execute(string input);
 }

RemoteLoader uses reflection to find the above mentioned types, and when found will execute the IPlugin.Execute method. This means that the IPlugin type info will actually be loaded into appDomainPluginB.

foreach (Type type in pluginAassembly.GetTypes())
{
  if (type.GetInterface("IPlugin") != null)
  {
    object instance = Activator.CreateInstance(type, null, null);
    string s = ((IPlugin)instance).Execute("test");
    Console.WriteLine("Return from call is " + s);
  }
}

So the app domains actually look like this:



When we are finished, we can use AppDomain.Unload to unload the domain:

//The app domain will be unloaded.
//This is because pluginB has been loaded into appDomainPluginB but not in AppDomain.CurrentDomain.
//This means that AppDomain.CurrentDomain does not link/point to pluginB.

AppDomain.Unload(appDomainPluginB);

This will now work fine, because none of the assemblies leaked into the main plugins app domain - they are nowhere linked, so they can be released. 

 

It now looks like we have an easy way to load and unload plugins in .net. It is simple, but the documentation on the net is unfortunately very scarce on this.

I hope you enjoyed the article and please share it if you found it usefull.

 

PS I am adding here a short copy paste of the code so people don't have to download the whole sample.

static void LoadPluginsInsideNewDomainUsingMarshalByRef()
{
    AppDomain appDomainPluginB = AppDomain.CreateDomain("appDomainPluginB");

    RemoteLoader loader = (RemoteLoader)appDomainPluginB.CreateInstanceAndUnwrap("plugins",
        "plugins.RemoteLoader");

    loader.LoadAndExecute("pluginB");

    //The app domain will be unloaded.
    //This is because pluginB has been loaded into appDomainPluginB but not in AppDomain.CurrentDomain.
    //This means that AppDomain.CurrentDomain does not link/point to pluginB.
    AppDomain.Unload(appDomainPluginB);
}

public class RemoteLoader : MarshalByRefObject
{
    public void LoadAndExecute(string assemblyName)
    {
        Assembly pluginAassembly = AppDomain.CurrentDomain.Load(assemblyName);

        foreach (Type type in pluginAassembly.GetTypes())
        {
            if (type.GetInterface("IPlugin") != null)
            {
                object instance = Activator.CreateInstance(type, null, null);
                string s = ((IPlugin)instance).Execute("test");
                Console.WriteLine("Return from call is " + s);
            }
        }
    }
}

Download sources: plugins.zip (67.48 kb)


Related posts

Comments

5/30/2008 3:25:58 AM

Eric Schneider

The is a new Addin namespace in .NET 3.5 :

msdn.microsoft.com/en-us/library/system.addin.aspx

Eric Schneider

5/30/2008 4:51:24 AM

adrian vintu

That looks very interesting, I will check it out. Thank you for the tip.

adrian vintu

5/30/2008 10:33:47 AM

pingback

Pingback from comsa-soho.dyndns.info

Unloadable plugins

comsa-soho.dyndns.info

9/24/2008 3:35:44 AM

adrian vintu

I haven't done any tests. Would be interesting to know though. If you have some benchmarks I would be glad to hear the results Smile

adrian vintu

1/24/2009 4:07:30 PM

LockeVN

Thanks for your tip, it work fine. It's more easy and simple than MS System.AddIn.

LockeVN

Comments are closed

About Adrian Vintu

Adrian Vintu

Adrian Vintu is an old timer who has had the wonderful experience of working in a variety of quality environments and with various quality people.

Throughout time he has got down with Assembler, C++, Borland Delphi, Java, C#, Android etc. He took part in projects concerning software security, industrial controllers, food and health-care ERPS, AI simulations, data mining, mobile development, computer games, augmented reality, online distributed transactions, and financial management and trading.

In his spare time he develops free educational applications that run on Android and Windows Phone. It is his way of thanking and giving back to the open source and free (as in beer) software community and a way of bringing value to the social community and young generation.

Send mail

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2008 - 2014

Sign in