The idea of performing certain actions prior to the script execution
is an frequent scenario. The example of such scenario is altering the
script code just before passing it to the C# compiler (for compilation
and further execution). Such a scenario is called Precompilation and
CS-Script has a build-in functionality for to support it. This CS-Script functionality is called Precompilers and this section will provides an overview of it as well as some
code samples.
A typical
precompiler is a C#
script (or a compiled assembly) which is capable of analyzing the C#
code and altering it if required. Such script/assembly must have very
specific interface in order to interact with the CS-Script engine.
Precompilation can also be implemented with
pre-execution scripts. While this indeed possible such an implementation will not
be very elegant nor maintainable. But nevertheless p
recompilers
conceptually are nothing else but specialized
pre-execution scripts. That
is it, while everything that can be done with
precompilers can also be done
with
pre-execution scripts use of
precompilers offers much simple syntax what leads to the more "comfortable"
development/maintenance.
The best way to explain
precompilers is to have a close look at one.
The following is an example of a very light
precompiler.
public class Precompiler { public static bool Compile(ref string code, string scriptFile, bool isPrimaryScript, Hashtable context) { if (code.StartsWith("#!")) { code = "//" + code; //comment the Linux hashbang line to keep C# compiler happy
return true; //true as the code has been modified } return false; //false as the code has not been modified } }
|
This particular
precompiler
comments the very top hashbang (shebang/"magic line") line of the
script. This allows using hashbang string in the C# scripts on Linux
without upsetting the C# compiler any yet without interfering with the shell handling
the hashbang string in a traditional Linux way.
There are a few thing that make the code above a
precompiler:
Now let's see what this particular precompiler does at runtime.
Just
before executing the script the script engine loads the precompiler and
calls its Campile method for the script being executed (
primary script)
as well as for the scripts being included/imported by the primary
script. The script engine passes the script content (parameter
code) of the
script to be precompiled and it is up to the precompiler what to do with the script content.
Note
that because you can specify multiple precompilers the code parameter
may contain the code already modified by one of the precompilers.
The rest of the parameters of the
Compile() method is an additional
information about the execution context:
- scriptFile - name of the script file which is being precompiled
- isPrimaryScript - flag indicating if the script being precompiled is
a primary script or an included/imported script.
- context - a free-style dictionary containing information about various aspects of the runtime and compilation
context.
This parameter can be useful only in very rare cases for
"advanced precompiling".
- context["NewDependencies"] - List<string>
Collection of file names that should be checked for the modification
every time the primary script is executed and the decision needs to be
made whether to load the script from the cache or compile it from the
sources if any source file (dependency) has been modified since the
last execution.
- context["NewSearchDirs"] - List<string>
Collection of extra directories that should be used for assembly and
script probing during actual script compilation after precompilation is
done.
- context["NewReferences"] - List<string>
Collection of extra referenced assemblies
that should be used for assembly and script probing during actual
script compilation after precompilation is done.
- context["NewCompilerOptions"] - string
Additional compiler options to be passed to the script compiler.
- context["SearchDirs"] - string[]
Read-only. Collection of directories that are used for assembly and
script probing during actual script compilation after precompilation is
done. This is a fixed collection, which is defined by the global CS-Script configuration.
- context["CompilerOptions"] - string
Read-only. String containing compiler options that are passed to the C# compiler during the compilation of
the primary
script.
The example above is the precompiler
which is embedded in the script engine and by default applied for all
scripts being executed.
There are a few ways of configuring/engaging precompilers for a particular script:
Command-line argument:
cscs /precompiler[:<file 1>,<file N>] script.cs
file - precompiler (script or assembly)
Specifies custom precompiler file(s). If no file(s) specified prints the code
template for the custom precompiler.
There is a special reserved word 'nodefault' to be used as a file name.
It instructs script engine to prevent loading any built-in precompilers.
CS-Script directives in the
script:
//css_precompiler <file 1>,<file 2>;
file
- precompiler (script or assembly)
It is a logical equivalent of the command line argument /precompiler.
You can also use the directive alias //css_pc.
Tutorial
This is the tutorial we will create the prcecompiler, which would
allow execution of a free-style C# code containing no class nor method
definition.
The tutorial is based on the <cs-script>\Samples\Precompilers\Script.cs sample.
The subject of this tutorial is related to the "classless scripts" and "autoclass" features.
- Create free-style script script.cs containing the loop iterating
through script arguments and statement for displaying a message:
//css_precompiler freestyle using System.Windows.Forms; for (int i = 0; i < args.Length; i++) Console.WriteLine(args[i]); MessageBox.Show("Hello World!");
|
Note that the script is configured to be precompiled with the precompiler script freestyle.cs.
- Execute from command prompt the following command which creates the precompiler template with the name freestyle.cs:
cscs /precompiler > freestyle.cs
This will create the precompiler skeleton:
using System; using System.Collections; public class Sample_Precompiler //precompiler class name must end with 'Precompiler' { public static bool Compile(ref string scriptCode, string scriptFile, bool isPrimaryScript, Hashtable context) { //if scriptCode needs to be altered assign scriptCode the new value and return true. Otherwise return false //scriptCode = "code after precompilation"; //return true; return false; } }
|
- Modify the precompiler as follows
using System; using System.Collections; using System.Text; using System.IO; public class FreestyleScriptPrecompiler { static public bool Compile(ref string content, string scriptFile, bool isPrimaryScript, Hashtable context) { if (!isPrimaryScript) return false; var code = new StringBuilder(4096); code.AppendLine("//Auto-generated file"); code.AppendLine("using System;"); code.AppendLine("using System.IO;"); bool headerProcessed = false; string line;
using (var sr = new StringReader(content)) while ((line = sr.ReadLine()) != null) { if (!headerProcessed && !line.TrimStart().StartsWith("using ")) //not using...; statement of the file header if (!line.StartsWith("//") && line.Trim() != "") //not comments or empty line { headerProcessed = true; code.AppendLine(" public class ScriptClass"); code.AppendLine(" {"); code.AppendLine(" static public int Main(string[] args)"); code.AppendLine(" {"); } code.Append(line); code.Append(Environment.NewLine); } code.AppendLine(" return 0;"); code.AppendLine(" }"); code.AppendLine("}"); content = code.ToString(); return true; } }
|
The implementation of the precompiler is fairly simple as it simply
wraps the content of the script file being executed into the class
definition with some extra "using" statements.
- Execute the script script.cs as any normal script:
cscs script.cs
During the execution the precompiler will modify the content of the
script file by injecting the missing declaration parts just before
passing the content to the C# compiler. The injected code is indicated
with the gray color in the following listing:
//Auto-generated file using System; using System.IO; //css_precompiler freestyle using System.Windows.Forms; public class ScriptClass { static public int Main(string[] args) { for (int i = 0; i < args.Length; i++) { Console.WriteLine(args[i]); } MessageBox.Show("Hello World!"); return 0; } }
|
Conclusion
Precompilers is a powerful feature , which opens almost unlimited scripting opportunities. Replacing the magic string
is just a very simple example but precompiling can do so much more. For
example with precompiling you can easy implement C++ style #define for
C# (even #define is a controversial feature). It is even possible to
implement Domain Specific Language of a sort.
You can find more Precompilers samples in the <cs-script>\Samples\Precompilers directory. These samples doe not have
very high practical value but they do show the idea.
See Also
Command-line
interface | Classless Scripts