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.
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
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;
}
}
}
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
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; } }
}
}
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