Summary
This article describes a "script engine"
for the C# language. Here are some of the key features of
the presented scripting system:
- Based on the full featured C#.
- Full access to CLR and CTS.
- Can be run with any version of CLR (.NET
1.1/2.0), even potentially with future ones.
- Possibility of extending functionality
of any CLR application with "scripting", by
hosting the script engine in the application.
- Integration with common IDEs and debuggers.
- Integration with OS.
Introduction
There are very specific programming tasks
that require significant effort to accomplish by the traditional
development scenario:
design->coding->compiling->application.
On the other hand, it can be achieved quite
successfully by using some scripting languages. Scripting
can be very useful when:
- Development environment is changeable:
frequent code changes are expected.
- Application logic is simple:
application is needed for a very specific isolated task.
- Intellectual property protection
is not an issue: source code is open.
- Maintenance and deployment requirements
are very strict: SW solutions ideally reside in a
single file and is deployed by simple copying of minimal
number of files.
- Development speed is expected to
be faster than usual: code is composed of relatively
heavy chunks.
This article presents a “scripting engine”
for the C# language, which I have implemented to simplify
such usual tasks as configuring development or testing environment,
automating build procedure, automating testing, and collecting
test results… All the tasks that I, as an active C++/C#
programmer, do not normally like to do.
Background
Microsoft has not provided any adequate
scripting solution. VBScript was the language which they
expected to be used for scripting. Despite the fact that
VB functionality can be extended (with medium effort) by
creating COM components, its native functionality is extremely
limited and syntactic concept is badly designed. I believe,
a lot of people know that terrible aftertaste when working
with VBScript.
When .NET arrived, I expected that a script
engine for the C# language will solve this problem. However,
it did not happen. Microsoft did not provide a C# engine
for WSH.
And here is a reason given in MSDN:
Scripting has very little to do with
.NET for two good reasons. First and foremost, WSH relies
completely on COM. Second, the advantages you get from
.NET languages over scripting languages are substantial
in terms of performance and programming ease.
Microsoft provided only three scripting
engines with .NET: old VBScript and Jscript and also .NET
intermediate language (IL).
I do not know why C# is not there. A lot
of developers need a powerful scripting language not because
it does or does not fit some existing software concept but
because of all the reasons that I mentioned earlier (simple
and rapid development, frequent changes, effortless maintenance
and deployment). I consider the absence of a script engine
for C# as a Microsoft marketing mistake.
I was very excited when I saw Python for
the first time. Finally, there was a powerful and flexible
scripting solution. Here are some of the “hard to match”
Python features:
- Language is very rich.
- Possibility to pre-compile, and avoid
using compiling “on the fly”.
- Ability to do GUI development.
- Possibility to create objects (yes,
it is an OO language).
- Extensible by developing the modules
in the same language.
- Portable.
And the more I worked with Python, the
more I wanted to have something similar for C#. The idea
was in the air. Browsing the web, I have come across a few
attempts to tackle this problem. All the time, I felt it
was possible to use C# as a scripting language. One of the
reasons for this is that running an application under CLR
is very similar to running a script under a script engine.
In both cases, execution happens under the runtime system,
and in both cases, language interpretation takes place.
Another interesting thing about the .NET framework is that
the C# compiler can be hosted by an application. All this
eventually allowed me to implement a C# scripting engine.
This host application has nothing to do with WSH despite
some interface resemblance.
C# Script Engine
I wouldn’t like to spend too much time
describing the script engine application. Its code is available,
help yourself out. But still I have to say a few words about
it.
The design of such an application is straightforward.
Script execution consists of two steps: compilation and
execution.
Compilation part is trivial and a sample
code can be found in MSDN.
The more challenging task was to implement
the loading of all referenced assemblies. The general problem
with referenced assemblies is that it is not possible to
determine what assemblies they are by just analyzing the
code. Microsoft decided to handle this problem the easy
way: the user explicitly nominates such assemblies by adding
them into a special XML file (.csprj). I could not
use such an approach as I decided to use a plain vanilla
.cs file as the only input file for running the script.
My approach was based on the fact that in real life there
is a strong correlation between the assembly name and the
root namespace. For example, the namespace System.Windows.Forms
is implemented in the assembly System.Windows.Forms,
and the namespace System.XML implemented in
the assembly System.XML. This is also applicable
to the assembly file name, which is usually a DLL file with
the same name as the assembly name. Thus, I resolve namespaces
from the script into assemblies. And I do it for both global
(GAC) and local assemblies.
Unfortunately, there is no .NET API available
for navigating in the GAC, only the COM one. But after some
research, the problem was solved. I’d like to thank atoenne
(CodeProject), John Renaud (CodeProject), and Junfeng
Zhang for their very interesting articles regarding
working with the GAC. It helped me a lot.
Dealing with the local assemblies is as
interesting as with the global ones. I use the predicting
assembly file name on the base of the namespace as a starting
point. But as I mentioned before, there is only the correlation
between the namespace and the assembly name; there is no
true relationship. Things can get more complicated if the
assembly file name is different from the assembly
name. On top of this, the assembly root namespace
can have no resemblance to any of those names. Thus, it
is not possible to reliably predict what assembly is referenced.
The only way out of this is to apply Reflection to
the assemblies found in the script directory (current
directory). But still there is no guarantee that all
namespaces from the script can be resolved. That is why
I provided a "back door": the assembly can be
explicitly specified as a command line argument. But I have
never had to use it as, so far, all the namespaces were
resolved. The only restriction I have to impose is that
any assembly which is referenced in the script must
reside either in GAC or in the same folder where the script
is".
Basically, the script execution looks like
this: The user runs the script engine application and specifies
a script file as an argument. The script file is just a
.cs file with a defined method Main(...) .
The engine compiles the script into an assembly and executes
a well-known entry point in this assembly: “Main ”.
The advantages of such a scripting system are obvious, and
I will illustrate them by stating some of the features of
this new scripting system:
- Simple deployment approach: just
bring both script and engine files (about 30 K size) on
a system that has .NET runtime installed, and the script
can be run.
- Portability: Scripts can be run
on any system which has CLR installed.
- Base language is a truly OO language:
full featured C# is used. I call it “C# Script” but actually
it is “C#”. “C# Script” slightly better represents the
execution nature.
- All .NET functionality is available
(FCL, COM Interop, Remoting…).
- Easily available debugger and rich
IDE (MS .NET Studio).
- Execution model within the script
is the same as any .NET application:
static
void Main(…) .
- Any script can be easily converted
into an application and vice versa.
- Optimized interpretation. Interpretation
of any statement in the script is done only once even
if the statement is frequently used throughout the code.
- Script language is type safe.
Strong typing is a luxury not available for most of the
scripting languages.
- Dynamic typing is also available
for “die hard” VB lovers. Just go with
Object
as a default type and do boxing/unboxing all the time.
- All SW development tasks can be done
in the same language.
- GUI development for script applications
becomes easy.
- Extensibility: it can be extended
by using new assemblies written in any .NET language or
COM components.
Using C# Script
I have already done some development with
C# Script and found the system to be stable and reliable.
And here are some of the features of the script engine application
itself:
- Comes with two interfaces: WinForm and
console application.
- Can execute a standard .cs file
that has
Main() method defined.
- Can store compiled assembly file in
the same location as a script file and execute it next
time instead of compiling the script again if script was
not changed since last execution.
- Can generate template script file as
a starting point for further development.
- Can generate executable from the script.
Thus no script engine is required to run it again.
- Can generate assembly from the script
so it can be used as any other class library.
- Script engine is not sensitive to script
file extensions. The user can specify any file or file
name without extension (in this case, .cs will
be assumed).
- Script engine looks for the script file
in the directories specified as PATH environment variable,
if it is not found in the current directory.
Features added on user demand:
- Script engine can be hosted by any CLR
application. Assembly that implements the script engine
as a ClassLibrary is available as part of the downloadable
package.
- Script engine can load multiple scripts
(useful when one script uses functionality of another
one).
- Explicitly referenced assemblies can
be added not only from command-line but also directly
from code.
The script engine application is named
cscscript.exe (or cswscript.exe for WinForm
mode). The simplest way to prepare any script is to execute
from a command prompt:
cscscript /s > Sample.cs
This will generate the Sample.cs
file in the current directory. Sample.cs is a C#
source file. You will need to add any valid C# code to this
file to do your specific tasks. After that, it can be compiled
and executed by the script engine (command: cscscript.exe
sample.cs).
Sample.cs:
using System;
using System.Windows.Forms;
class Script
{
static public void Main(string[] args)
{
MessageBox.Show("Just a test!");
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine(args[i]);
}
}
}
Script execution:
Once again, script file contains ordinary
C# code. There is not even a single language statement that
would be understood only by the script engine but not by
any C# compiler (MS .NET Studio). This is one of the strongest
points of C# Script:
C# Script is not another flavor of C#. It is C#, which
is compiled and executed differently.
Any script file can also be opened, compiled,
and debugged with .NET Studio because it is just nothing
else but C#.
I have to say that I got more than I anticipated
at the start of this whole exercise. The script engine is
just an application (and relatively a primitive one). But
its functionality can be extended with scripts. The system's
main purpose is to execute scripts that can be improved
by other scripts. It may sound odd but it is exactly what
is happening. Such scripts can:
- ‘Install’ the script engine (publish
in EnvVar PATH).
- Insert any script file into a predefined
C# project and open it in .NET Studio. Thus, it is ready
to be run under the debugger.
- Create shell extensions to run or open
any script file in .NET Studio just with mouse right-click.
As you can see, the same script engine application is
much more capable now.
- Compose C# script on the fly from a
code fragment and execute it (E.g.: cscscript code
MessageBox.Show("Just a test!") will pop
up a message "Just a test!").
As you can see, the same script engine
application is much more capable now.
C# Script Package
You can download the whole ‘C# Script’
package which contains the script engine, useful scripts,
and the samples, from this page (at the start of the article)
or from my web page. To install, you will need to extract
everything from the ZIP file and execute install.bat,
and it is ready. Of course, .NET framework has to be installed
first. After the installation, you will have Environment
variables updated and all shell extensions created.
Strictly speaking, no installation is required.
The script engine application is self-sufficient and can
be run immediately after downloading. The only action performed
during the installation is an adjustment to the OS environment
in order to make scripting as convenient as possible.
Here is the list of some scripts (very
simple ones) that I have created and added to the package:
- NKill.cs - Kills processes specified
as command-line parameters.
- GetUrl.cs - Gets URL content
and saves it in a file. Can handle proxy authentication.
- Debug.cs - Opens script with
temporary C# project for running it under debugger.
- Install.cs - Installs/uninstalls
'C# Script' shell extensions that will allow to run/debug
any script with mouse right-click.
- ImportData.cs - Imports data
from specified file in SQL Server table.
- ExportData.cs - Exports data
to specified file from SQL Server table.
- RunScript.cs - Script that runs
another script in background with redirection of the output
to the 'parent script'.
- Tick.cs - Simple script that
counts specified number of seconds (used as demo for RunScript).
- SynTime.cs - Gets time from here
and synchronizes PC system time.
- MailTo.cs - Sends e-mail to the
specified address.
- Reflect.cs - Prints assembly
reflection info.
- Creditentials.cs - Prompts and
collects user login information.
- GetOptusUsage.cs - Retrieves
monthly data usage (applicable only for "Optus Australia"
customers).
- Interop.cs - Creates and accesses
COM and CLR objects.
- ImportTickScript.cs - Imports
(with '
import ' directive) and executes
tick.cs script.
- Client.cs Script.cs Common.cs
- Example of extending any application with CS-Script
scripting.
- And others...
Points of Interest
Previously, I had about a hundred of C#
projects on my hard drive, and in most of the cases, these
projects contained about 100-200 lines of code (just to
test some algorithm or code sample from MSDN). Now, I have
only a single .cs file for any coding exercise. Right-click
on it, and it is in the .NET Studio and ready to go. It
is so convenient!
There is another thing that I really like
about scripting. Development and testing can now be done
in the same programming language. Here is a simple example:
You need to test your assembly that does,
for example, printing. Under runtime conditions, the assembly
is used by the main application. However, you need
to do printing thousand times to get some statistics.
To do it from the main application would mean contaminating
the main application with testing code. What you can do
is copy the code that invokes the assembly from the main
application into the .cs file and run it with
the script engine. In this case, the assembly is tested
under the same conditions as at run-time, and the design
of the test is more elegant.
Therefore, the script engine combined with
the scripts becomes a small Development System.
Conclusion
C# scripting is not supposed to compete
with traditional C# development. They serve completely different
purposes. C# scripting is simply something that was missing
in the .NET family (that ‘missing puzzle piece’ in
the title).
I am interested to know what other people
think about it and would appreciate any feedback.
Feedback
I would like to thank all people who sent
me their suggestions and ideas. Some of them I have implemented
in the current version of the script engine.
One of the most asked questions is about
hosting the script engine. Just by accident I have found
some forum where I was criticised for underestimating the
importance of script hosting. I guess they are right. Despite
the technical possibility to host the script engine from
the very early versions (Math.cs and MathClient.cs
samples), a lot of users wanted hosting to be simpler and
more straightforward.
Following users' requests, I have implemented
full scale script engine hosting. All possible hosting scenarios
are described here. Briefly, hosting would involve:
- compiling script into assembly (script
assembly) by script engine.
- loading script assembly into
appropriate AppDomain.
- exercising objects implemented in script
assembly.
I have added two methods to the CSScript
class (CSScriptLibrary.dll):
Load - Compiles script
into assembly and loads it into current AppDomain. Returns
loaded assembly object.
Compile - Compiles script
into assembly. Returns assembly file name thus assembly
can be loaded in any AppDomain.
All this allows simple script hosting with
unrestricted data exchange between host and script with
just a few extra lines of code. See Client.cs, Script.cs
and Common.cs samples.
There were a few very interesting suggestions
that I have not implemented in a way as they were proposed.
I will explain it by an example.
There was a proposal to have the script
engine take a C# statement and execute it without having
the actual script written. The idea was good but I did not
want to put such functionality into the script engine. I
see the engine as a simple, reliable, and self-contained
application, which is not overloaded with functionality.
Only this way, it can be robust and stable. I do not want
to modify it each time I decide to execute scripts differently.
I see the presented scripting system extensible by other
scripts and assemblies rather than by re-implementing the
engine. This way, the script engine will be protected from
any changes in the system. That is why I implemented the
proposed feature as a script (code.cs). Thus the
purpose is achieved without any changes in the script engine.
The following is an example of how to use this script:
cscscript code MessageBox.Show("Just a test!")
Script Samples.cs is implemented
in a similar manner:
- cscscript samples - prints list
of available samples.
- cscscript samples mailto myMailto.cs
- saves content of "mailto" sample into myMailto.cs
file.
- cscscript samples 1 myScript.cs
- saves content of sample #1 into myScript.cs file.
There was another proposal to have the
script running within a specified security context. This
can be implemented using the same approach. For example:
cscscript runas sa:sysadmin myScript - runs myScript
as a user 'sa' with password 'sysadmin'
Also, many people have asked me about running
multiple script files. In such a scenario, one script would
execute another one to do some job. I have implemented this
(5 Feb 2005 update) and the downloadables are available.
I have to say that I consider this feature as something
outside of the original scope of the script engine. This
is because, as I mentioned before, a complex application
that requires more than one file can be implemented as a
traditional C# application (no need to use C# Script). However,
from users' feedback, I see that many people want to see
that feature in the script engine.
It is implemented in a manner similar to
the C++ "#import ". 'Import '
directive will merge script files together at run time,
thus all the code from one is available for another and
vice versa. Syntax is available in a help message ("cscscrip.exe
/?"). To avoid naming conflicts, importing can do optional
namespace renaming. See provided sample file importTickScript.cs
for any details.
Recently I have added support for a new
directive "reference ".
It can be used to reference assemblies directly from code.
Syntax is available from help (you can also see TeeChartForm.cs
sample). As I mentioned earlier, in some circumstances,
a namespace cannot be resolved into assembly name. It is
really a rear case but it can happen (for example, "Steema.TeeChart "
namespace is implemented in teechart.lite.dll assembly).
To solve this problem such an assembly can be provided as
a command line argument. In some cases it can be inconvenient,
thus I implemented "reference "
directive to reference an assembly directly from code.
Both "import "
and "reference " directive
syntaxes are designed in a way to keep the script code 100%
CLS complaint.
Thanks again to all people who showed interest
in this project. If you are interested in the hot fixes
between the article updates, you will be able to download
them from my web page. |