2012-04-01

Run-time assembly loading from SD card

I always thought .NET reflection was cool. To expose knowledge of types and ability to manipulate code itself at run-time is one of the most amazing tools a code designer can be given. So, when a question was posed on how to use reflection to load different pieces of code from a storage device, at run-time, through an interface, I took it upon myself to figure it out.

In order to achieve the goal, I set up two separate solutions in VS2010. This ensures that the code from one of my solutions can only be distributed by me, by copying it to an SD card.

The main program solution was a Gadgeteer Application. In it, I added an additional Class Library project called CommonInterfaceLibrary. This project had only one file in it, containing only one interface declaration.
namespace CommonInterfaceLibrary
{
    public interface IDataStorage
    {
        string Data { get; }
    }
}
This interface is my "contract" for any types I load from an external assembly.

The bulk of the main application code is contained in the following method.
void LoadAndExecuteExternal()
{
    if ( !sdCard.IsCardInserted )
    {
        Debug.Print( "Card not inserted." );
        return;
    }
    if ( !sdCard.IsCardMounted )
    {
        sdCard.MountSDCard();
        if ( !sdCard.IsCardMounted )
        {
            Debug.Print( "Card could not be mounted." );
        }
        return;
    }

    // this is a bit of an assumption here, but it works for the test
    if ( !VolumeInfo.GetVolumes()[ 0 ].IsFormatted )
    {
        Debug.Print( "Card not formated" );
        return;
    }

    // open a file stream to the test assembly. if this were not a test,
    // we could search/load all *.pe files, but the essence of the
    // given code would remain the same
    string rootDirectory = sdCard.GetStorageDevice().RootDirectory;
    FileStream fileStream = new FileStream( rootDirectory + @"\TestLibrary.pe", FileMode.Open );

    // load the data from the stream
    byte[] data = new byte[ fileStream.Length ];
    fileStream.Read( data, 0, data.Length );

    fileStream.Close();

    // now generate an actual assembly from the loaded data
    System.Reflection.Assembly testAssembly = System.Reflection.Assembly.Load( data );

    // find an object in the loaded assembly that implements
    // our required interface
    CommonInterfaceLibrary.IDataStorage dataStorage = null;
    Type[] availableTypes = testAssembly.GetTypes();
    foreach ( Type type in availableTypes )
    {
        Type[] interfaces = type.GetInterfaces();
        foreach ( Type i in interfaces )
        {
            // not sure if there is a better way of comparing
            // Type to actual CommonInterfaceLibrary.IDataStorage
            if ( i.FullName == typeof( CommonInterfaceLibrary.IDataStorage ).FullName )
            {
                // if we found an object that implements the interface
                // then create an instance of it!
                dataStorage = (CommonInterfaceLibrary.IDataStorage)
                    AppDomain.CurrentDomain.CreateInstanceAndUnwrap(
                        testAssembly.FullName, type.FullName );
                break;
            }
        }

        // if we created an instance
        if ( dataStorage != null )
        {
            // use it!
            Debug.Print( dataStorage.Data );
            break;
        }
    }
}
That's it! We load an assembly. Fetch all types from it. For each one, we fetch all the interfaces they implement. Then compare each of those to the interface we previously declared. Once we found an object that implements our interface, we create an instance of it and use it.

The remote solution is also a definition of simplicity. It has a single project in it. The only requirement is that the project reference our CommonInterfaceLibrary class library. You do that by right clicking on References folder, select Add Reference, then Browse to the Debug (or Release) folder of the CommonInterfaceLibrary project. Make sure not to go into 'le' or 'be' folders. Once the reference to the assembly DLL file is created, you can declare the interface type in your remote code. In my test code, I created and implemented an additional interface, to better demonstrate reflection in the application code.
namespace TestLibrary
{
    public interface IConfusingInterface
    {
        int Data { get; }
    }

    public class DataStorage : IConfusingInterface, IDataStorage
    {
        string data = "foo";

        string IDataStorage.Data { get { return data; } }

        int IConfusingInterface.Data { get { return 5; } }
    }
}
To test it, compile the main application project. Then compile the satellite class library. Copy the .pe file for the library (e.g. TestLibrary.pe) onto an SD card. Plug the SD card into the Gadgeteer SD slot and execute the above method.

If you wish to test the main application's agnosticism towards the loaded assembly, go back into the satellite assembly solution, change the data string to something else (e.g. "bar"). Make sure to change the assembly version number as well. Compile, copy onto and insert the SD card back into Gadgeteer. Execute the above function again. VoilĂ !

The only gotcha that I found, was the assembly version. If you don't change the assembly version, the running instance of the Gadgeteer will somehow cache the old assembly, internally. And even if you go through the process of loading a newly compiled version, it will end up using the old one. The only way you can ensure the new assembly is loaded fresh, is to change its version number. I would love to hear about alternative solutions to this problem (i.e. how to flush an assembly that is no longer used).

No comments:

Post a Comment