|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionFrequently, developers discover that they have very specific requirements given by clients which would benefit only a very small group of people, and it becomes hard to justify the inclusion of features that meet only their requirements within the main codebase. Code bloat and feature creep can be dangerous for many reasons which we won't address here, but generally, the best solution to the problem is to allow external plug-ins to address those specific requirements without causing the code to become unmanageable. .NET allows the dynamic loading of assemblies that is necessary for a good plug-in implementation. Runtime compilation can sometimes be a good alternative, especially when the end users are likely not to have development tools available to them, but there are some sacrifices in performance, and if the end users do happen to have access to Visual Studio, they will quite likely discover that they cannot use many of the context-sensitive tools that the IDE provides. Also, obviously, anyone can look at the code for a runtime compiled script, so deployment of uncompiled code containing trade secrets to a web server may be a security risk that some are not willing to take. Likewise, if plug-ins are to be sold by third parties, uncompiled scripts are really unsuitable. If a plug-in system using dynamic assembly loading is the best solution, developers wishing to implement one have several hurdles to overcome first, not the least of which is poor documentation related to the classes and methods required to carry out the task. A fairly extensive knowledge of the inner workings of the .NET architecture is required to successfully build such a system. Unfortunately, there are precious few articles currently in existence on the subject, and the ones that do exist do not fully explain the subject. This article attempts to fill that void, as well as provide a solution for both dynamically loading plug-in assemblies and runtime compilation of plug-in scripts. Major ObstaclesIn .NET, assemblies that are loaded cannot be directly unloaded, and with good reason. There is no So instead, we'll be looking at a method that extensively uses reflection to locate and make the plug-in types available to the main application code. Traditionally, plug-in managers have required plug-in authors to implement a specific interface (generally named something like In order to allow loaded plug-ins to be overridden with newer versions, we must make use of a special feature called shadow copying. If you set up shadow copying for an The CodeThe major issues mentioned above affect several places in the code, and we'll take a closer look at them now. First off, the construction of the secondary /// <summary>
/// Creates the local loader class
/// </summary>
/// <param name="pluginDirectory">The plugin directory</param>
/// <param name="policyLevel">The security policy
/// level to set for the plugin AppDomain</param>
public LocalLoader(string pluginDirectory, PolicyLevel policyLevel)
{
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationName = "Plugins";
setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
setup.PrivateBinPath =
Path.GetDirectoryName(pluginDirectory).Substring(
Path.GetDirectoryName(pluginDirectory).LastIndexOf(
Path.DirectorySeparatorChar) + 1);
setup.CachePath = Path.Combine(pluginDirectory,
"cache" + Path.DirectorySeparatorChar);
setup.ShadowCopyFiles = "true";
setup.ShadowCopyDirectories = pluginDirectory;
appDomain = AppDomain.CreateDomain(
"Plugins", null, setup);
if (policyLevel != null)
{
appDomain.SetAppDomainPolicy(policyLevel);
}
remoteLoader = (RemoteLoader)appDomain.CreateInstanceAndUnwrap(
"PluginManager",
"rapid.Plugins.RemoteLoader");
}
We set the /// <summary>
/// Initializes the plugin manager
/// </summary>
public void Start()
{
started = true;
if (autoReload)
{
fileSystemWatcher = new FileSystemWatcher(pluginDirectory);
fileSystemWatcher.EnableRaisingEvents = true;
fileSystemWatcher.Changed += new
FileSystemEventHandler(fileSystemWatcher_Changed);
fileSystemWatcher.Deleted += new
FileSystemEventHandler(fileSystemWatcher_Changed);
fileSystemWatcher.Created += new
FileSystemEventHandler(fileSystemWatcher_Changed);
pluginReloadThread = new
Thread(new ThreadStart(this.ReloadThreadLoop));
pluginReloadThread.Start();
}
ReloadPlugins();
}
We deal with the /// <summary>
/// Loads the assembly into the remote domain
/// </summary>
/// <param name="fullname">
/// The full filename of the assembly to load</param>
public void LoadAssembly(string fullname)
{
string path = Path.GetDirectoryName(fullname);
string filename = Path.GetFileNameWithoutExtension(fullname);
Assembly assembly = Assembly.Load(filename);
assemblyList.Add(assembly);
foreach (Type loadedType in assembly.GetTypes())
{
typeList.Add(loadedType);
}
}
This piece of code resides in the /// <summary>
/// Returns a proxy to an instance of the specified plugin type
/// </summary>
/// <param name="typeName">The name of the type to create an instance of</param>
/// <param name="bindingFlags">The binding flags for the constructor</param>
/// <param name="constructorParams">The parameters to pass
/// to the constructor</param>
/// <returns>The constructed object</returns>
public MarshalByRefObject CreateInstance(string typeName,
BindingFlags bindingFlags, object[] constructorParams)
{
Assembly owningAssembly = null;
foreach (Assembly assembly in assemblyList)
{
if (assembly.GetType(typeName) != null)
{
owningAssembly = assembly;
}
}
if (owningAssembly == null)
{
throw new InvalidOperationException("Could not find" +
" owning assembly for type " + typeName);
}
MarshalByRefObject createdInstance =
owningAssembly.CreateInstance(typeName, false, bindingFlags, null,
constructorParams, null, null) as MarshalByRefObject;
if (createdInstance == null)
{
throw new ArgumentException("typeName must specify" +
" a Type that derives from MarshalByRefObject");
}
return createdInstance;
}
Here, the /// <summary>
/// Returns the value of a static property
/// </summary>
/// <param name="typeName">The type to retrieve
/// the static property value from</param>
/// <param name="propertyName">The name of the property to retrieve</param>
/// <returns>The value of the static property</returns>
public object GetStaticPropertyValue(string typeName, string propertyName)
{
Type type = GetTypeByName(typeName);
if (type == null)
{
throw new ArgumentException("Cannot find a type of name " + typeName +
" within the plugins or the common library.");
}
return type.GetProperty(propertyName,
BindingFlags.Public | BindingFlags.Static).GetValue(null, null);
}
Tragically, we have to write a custom method for accessing static properties and methods. It would have been nice if this could have been avoided. In any case, this code is pretty self-explanatory. It checks to make sure the type is indeed a plug-in type and then calls /// <summary>
/// Generates an Assembly from a list of script filenames
/// </summary>
/// <param name="filenames">The filenames of the scripts</param>
/// <param name="references">Assembly references for the script</param>
/// <returns>The generated assembly</returns>
public Assembly CreateAssembly(IList filenames, IList references)
{
string fileType = null;
foreach (string filename in filenames)
{
string extension = Path.GetExtension(filename);
if (fileType == null)
{
fileType = extension;
}
else if (fileType != extension)
{
throw new ArgumentException("All files" +
" in the file list must be of the same type.");
}
}
// ensure that compilerErrors is null
compilerErrors = null;
// Select the correct CodeDomProvider based on script file extension
CodeDomProvider codeProvider = null;
switch (fileType)
{
case ".cs":
codeProvider = new Microsoft.CSharp.CSharpCodeProvider();
break;
case ".vb":
codeProvider = new Microsoft.CSharp.CSharpCodeProvider();
break;
case ".js":
codeProvider = new Microsoft.JScript.JScriptCodeProvider();
break;
default:
throw new InvalidOperationException(
"Script files must have a .cs, .vb," +
" or .js extension, for C#, Visual Basic.NET," +
" or JScript respectively.");
}
ICodeCompiler compiler = codeProvider.CreateCompiler();
// Set compiler parameters
CompilerParameters compilerParams = new CompilerParameters();
compilerParams.CompilerOptions = "/target:library /optimize";
compilerParams.GenerateExecutable = false;
compilerParams.GenerateInMemory = true;
compilerParams.IncludeDebugInformation = false;
compilerParams.ReferencedAssemblies.Add("mscorlib.dll");
compilerParams.ReferencedAssemblies.Add("System.dll");
// Add custom references
foreach (string reference in references)
{
if (!compilerParams.ReferencedAssemblies.Contains(reference))
{
compilerParams.ReferencedAssemblies.Add(reference);
}
}
// Do the compilation
CompilerResults results = compiler.CompileAssemblyFromFileBatch(
compilerParams,
(string[])ArrayList.Adapter(filenames).ToArray(typeof(string)));
// Do we have any compiler errors
if (results.Errors.Count > 0)
{
compilerErrors = results.Errors;
throw new Exception(
"Compiler error(s) encountered" +
" and saved to AssemblyFactory.CompilerErrors");
}
Assembly createdAssembly = results.CompiledAssembly;
return createdAssembly;
}
When batch compiling scripts, the first thing we need to do is ensure that all scripts are of the same type. Mixing and matching VB and C# in one Then we take the extension that is shared by all of the files and switch on it to choose the appropriate Points of InterestThis was not as easy as it might look. This article was the culmination of something close to four weeks of rather frustrating research and fiddling. Lessons learned: wait until some other enterprising individual comes up with a solution when faced with a technical problem of this level of ridiculousness. History
| ||||||||||||||||||||