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)

Foreach Loop VS For Loop

by adrian vintu 4/23/2008 11:42:00 PM

What do you suppose happens when this piece of code is run? Please take notice that the secondary foreach loop is using the same dataset.

DataSet dsMain = GetMainDataset();

foreach (DataRow dr in dsMain.Tables[0].Rows)
{
    Console.WriteLine(dr["ID"] + " | " + dr["NAME"]);

    dsMain = GetSecondaryDataset();

    foreach (DataRow drSecondary in dsMain.Tables[0].Rows)
        Console.WriteLine(drSecondary["DETAIL"]);
}

Will it generate a run time error? Infinite loop? Half the results? Is the DataRow variable dr equal to drSecondary? Is dsMain the same all over the code?

And, of course, WHY?


Well, let's take it step by step.


We first step through the first foreach loop and notice the dsMain pointer is 15335436 - badly shown in the photos in the watch.


The dataset dsMain contains 2 columns: ID and NAME.


We step after the

dsMain = GetSecondaryDataset();

and see that the pointer has changed to 15400908 - the assignment was successful and we have a new dataset.


The dataset dsMain now contains one column called DETAIL.


At this point we can raise the question: what happens after this inner foreach finishes and the loop goes back to the outer one?

Will the outer foreach loop end because the rows of the dataset were run to the end by the inner foreach loop?

Will the outer foreach loop crash because 

Console.WriteLine(dr["ID"] + " | " + dr["NAME"]);

is looking for columns ID and NAME that don't exist in dsMain dataset - remember the previous step where dsMain became a new dataset, with only one column DETAIL?


In order to answer our question, we need to look at the disassembly.

 


Did you catch the idea? The foreach loops are not using direct pointer to the dataset dsMain rows collection, but rather pointers to the enumerator to the rows collection. And the enumerator enumerates through the collection regardless of what happens to the dsMain pointer.

So the answer is: the code will work fine, because foreach loops use the (pointers to the) enumerators of the rows collection, not pointers to the rows.


Let's see what happens when we use the normal for loop.

DataSet dsMain = GetMainDataset();

for (int i = 0; i < dsMain.Tables[0].Rows.Count; i++)
{
    DataRow dr = dsMain.Tables[0].Rows[i];

    Console.WriteLine(dr["ID"] + " | " + dr["NAME"]);

    dsMain = GetSecondaryDataset();

    foreach (DataRow drSecondary in dsMain.Tables[0].Rows)
        Console.WriteLine(drSecondary["DETAIL"]);
}


Explained by the red highlights:

1. the address of the dataset is at index 0 - location 0

2. the dataset is built outside the outer for loop and stored at location 0 - as expected.

3. the dataset is loaded from location 0 by the for loop

4. in the for loop, just before the inner foreach loop, we create a new dataset and store it at location 0, overwriting the previous pointer - same behaviour as in the double foreach example above.

5. the for loop returns to address L_000e which is our point 3.


What happens, is that the dataset containing the DETAIL column (which is stored at location 0) is used by the for loop. The for loop will continue and try to access the second row in the dataset. This will work.

What won't work is the

Console.WriteLine(dr["ID"] + " | " + dr["NAME"]);

because the row is accessed through the location0.row[i] and this row belongs to the dataset containing the DETAIL column, not the ID and NAME. 


I hope you have enjoyed this article as much as I have, and I hope things are now clearer regarding the differences between the for and foreach loops.


As an extra piece of information, in the first example, after the code has been run 1once, and it's going the second time through the outer foreach loop, the Visual Studio 2k5 and 2k8 watch will show the contents of dsMain as the contents of the overwriting dataset i.e. the one containing the DETAIL column. This makes perfect sense actually.

You can see the contents of the ~right~ dataset/table though, by going to the dr variable in the watch and looking for the Table property.

 

PS This article is a well known/self understood for experienced developers, but I do sometimes get strange faces when asking people about the example I described.

Powered by BlogEngine.NET Original theme by Mads Kristensen modified by Adrian Vintu


Advertisments

Disclaimer

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

© Copyright 2008

Sign in