Disclaimer: The information in this article & source code is published in
accordance with the final (V1) bits of the .NET Framework
Contents
Abstract
This sample shows how to compile C# source-code on-the-fly, and how to use this
in a plug-in / plug-out component model.
Compilation
The namespace Microsoft.CSharp
located in the .NET component "Managed C#
Compiler" (cscompmgd.dll) has a Compiler
class with the Compile()
method to parse C# code in-process:
public static CompilerError[] Compile(
string[] sourceTexts,
string[] sourceTextNames,
string target,
string[] imports,
IDictionary options
);
The LiveApp Windows Forms sample shows a TextBox
control
where the user can enter the C# source. Later we simply pass the TextBox.Text
string to Compile()
.
Plug-in
To make the plug-in as flexible as possible, we only use a predefined interface ILiveInterface
for any communications between the host and the plug-in.
Note we don't use any late-binding, reflection or Invoke mechanics, but true
interfaces!
This interface is compiled into a separate .NET library assembly, LiveInterface.dll.
To keep our example as simple as possible, this interface has just one method:
public interface ILiveInterface
{
string ModifyString( string inpString );
}
With ModifyString()
we pass in a string and get the modified
string back.
But note, the interface can have any number of methods with any kind of
parameters and return values. Although there may be some limitations for cross
app-domain communications.
Plug-out
Once loaded in an application, a .NET assembly is kept in memory. There is no
possibility to release just one single assembly. The only way to get this
effect is to unload the complete AppDomain
.
This limitation forces us to use some techniques:
- We have to create a separate (secondary)
AppDomain
to be unloaded later.
- Unfortunately, there is no activation method to create a class instance in another
AppDomain
and to only return just a specific interface to this
instance. We always get back an object reference, and this would force the
plug-in-assembly to be attached to our primary AppDomain
! If this happens, there
is no way to "plug-out" the plug-in-assembly later.
- To workaround this, I used a class-factory approach.
The class-factory is just a simple class also located in the same assembly LiveInterface.dll
like ILiveInterface
is. It provides one Create()
method
as a wrapper for the .NET Activator.CreateInstanceFrom()
call:
public class LiveInterfaceFactory : MarshalByRefObject
{
...
public ILiveInterface Create( string assemblyFile, string typeName,
object[] constructArgs )
{
return (ILiveInterface) Activator.CreateInstanceFrom(
assemblyFile, typeName, false, bfi, null, constructArgs,
null, null, null ).Unwrap();
}
The most important feature of the Create()
method is to only
return our interface, but NOT the object! This way, the only real connection
between the two app-domains is our well-known interface.
Finally, this lets us to unload an app-domain like:
AppDomain.Unload( secDom );
Sample App
The LiveCode.NET example solution contains this two projects:
- LiveInterface: interface & class-factory only assembly
- LiveApp: sample Windows Forms application for hosting plug-ins
You can see the LiveApp GUI on the screenshot at the top of this page.
The initial plug-in source-code is loaded from file InitialLiveClass.cs into
the TextBox
. The user can now modify this code, as long as the interface and
class-name is unchanged.
Another TextBox
lets the user type the string for the class constructor
parameter, and one more is for the ModifyString()
method
parameter.
If the user hits the Run! button, this executes these two steps:
private void btnRun_Click(object sender, System.EventArgs e)
{
try
{
DoCompile();
DoRun();
}
catch( Exception )
...
}
DoCompile()
just compiles passing the C# source, the referenced
assemblies and compiler options:
private void DoCompile()
{
string[] srcCodes = new string[] { textSourceCode.Text };
string[] referAsm = new string[] { "LiveInterface.dll" };
string outFile = "LiveAssembly.dll";
ListDictionary cplOpts = new ListDictionary();
cplOpts.Add( "target", "library" );
CompilerError[] ces = Microsoft.CSharp.Compiler.Compile( srcCodes,
srcNames, outFile, referAsm, cplOpts );
...
}
DoRun()
loads the new assembly into a secondary app-domain and executes
the method:
private void DoRun()
{
AppDomain secDom = AppDomain.CreateDomain( "SecondaryDomain" );
LiveInterfaceFactory factory = (LiveInterfaceFactory) secDom.CreateInstance(
"LiveInterface", "LiveCode.LiveInterface.LiveInterfaceFactory" ).Unwrap();
object[] constructArgs = new object[] { textConstruct.Text };
ILiveInterface ilive = factory.Create( "LiveAssembly.dll",
"LiveCode.LiveSample.LiveClass", constructArgs );
textReturned.Text = ilive.ModifyString( textParam.Text );
factory = null;
ilive = null;
AppDomain.Unload( secDom );
}
The string returned by ModifyString()
is made visible in the
bottommost TextBox
. All source code is included in the download. With
VS.NET, just load the solution \LiveApp.sln
MultiPlug sample
Another sample is included to show a generic use of plug-ins. It is just a
visualisation of the presented component concept. You can interactively create
application domains, load assemblies and instantiate plug-in classes. Finally,
you could release the instances and unload a complete AppDomain.
An important hint if you extend the component model: Please be sure to specify
the class interface inheritance list by placing the plug-in interfaces at the
beginning:
public class YourPluginClass : MarshalByRefObject, IPlugInterface,
IYourLocalInterface
If you write the wrong order:
public class YourPluginClass : MarshalByRefObject, IYourLocalInterface,
IPlugInterface
your plugin-assembly will be attached to the primary domain and thus be locked.
Limitations
Microsoft.CSharp.Compiler.Compile
needs the multithreaded apartment => [MTAThread]
- plugin namespace- and class names have to be fixed or e.g. 'registered' in any way.