UML软件工程组织

C# Script (missing puzzle piece)
转自:www.codeproject.com 作者:Oleg Shilo

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.

 

 

版权所有:UML软件工程组织