Introduction
The .NET Framework has great features to support compiling code at runtime and running it, to achieve some kinds of scripting functionalities. There are already some scripting engines available here on CodeProject, and Microsoft has its own library as well.
Unfortunately, none of the projects I've found on the net satisfied my requirements. My plan was to make a flexible service that could run some pre-defined tasks that should be changeable without having a compiler. As I said, the scripting libraries I found could compile and run the scripts, but every single one had at least one of the following disadvantages:
- Support for VB.NET only (no C#)
- They blow up memory
- They have to be configured by the application that runs the script
- They don't support scripts with more than one file
This article is the result of my decision to write my own library :)
Background
The main problem of runtime-compiling/running is that any assembly that is loaded needs memory, and once loaded, an assembly can not be unloaded again.
To solve this problem, I use a separate AppDomain to compile/run the scripts. The dotnetScriptor
class unloads and reloads that AppDomain if required, blocks the script executions while the reload is in progress, and the ScriptRunner
class holds the compiled assemblies, rebuilds them if required, and informs d
otnetScriptor
whether a restart is required or not.
The usage of Remoting and the restart capabilities brings new problems:
MarshalByRefObject
proxies have a limited life-time.
- If a
MarshalByRefObject
member-function takes a serialized parameter, eventual changes will not apply.
The life-time problem is solved with the ScriptSponsor
class. If a MarshalByRefObject
derived object is passed that leaves the Appdomain that holds the original object, dotnetScriptor
or ScriptRunner
will put a ScriptSponsor
on it.
The serializable problem is solved by the MarshalSerializer
class. I know this approach could solve the life-time problem as well, but sometimes, a script needs to interact with the main application.
The MarshalSerializer
must be initialized by the host-application and will be explained later, the life-time sponsorship works automatically.
Using the code
The usage of dotnetScriptor
in an application is quite easy. Take a look at this little class here:
public class ScriptorClient
{
private dotnetScriptor scriptor;
public ScriptorClient()
{
ArrayList list = new ArrayList(new string[]{"Text1","Text2","Text3"});
dotnetScriptor.RebootTimeout = TimeSpan.FromMinutes(10);
scriptor = new dotnetScriptor();
scriptor.PopulateObject("Parameters",list);
scriptor.PopulateObject("MyNonProxyObject",
new MarshalSerializer(typeof(SomeMarshalByRefObjectClass),
"Param1","Param2",3,4L));
}
public void RunMyScript()
{
scriptor.RunScript(@"C:\MyFooScript.cs");
}
}
If the script does not change, it will not be re-compiled. Only if you change the script, will ScriptRunner recompile it. If you keep changing the script all the time, dotnetScriptor
will restart in a 10-minute frequency, but the changes are applied instantly. Memory will blow up within these 10 minutes, and go down after reboot.
The additional populated object ("MyNonProxyObject") is a wrapper object that will be converted into an object of SomeMarshalByRefObjectClass
by using the appropriate constructor. This won't be used in most of the cases. But, it's useful if you have a MarshalByRef
object with a member function that takes a serializable object as a parameter and changes something in it (i.e., it takes an ArrayList
and adds an item or something...).
How to script
The scripts has two parts:
- configuration
- script
The configuration part is an XML that is terminated by a '!' followed by 70 '-'s like this:
There are several options in the XML:
EntryPoint
is the fully qualified (Namespace.Classname.FunctionName) main function of the script. It must be a static void
, and either parameterless, or with one parameter of type ScriptRunner
.
Reference
references an assembly. It's either a path or an assembly name.
Language
tells ScriptRunner which compiler you want to use, VB or C#.
RefScript
adds a file you want to compile with the script.
Flag
sets CompilerParameters
to the ICompiler
used for the script compilation.
Debug
turns the debug mode on this tag. If it doesn't take parameters, the usage is simply: <Debug/>
.
The script part is just a simple C# or VB.NET file:
using System;
using System.Collections;
using dotnetScriptor;
namespace Test
{
public class TestScript
{
private static ScriptRunner myRunner;
public static void RunMe(ScriptRunner MyRunner)
{
Console.WriteLine("Hello World");
}
}
}
Points of Interest
The main part of the engine, compiling, and running the scripts were pretty easy to code, and should be easy to understand.
The more time consuming for me was the sponsor handling. I never used them before because I didn't have Remoting sessions that would take so long that it would make a difference, or I simply set the initial lifetime to one day, which was mostly enough. But for the scripting, I thought a dynamic lifetime lease handling would be more appropriate.