Summary
This article describes a script engine for the C# language. Here are some key features of the presented scripting system:
- Based on full-featured C#
- Full access to CLR and CTS
- Can be run with any version of CLR (.NET 1.1/2.0-3.5), even potentially with future releases
- Possibility of extending the functionality of any CLR application with "scripting" by hosting the script engine in this application
- Integration with common IDEs and debuggers
- Integration with OS
- Availability of comprehensive documentation, i.e. local and online Help, Tutorials, Samples
Introduction
There are very specific programming tasks that require significant effort to accomplish in the traditional development scenario:
Collapse | Copy Code design -> coding-> compiling -> application
On the other hand, they can be achieved quite successfully by using scripting languages. Scripting can be very useful when:
- The development environment is changeable: frequent code changes are expected
- Application logic is simple: the 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: software solution ideally resides in a single file and is deployed by simple copying of a 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 tasks as configuring development, testing the environment, automating a build procedure, automating testing and collecting test results. These are all tasks that I, as an active C#/C++ programmer, do not normally like to do.
Latest Updates
The C# Script project (product name CS-Script
) has already grown past the scale of a single article. It has its audience and it has found its way into a number of commercial and non-commercial products. For example MediaPortal, FlashDevelop, K2 API or "WinTin" and "C# Script" projects on sourceforge.net. The version of CS-Script
described in this article is v1.1.0. However, the currently available CS-Script
version (v2.5.0) has some features that I could not have foreseen at the time of writing this article. Many of these features are the result of CS-Script
user feedback. Some of them have changed the shape of CS-Script
dramatically:
- Ability to generate Runtime callable wrappers, WebService, Remoting and WCF proxies dynamically on-the-fly, without developer involvement. This makes
CS-Script
a very dynamic runtime environment while maintaining its statically typed nature
- Extremely simple way of debugging C# scripts with any CLR debugger available on the target system
- Support for other programming languages such as C++/CLI, VB.NET, J# and classless C#
- Very simple script hosting model (hosting script engine from any CLR application)
- True DuckTyping allowing aligning the scripted class to the interface defined in the host application.
- In hosting scenarios ability to emit extremely fast delegates based on scripts containing method definition only
Anyway, I would encourage you to read the article because it explains C# Script's purpose and internals. I would also suggest that you download the binaries from the CS-Script home page. Below is a list of some of the latest release changes and features that are not in the article downloads, but are present in the current release:
- Support for custom CLR compilers. VB.NET and JScript compilers are installed as part of the default
CS-Script
installation (v1.3.0)
- Configuration console which simplifies the following tasks (v1.3.0-1.3.1):
- enabling a particular IDE/debugger (Visual Studio 2003/5/8, Visual Studio 2005/8 Express, CLRDebugger, SharpDevelop)
- selecting the target CLR version installed on the system
- enabling
CS-Script
shell extensions
- checking for updates, sending feedback, and accessing local and online documentation
- Support for classless C# scripts (v1.5)
- Ability to execute pre/post execution scripts (v1.5)
- "Single-line access" for COM and Web Services (no importing type library or running wsdl.exe is required) (v1.5)
- Conversion of C# script into Web Service (v1.5)
CS-Script
automatic update (v1.5)
- Command line switch
//x
(similar to the one used for VBScript) for executing C# scripts under any CLR debugger available on the target system (v1.6)
- Script engine launcher css.exe, which allows running console scripts without displaying the console window, in case the script does not do any console output(v1.6)
- Limited support for Compact Framework (v1.7)
- Configurable (batch file based) Advanced Shell Extensions (v1.8):
- Open script file in Visual Studio
- Run script
- Start script under debugger
- Check script file for syntax errors
- Start configuration console
- Convert script to a Visual Studio 2005 project
- Convert script to a Visual Studio 2008 project
- Create shortcut to the script file for running it by double-clicking
- Compile script into an executable
- Compile script into a class library
- Support for C++/CLI syntax (v1.9)
- Support for WPF/XAML scripts (v1.9)
- Sample scripts (Remoting, WCF, WPF, WWF) to the Script Library (v1.9-2.1):
- Support for script probing directories SearchDirs. This functionality is similar to the system environment variable PATH. Also implemented SearchDirs console for simple management of the SearchDirs items (v1.9-2.0)
- Vista support. Implemented process elevation for the script execution (v2.0)
- Script caching algorithm for hosting the script engine (CSScriptLibrary.dll) from the applications (v2.0)
- Concept of "script alias". Script alias is a "script like" file which links to the actual script file. Script alias is a logical equivalent of a file shortcut in Windows (v2.0)
- Simplified Hosting Model: unrestricted 2-way communication between Script and Host without any setup cost (v2.1)
- Support for C++ style Macros. CS-Script extends its ability to dynamically generate code at runtime and allows defining C++ style macros in C# code (v2.1)
- Complete Visual Studio Integration with toolbar buttons/commands and code snippets library (v2.1)
- Support for loading method definitions only (classless scripts) when hosting the script engine from managed applications (v2.2)
- Remarkable performance (when hosting the script engine) due to the usage of emitted dynamic delegates (v2.2)
- Simplified syntax for calling methods of dynamically loaded assemblies. Similar to C# 4.0 dynamics(v2.2)
- Added interactive environment CSI (v2.3)
- Support for specifying probing directories from the script and/or command line(v2.3)
- Support for extendable environment variables in all import/include script directives (v2.3)
- Added verbose execution mode which allows analysis of runtime information during the script execution (v2.3)
- Added native support for classless script files with /autoclass directive (v2.4)
- Implemented Interface Alignment (DuckTyping) for classes defined in script and instantiated in the hosta aplication (v2.4)
- Added CS-Script launcher for Linux (v2.4)
- Added integration with NAnt (v2.5)
- Added Silverlight Player for viewing XAP files and converting Silverlight Web applications into self-sufficient desktop applications (v2.5)
- Updated Linux related implementation modules. Now C# and VB.NET scripts can be run on Linux with simple double-click (v2.5)
Background
Microsoft has not provided many adequate scripting solutions. VBScript was the language that 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 the 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 would solve this problem. However, that did not happen. Microsoft did not provide a C# engine for WSH. 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, old JScript and .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 doesn't fit some existing software concept, but because of all of the reasons I mentioned earlier. To recap, these are the need for 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
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 came across a few attempts to tackle this problem. Some of them were reasonably successful (there are a few CodeProject articles on the subject), however every C# scripting solution I have seen was either based on the usage of some non-C# syntactical elements in the code or required an additional custom format file with some extra information (e.g. referenced assemblies). But I wanted to have a script engine capable of executing "plain vanilla" C# code.
All the time, I felt it was possible to use pure C# as a script 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. The engine application has nothing to do with WSH, despite some interface resemblance.
CS-Script Engine
I wouldn't like to spend too much time describing the script engine application. Its code is available, so help yourself. However, I still 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. The compilation part is trivial and some sample code can be found here.
The more challenging task was to implement loading of all of the 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
. Similarly, the namespace System.XML
is 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.
Collapse | Copy Code [assembly file name] = [assembly name] + .dll
Thus, I can resolve the namespace
s from the script into assemblies. I do this for both global (GAC) and local assemblies.
Collapse | Copy Code [namespace] -> [assembly name] -> [assembly file name]
In order to find the location of the global assemblies, it is necessary to browse GAC. Unfortunately, there is no .NET API available for navigating in the GAC, only the COM one. 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. They helped me a lot.
Dealing with the local assemblies is as challenging as with the global ones. I use the predicting assembly file name algorithm, based on the namespace, as a starting point. However, as I mentioned before, there is only the correlation between the namespace
and the assembly name but no true relationship. Things can get even 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).
However, there is still no guarantee that all of the namespace
s 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. I have never had to use this feature as, so far, all of the namespace
s in my scripts were always resolved. The only restriction I have to impose is that any assembly which is referenced in the script must reside either in the 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 static
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 the script and engine files (about 30 K size) on a system that has the .NET runtime installed, and the script can be run
- Portability: scripts can be run on any system that has the 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 functionalities are available (FCL, COM Interop, Remoting)
- Easily available debugger and rich IDE (Microsoft .NET Studio, CLR Debugger)
- 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 in 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 of the software 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 CS-Script
and found the system to be stable and reliable. Here are some of the features of the script engine application:
- Comes with two interfaces: WinForms and console applications
- Can execute a standard *.cs file that has the
Main()
method defined
- Can store the compiled assembly file in the same location as a script file; can execute it next time instead of compiling the script again if the script was not changed since the last execution
- Can generate a template script file as a starting point for further development
- Can generate an executable from the script. Thus, no script engine is required to run it again
- Can generate an 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 a file name without an extension. In this case, *.cs will be assumed
- Script engine looks for the script file in the directories specified as the 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. An assembly that implements the script engine as a Class Library is available as part of the downloadable package
- Script engine can load multiple scripts. This is useful when a script uses the functionality of another one
- Explicitly referenced assemblies can be added not only from the command-line but also directly from the code
The script engine application is named cscscript.exe, or cswscript.exe for the WinForms mode. The simplest way to prepare any script is to execute it from a command prompt:
Collapse | Copy Code 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 valid C# code to this file to do your specific tasks. After that, it can be compiled and executed by the script engine via the command: cscscript.exe sample.cs
Sample.cs
Collapse | Copy Code 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, the script file contains ordinary C# code. There is not a single language statement that would be understood only by the script engine but not by any C# compiler (Microsoft .NET Studio). This is one of the strongest points of CS-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 nothing but C#. I have to say that I got more than I had anticipated at the start of this whole exercise. The script engine is just an application and relatively a primitive one at that. However, 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 that 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 with just a right-click of the mouse. As you can see, the same script engine application is much more capable now
- Compose a C# script on the fly from a code fragment and execute it, e.g.
cscscript code MessageBox.Show("Just a test!")
will pop up the message "Just a test!"
As you can see, the same script engine application is much more capable now.
CS-Script Package
You can download -- from the top of this page or from my Web page -- the whole CS-Script
package, which contains the script engine, useful scripts and the samples. To install, you will need to extract everything from the ZIP file and execute install.bat. Of course, .NET Framework has to be installed first. After the installation, you will have the Environment variables updated and all of the 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 very simple scripts that I have created and added to the package:
- NKill.cs - Kills processes specified as command-line parameters
- GetUrl.cs - Gets the URL content and saves it in a file. Can handle proxy authentication
- Debug.cs - Opens a script with a temporary C# project for running it under the debugger
- Install.cs - Installs/uninstalls the
CS-Script
shell extensions that will permit running/debugging of any script with a mouse right-click
- ImportData.cs - Imports data from the specified file to an SQL Server table
- ExportData.cs - Exports data to the specified file from an SQL Server table
- RunScript.cs - Script that runs another script in the background with redirection of the output to the parent script
- Tick.cs - Simple script that counts a specified number of seconds; used as demo for
RunScript
- SynTime.cs - Gets time from here and synchronizes the PC system time
- MailTo.cs - Sends an e-mail to the specified address
- Reflect.cs - Prints the assembly reflection information
- Creditentials.cs - Prompts and collects user login information
- GetOptusUsage.cs - Retrieves monthly data usage. This is applicable only for "Optus Australia" customers
- Interop.cs - Creates and accesses COM and CLR objects
- ImportTickScript.cs - Imports -- with the
import
directive -- and executes the 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 C# projects on my hard drive. In most 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. I just 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 a thousand times to get some statistics. To do it from the main application would mean contaminating the main application with the testing code. What you can do is copy the main application code that invokes the assembly 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, the "missing puzzle piece" in the title of this article. I am interested in knowing what other people think and would appreciate any feedback.
Feedback
I would like to thank all of the people who sent me their suggestions and ideas. Some of them have been 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 found a forum where I was criticized for underestimating the importance of script hosting. I guess they are right. Despite the technical possibility of hosting 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 the script into assembly, i.e. script assembly by script engine
- Loading the script assembly into the appropriate
AppDomain
- Exercising objects implemented in the script assembly
I have added two methods to the CSScript
class (CSScriptLibrary.dll):
Load
- Compiles the script into assembly and loads it into the current AppDomain
. Returns the loaded assembly object
Compile
- Compiles the script into assembly and returns the assembly file name. Thus, the assembly can be loaded in any AppDomain
All this allows simple script hosting with unrestricted data exchange between the host and the script, with just a few extra lines in code. See the Client.cs, Script.cs, and Common.cs samples.
There were a few very interesting suggestions that I have not implemented in the way in which 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 that is not overloaded with functionality. Only in this way can it 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:
Collapse | Copy Code cscscript code MessageBox.Show("Just a test!")
The script Samples.cs is implemented in a similar manner:
cscscript
samples: Prints a list of available samples
cscscript
samples mailto myMailto.cs: Saves the content of the "mailto" sample into the myMailto.cs file
cscscript
samples 1 myScript.cs: Saves the content of sample #1 into the 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...
Collapse | Copy Code 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 (5th Feb 2005 update) and the downloads are available. I have to say that I consider this feature as something outside 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. So, there is no real need to use CS-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
. The Import
directive will merge script files together at run time. Thus, all of the code from one is available for another and vice versa. The syntax is available in a help message ("cscscrip.exe /?"). To avoid naming conflicts, importing can do optional namespace
renaming. See the provided sample file importTickScript.cs for details.
I recently added support for a new directive called reference
. It can be used to reference assemblies directly from the code. The syntax is available from the help. You can also see the TeeChartForm.cs sample. As I mentioned earlier, in some circumstances the namespace
cannot be resolved into an assembly name. It is a really rare case, but it can happen. For example, if the Steema.TeeChart namespace
is implemented in the 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, so I implemented the reference
directive to reference an assembly directly from the code. The syntax for both the import
and reference
directives are designed in a way that keeps the script code 100% CLS compliant.
Thanks again to all of the people who showed interest in this project. If you are interested in hot fixes between the article updates, you will be able to download them from CS-Script home page (www.csscript.net).
History
- 26th Oct, 2004
- 29th Oct, 2004
- Script file (install.cs) updated to fix an installation problem
- 12th Nov, 2004
- Added to the article:
- Description of assembly resolving algorithm
- Description of new features
- Feedback section
- Changes in the downloadable items:
- Added support for local assemblies
- Implemented compiling script into an EXE file
- Added the
CSScriptLibrary
assembly (CSScriptLibrary.dll) for hosting the script engine by CLR applications
- Script engine applications are mirrored now, cscscript.exe/csc.exe and cswscript.exe/csw.exe
- 5th Feb, 2005
- Added to the article:
- Updated list of keywords and feedback section
- Changes in the downloadable items:
- Added support for running multiple script files
- Updated the debug.cs script. It is now able to resolve all the referenced
namespace
s, or references to other scripts from the code and generate the appropriate Visual Studio project
- 10th Jun, 2005
- Added to the article:
- Changes in the downloadable items:
- Added support for referencing assemblies from the code
- Updated the debug.cs script. It is now able to add assemblies referenced from the code to the appropriate Visual Studio project
- 20th Jul, 2005
- Added to the article:
- Changes in the downloadable items:
- Added support for the .NET 2.0 Framework
- Improved support for script hosting
- Added support for Visual Studio 8.0, SharpDevelop, and the Microsoft CLR Debugger
- 16th Aug, 2005
- Changes in the downloadable items:
- Fixed bug that prevented installation of shell extensions for Visual Studio 2005
- Added support for
[STAThread]
and [MTAThread]
attributes in the script
- Added some new sample scripts
- 27th Oct, 2005
- Added update description (v 1.2) at the start of the article
- 25th May, 2007
- 19th July, 2007
- Article and downloads updated
- 8th April, 2008
- 28th Feb, 2009
- 22nd Jun, 2009
- 29th Sep, 2009
I was born in Ukraine. After completing the university degree worked there as a chemist. Last 16 years I live in Australia where I've got my second qualification as a programmer.
"I am the lucky one: I do enjoy what I am doing!"