Introduction
This project is an example of how to provide scripting support in your application without having to use a scripting host or create a new application domain.
Background
I've come across many articles on how to use C# as a scripting language. All have focused on either shell or runtime scripting. By runtime scripting, I mean scripts that can be modified at runtime with immediate effects in the application. My approach is slightly different. I compile scripts at runtime, but only once. If the user wants to change a script then the application has to be restarted for the changes to have effect. This is a limitation, but I don't feel that it is a serious one. The benefits are the following:
- No overhead for passing objects over application boundaries
- No need to specify interfaces between the script and your application
- Familiar OO-model for scripting
This scripting solution simply allows the user to extend the functionality of any class in your application. All code that is written in the script files will be treated as if it was part of the original code.
Using the code
Presuming that you have a class named ScriptedClass
, it's pretty dumb and doesn't utilize all info we are feeding it.
public class ScriptedClass
{
private string name;
public string Name
{
get
{
return name;
}
}
private int age;
public int Age
{
get
{
return age;
}
}
public ScriptedClass()
{
}
public ScriptedClass(string name, int age)
{
this.name=name;
this.age=age;
}
public virtual string Message
{
get
{
return string.Format("Hi {0}!", name);
}
}
}
To use the ScripterEngine
, you first have to initiate it. The Init
-method takes three arguments, one path to the directory that contains the script files, one path to the directory to use for temporary files, and a delegate used for progress:
ScripterEngine.Init(
string.Format("{0}/script", Application.StartupPath),
string.Format("{0}/tmp", Application.StartupPath),null);
Then you can create an instance of your class using either any of the constructors.
ScriptedClass clsa=
(ScriptedClass)ScripterEngine.CreateObject(typeof(MyClass),
Type.EmptyTypes,null);
ScriptedClass clsb =
(ScriptedClass)ScripterEngine.CreateObject(typeof(MyClass));
ScriptedClass clsc =
(ScriptedClass)ScripterEngine.CreateObject(typeof(MyClass),
new Type[]{typeof(string),typeof(int)},
new object[]{"Hugo",23});
If you, or a user, would like to extend the functionality of this class, you have to write a script.
First, we need a configuration file for the script. This file must be placed in the script directory together with the source file.
="1.0" ="utf-8"
<ScriptConfiguration name="Intelligent messenger" load="true">
<References>
<Reference>System.Web</Reference>
</References>
<Files>
<File>messenger.cs</File>
</Files>
<Types>
<Type>ScriptingArchitecture.ScriptedClass,
ScriptingArchitecture</Type>
</Types>
</ScriptConfiguration>
As you can see, it's plain XML. The References
tag contains names of assemblies that are necessary to compile the script. We don't really need the System.Web assembly, but I put it there to illustrate the syntax. All loaded assemblies are automatically used as references when compiling the script, so there isn't any need to add references to any basic assemblies.
The Files
tag contains all the source files that compose the script. There can be one or more files in a script.
The Types
tag contains all the types that are extended by the script. Note that the type must be specified using the full name of the type followed by the name of the assembly the type is defined in.
When we get this file, we can create the file for the actual script.
using ScriptingArchitecture;
using System.Windows.Forms;
using System.Drawing;
namespace Messenger
{
public class IntelligentMessage : ScriptingArchitecture.ScriptedClass
{
public IntelligentMessage():base()
{
}
public IntelligentMessage(string name, int age):base(name,age)
{
}
public override string Message
{
get
{
return string.Format("Hi {0}! You are {1} years old!", Name, Age);
}
}
}
}
Points of Interest
To make it possible to accommodate ad-hoc scripts form different sources, the scripting engine uses inheritance chains. If a bunch of scripts extends a particular class in your application, they must all co-exist. So, in this scenario where your class is A
:
B:A
C:A
D:A
E:A
The code is rewritten to:
B:A
C:B
D:C
E:D
And the class E
is supplied when a class of the type A
is requested.
What I miss in my scripting implementation is a more sugary syntax for creating scripted objects.
ScriptedClass clsc=(ScriptedClass)ScripterEngine.CreateObject(
typeof(MyClass),
new Type[]{typeof(string),typeof(int)},
new object[]{"Hugo",23});
Isn't exactly beautiful code. I would like something more along the lines with:
ScriptedClass clsc=new ScriptedClass("Hugo",23);
This can be fixed with a static
method in ScriptedClass
, but I would like a fix to this that is less of a hack.
public static ScriptedClass GetInstance(string name, int age)
{
return (ScriptedClass)ScripterEngine.CreateObject(
typeof(MyClass),
new Type[]{typeof(string),typeof(int)},
new object[]{name,age});
}
Caveats
There are some limitations to script code that does not exist in normal development:
History