![]() |
Languages »
C# »
General
Intermediate
License: The Code Project Open License (CPOL)
C# Script: The Missing Puzzle PieceBy Oleg ShiloAn article on a "scripting engine" for the C# language |
C#, Windows, .NET 1.1, .NET 2.0VS.NET2003, VS2005, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
This article describes a script engine for the C# language. Here are some key features of the presented scripting system:
There are very specific programming tasks that require significant effort to accomplish in the traditional development scenario:
design -> coding-> compiling -> application
On the other hand, they can be achieved quite successfully by using scripting languages. Scripting can be very useful when:
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.
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:
CS-Script a very dynamic runtime environment while maintaining its statically typed nature 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:
CS-Script installation (v1.3.0) CS-Script shell extensions CS-Script automatic update (v1.5) //x (similar to the one used for VBScript) for executing C# scripts under any CLR debugger available on the target system (v1.6) 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:
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.
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.
[assembly file name] = [assembly name] + .dll
Thus, I can resolve the namespaces from the script into assemblies. I do this for both global (GAC) and local assemblies.
[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 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. I have never had to use this feature as, so far, all of the namespaces 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:
static void Main() Object as a default type and do boxing/unboxing all the time 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:
Main() method defined Features added on user demand:
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:
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
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# Scriptis 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:
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.
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:
CS-Script shell extensions that will permit running/debugging of any script with a mouse right-click RunScript import directive -- and executes the tick.cs script CS-Script scripting 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.
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.
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:
AppDomain 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:
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...
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).
CSScriptLibrary assembly (CSScriptLibrary.dll) for hosting the script engine by CLR applications namespaces, or references to other scripts from the code and generate the appropriate Visual Studio project [STAThread] and [MTAThread] attributes in the script
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 30 Sep 2009 Editor: Deeksha Shenoy |
Copyright 2004 by Oleg Shilo Everything else Copyright © CodeProject, 1999-2009 Web22 | Advertise on the Code Project |