CS-Script 3.28.9


Script Hosting Guideline

Script hosting can be as simple as a single line call or it can be a more complex solution, requiring design, deployment and code maintenance considerations. In most cases a success would depend on how correctly the hosting model was chosen.

"Isolated execution" is straightforward and does not really require any special consideration.

However the "type sharing" model can lead to some run-time and code maintenance problems if not implemented correctly. 

Note that the majority of the topics discussed in this chapter are quite generic but some of them are specific to the hosting engine CodeDOM vs. Evaluator ("compiler as service"). While all examples here are based on the CodeDOM code snippets it is strongly recommended that you also read the Evaluator - Compiler As Service chapter describing this hosting approach in details.       


Remote vs. Local loading

Note: this section is applicable to CodeDOM based hosting only.

Probably everyone who worked with dynamically loaded assemblies is aware about the important .NET limitation: once loaded assembly cannot be unloaded. This is it, if the script is compiled and loaded in the AppDomain for the execution it cannot be unloaded. Microsoft has acknowledged this a s a design flaw and even confirmed this to be a major "head ache" in the implementation of the ASP.NET runtime.

The only work around is to load the script in the temporary
AppDomain, execute the require script method and unload the AppDomain. This is exactly what AsmHelper does. It allows you to work with the script in two different loading modes:

Local

   Script is loaded in the Current AppDomain and stays loaded after the execution. To set up the AsmHelper to work in this mode instantiate it with the constructor that takes the loaded assembly as a parameter

Remote

   Script is loaded in the temporary AppDomain and unloaded after the execution. To set up the AsmHelper to work in this mode instantiate it with the constructor that takes the assembly file name as a parameter

See Advanced Scripting for details.


While Remote Loading is more flexible (in terms of memory management) than Local Loading it does introduce a few significant practical constrains:
- all types to be passed cross AppDomain boundaries must be either serialization or inherited from MarshalByRefObject.

- AppDomain initialization and unloading as well as  types serialization can yield some significant performance penalties.


Remote Loading also requires proper object lifetime management. The technique is demonstrated in <cs-script>\Samples\Hosting\LifetimeManagement.cs sample.

The script-assembly loading approach is not as critical as it seams. Even if with the Local Loading the compiled script cannot be unloaded it is still the preferred option when the number of the script to be executed by the host application is a finite number as it introduces no constraints whatsoever. However if the number of scripts to be executed by the host application is unknown and potentially infinite the Local Loading can effectively lead to the memory leaks thus the Remote Loading becomes an unpleasant but the only adequate choice.


Performance considerations
The performance is one of the most important aspact of any scripting system. While the runtime performance is discussed in one or another form quite often in this documantation it is still worth to recapture the major poins in this section.

CS-Script engine runs compiled (not interpreted scripts) and as such it offers the fastest possible (under CLR) execution. Meaning that C# script code runs with the speed completely identical to the compiled code loaded/executed from the dependency assembly.

Saying that it is important to understand that such a remarcable performance comes with the price: a one off overhead associated with the script compilation. In the wast majority of the cases this overhead is fully compensated by the script caching.

Caching mechanizm was inspired by the similar feature in Python. The concept is very simple. If the script file was not modified since trhe last execution the compilation is not performed and the previous comilation result (assembly) is used instead. This ensures that the relatively expensive script recompilation is performed only if it is really required.

If the script is never to be modified after it is created it will ever be compiled only once (on the first execution) regardless how many times it is executed in the future.

But what constitutes the "change"? The "change" is not only the change in the timestamp of the script file but also the changes in any dependecy resources: referenced assemblies or dependency scripts.

Caching mechanizm works the same way for the script standalone execution (e.g. from command prompt) and for the execution with the hosted script engine. Cahing is also applicable for the hosted file-less scripts execution (e.g. CSScript.LoadCode). In this case the script engine uses string hashing to monitor and detect the script code changes. But of course the file-less scripts cache only valid within a giveen hosting session.  

Cahing with hosted scenarios is very flexible. It even allows user defining the custom "IsOutOfDateAlgorithm" for testing script file for changes:

class CSScript 
{
...
public static IsOutOfDateResolver IsOutOfDateAlgorithm;
 
Reflect or not to Reflect

AsmHelper if used on its own offers only Reflection base invocation mechanism, which attracts some performance penalties. Another less obvious draw back is the code maintenance difficulties associated with Reflection.

The greatest performance benefits can be achieved when using interfaces as the types to be passed between the host and the script. Another benefit of the interfaces is that they enforce good implementation/interface separation.  

Whereas the remote loading obviously is the most powerful model it does not gives any significant advantage, but introduces some significant design constrains. Thus use the following as a general guideline:

Use interfaces when you can and remote loading when you have to.

Samples\Hosting\HostingWithInterfaces folder contains example of script hosting with using interfaces. This is an example of a balanced usage of scripting. It uses local loading and allows unrestricted data exchange between the script and the host using an ordinary C# coding technique.  

There is also another advantage of using interfaces. By using interfaces you can avoid using less-friendly coding techniques (pure reflection) in the implementation of the host application. The following code sample demonstrates the technique:


Host: interface definition code

public interface IWordProcessor
{
    void CreateDocument();
    void CloseDocument();
    void OpenDocument(string file);
     void SaveDocument(string file);
}

 

Script: interface implementation code
public class WordProcessor: IWordProcessor
{
    public void CreateDocument() { ... }
    public void CloseDocument()
 { ... }
    public void OpenDocument(string file)  { ... }
     public void SaveDocument(string file)  { ... }
}

Host: script usage code

AsmHelper helper = new AsmHelper(CSScript.Load( "script.cs" , null, true));

//the only reflection based call 
IWordProcessor proc =  (IWordProcessor)helper.CreateObject("WordProcessor");

//no reflection, just direct calls 
proc.CreateDocument();
proc.SaveDocument("MyDocument.cs");


Interface Alignment

In version 2.3.3 CS-Script introduces new script hosting model Interface Alignment, which is an attractive alternative to the interface inheritance while loading/accessing scripts through interfaces. 


This model allows manipulation with the the script by "aligning" it to the appropriate interface (DuckTyping). Important aspect of this approach is that the script execution is completely typesafe (as with any script accessed through an interface) but even more importantly the script does not have to implement the interface being used by the host application. 

In a way Interface Alignment is a forcible typecasting: typecast to interface is possible as long as the object has all methods defined in the interface .  

This promising technique allows high level of decoupling between the host and the script business logic without any type safety compromise.  

The core implementation of the Interface Alignment is based on the ObjectCaster by Ruben Hakopian, which is a subject of this Copyright.


Example of the Interface Alighnment:

using  CSScriptLibrary;

public interface IScript
{
    void Hello(string greeting);
}

class  Host
{
     static void Main()
    {
        var script = CSScript.Load( "HelloScript.cs" )
                             .CreateInstance( "Script" )
                             .AlignToInterface<IScript>();

        script.Hello( "Hi there..." );
    }    
}

HelloScript.cs
using System;

public class Script
{  
    public void Hello(string griting)
    {
         Console.WriteLine(greeting);
    }
   
    void SomeOtherMethod()
    {
        ...
    }
}
Note: ObjectCaster is an IL-emmitting engine on its own. And because it doesn't have any dependency discovery (assembly probing) mechanism you need to supply any dependency information with one of the AlignToInterface overloads. The easiest way is to pass true to allow ObjectCaster to reference any AppDomain already loaded assemblies:
IProcess script = instance.AlignToInterface<IProcess>(true);
Alternativelly you can pass the exact location of the dependency assembly:
IProcess script = instance.AlignToInterface<IProcess>(<dependency assembly path>);

If you need to align remote object (TransparentProxy) created in the different AppDomain you need to use AsmHelper to do actual alignment and in the appropriate AppDomain context (see <cs-script>\Samples\Hosting\InterfaceAlignment samples):

//Note using helper.CreateAndAlignToInterface<IScript>("Script") is also acceptable
using (var helper = new AsmHelper(CSScript.CompileCode(code), null, false))
{
    IScript script = helper.CreateAndAlignToInterface<IScript>("*");

    script.Hello("Hi there...");
}

Script hosting considerations

As almost with any other programming task script hosting can be a simple maintainable solution or it can lead to bloated unstable system, which is hard to maintain or test. As usual it all depends on design and implementation decisions. Decisions, which are easier to make if you consider the following hosting aspects.

How to invoke script methods?
Strictly speaking you do not need any special framework to exercice script methods. The script assembly is (the primary output of the script engine) is an ordinary assembly and you can use Reflection to work with it as you would with any other assembly.  But the problem is that reflection based code is harder to read and maintain. AsmHelper is a CS-Script class, which significantly simplifies working with dynamically loaded assemblies. You can find various invocation samples in cs-script\Samples\Hosting\MethodSignatures.cs.  

Assembly assembly = CSScript.LoadCode(
            @"using System;
              public class Calculator
              {
                    static public int Add(int a, int b)
                    {
                       return a + b;
                    }
              }"
);

AsmHelper calc = new AsmHelper(assembly);
int sum = 
calc.Invoke ( "Calculator.Add", 1, 2); //sum == 3; 

AsmHelper implements Dispatch invokation pattern by resolving user specified method names into actual method runtime type information. An alternative approach is to generate dynamic delegate for each method you are going to execute and work with these delegates instead of AsmHelper directly. 

...
AsmHelper calc = new AsmHelper(assembly);
var Add = calc.GetMethodInvoker("Calculator.Add", 0, 0);

//pass null because Calculator.Add is a static method otherwise pass class instance
int sum = Add(null, 1, 2); //sum == 3; 

Note: that you can use wild card for method names (e.g. "*.Add").

AsmHelper also has specialized method emitters GetStaticMethod() and GetMethod(), which return delegate for static or instance method, thus you do not need to pass instance value:

Assembly assembly = CSScript.LoadCode(
            @"
using System;
              public class Calculator
              {
                    static public int Add(int a, int b)
                    {
                       return a + b;
                    }
              }
");

var Add  =  new AsmHelper(assembly) .GetStaticMethod();
int sum = Add(1, 2);
In the example above GetStaticMethod() is used without any parameters, what is an equivalent of GetStaticMethod("*.*").  Meaning any method of any type from the specified assembly.

Note that CLR does not support delegates serialization thus GetMethod, GetStaticMethod and GetMethodInvoker cannot be used with the remote execution mode.

Another alternative is to use interfaces (see previous section code sample). When using interfaces a simple typecasting automatically maps entire set of class methods and properties into runtime object.

How important performance is?
AsmHelper operates through Reflection (MethodInfo) and this is the reason why if you have to invoke some script methods multiple times you will pay some performance penalties. The work around this is to avoid using Reflection and rather work with interfaces or delegates emitted by AsmHelper (see sections above).

Interfaces and emitted delegates are about 100 faster than Reflection  You can find detailed information about ways of improving performance in cs-script\Samples\Hosting\performance.cs sample (TestMethodDelegates() method).

How type safe your code is?
Dispatch pattern for method invocation is (Reflection) is error prone as it cannot be checked at by compiler. Thus if you want to achieve stronger typed code you need to use either dynamic (emitted) delegates or better yet to use interfaces. See cs-script\Samples\Hosting\TypeSafety.cs for samples.

How readable your code is?
Code readability (and maintainability in general) is something the you should invest in generously. Usage of extension methods is one of the possibilities for code improvements. CS-Script class library comes with a few extension methods which allow to minimize code footprint for script hosting routines. In the code samples below Assembly class extended with GetStaticMethod and CreateObject method extensions, which allow in the presented context completely avoid usage of AsmHelper.

var SayHello = CSScript.LoadMethod(
                           @"public static void SayHello(string greeting)
                             {
                                 Console.WriteLine(greeting);
                             }"
)
                             .GetStaticMethod();

SayHello("Hello World!");

var myCollection = CSScript.LoadCode(
              @"using System;
                using System.Collections;
                
                public class MyCollection : IEnumerator
                {
                    public IEnumerator GetEnumerator() { return null; }
                    public object Current { get { return null; } }
                    public bool MoveNext() { return false; }
                    public void Reset() { }
                }"
)
              .CreateObject("*") as IEnumerator;

myCollection.MoveNext();

GetStaticMethod has some obvious limitations as it returns the following delegate object MethodDelegate(params object[] paramters). Thus it is not possible to use neither out nor ref parameters. But nevertheless GetStaticMethod can be a usefull addition to the full bodied scripts.

eeeeeeeeeeeeeeeeeeeeeeeeee
"How much" scripting do you need?
The default scripting unit is a C# module containing type definition(s).  However it is possible to have a script which defines just a single method with no class (classless).

Assembly assembly = CSScript.LoadCode(
            @"static public void PrintSum(int a, int b)
              {
                 Console.WriteLine(a + b);
              }"
);

var PrintSum = 
new AsmHelper(assembly) .GetStaticMethod();

PrintSum(1, 2);

Note that you can have multiple methods implementations in the classless script code. You can also specify namespaces at the beginning of the code:

var script = new AsmHelper(CSScript.LoadMethod(
            @"using System.Windows.Forms;
             
             public static void SayHello(string greeting)
             {
                 MessageBoxSayHello(greeting);
                 ConsoleSayHello(greeting);
             }
             
             public static void MessageBoxSayHello(string greeting)
             {
                 MessageBox.Show(greeting);
             }
             
             public static void ConsoleSayHello(string greeting)
             {
                 Console.WriteLine(greeting);
             }"
));
          
script.Invoke("*.SayHello""Hello World!");

See cs-script\Samples\Hosting\Classless.cs sample for details.

Assembly and Script Probing scenarios


When implementing particular hosting scenario you may get into the situation when your host, script and script dependencies required to be located in different places. Such requirement sometimes is hard to meet and it is a common issue for dynamic assembly loading.

It is important to recognize that a part from the dealing with the common assembly probing problems developers also have to face script probing issues.
assembly probing - the way how CLR locates assemblies.
script probing - the way how CS-Script engine locates scripts.

Also it is worth to mention that script execution consist of two logical stages and they are affected by the probing problems in different degree:

Script compilation - involves assembly and script probing
Compiled script (assembly) execution - involves assembly probing only

Fortunately CS-Script can assist with solving all probing problems.  In general, the way of solving the assembly probing problems is to nominate probing directories (search directories) before executing the script. This can be done globally for all scripts or on the script by script base. An alternative approach is to use Simplified Hosting Model. In most of the cases Simplified Hosting Model would be sufficient to handle all possible script probing problems, however in some rare cases you may need to set probing directories by yourself.

 

NOTE: In some cases you may find that you need to have a complete control over assembly and script probing mechanism. In such cases the host application can supply the custom probing algorithm (ResolveAssemblyAlgorithm/ResolveSourceAlgorithm).  The code sample below shows how to implement custom probing for the assembly referenced by alias 'forms' (e.g. '//css_ref forms') 

var findAsm = CSScript.ResolveAssemblyAlgorithm;

CSScript.ResolveAssemblyAlgorithm = (name, searchDirs) =>
    {
        if (name == "forms")
            return new[] { typeof(System.Windows.Forms.Form).Assembly.Location };
        else
            return findAsm(name, searchDirs);
    };


This technique allows ultimate flexibility. You can generate the source file on fly, download the assembly from the Internet, check the integrity (or even decrypt) of the user supplied C# file or even implement license-based access to the specific assemblies.
See cs-script\Samples\Hosting\CustomProbing.cs sample for details.

 

Simplified Hosting Model probing directories

The simplest way to solve probing problems is to use Simplified Hosting Model. The main advantage of this approach is that CS-Script engine automatically includes directories of all loaded assemblies of the host application (including the application assembly itself) into the list of searchable directories. And because Simplified Hosting Model is enabled by default in most of the cases you do not have to do anything special about the script probing in your code. For example if your script is located in the different directory with respect to the host application and the script is accessing the host application type(s) you do not have to express host/script dependency in any special way: the script engine will do it for you.

//Host.cs
class Host
{
    ...
    void Process()
    {
        AsmHelper helper = new AsmHelper(CSScript.Load(@"Scripts\MyScript.cs"));
        helper.Invoke("*.Process"this);
    }
}

//MyScript.cs
static public void Process(Host host)
{
    ......
}



Global probing directories

Another way of solving the probing problems is to nominate probing directories globally. Such approach is the simplest however it does have some limitations.

The first line of the code below adds Lib directory to the list of the probing directories. These directories are used for probing at compiling stage (CSScript.Load) and also at execution stage (helper.Invoke).

CSScript.GlobalSettings.AddSearchDir(Path.GetFullPath("Lib"));
CSScript.AssemblyResolvingEnabled = true

AsmHelper helper = new AsmHelper(CSScript.Load("script.cs"));
helper.Invoke("Script.Report""Hello!");


Note that assembly resolving at the execution stage is forced to use the same global probing directories by setting AssemblyResolvingEnabled to true. If AssemblyResolvingEnabled is false you have to set probing directories for script execution explicitly by modifying the AsmHelper's object ProbingDirs property. 

CSScript.GlobalSettings.AddSearchDir(Path.GetFullPath("Lib"));

AsmHelper helper = new AsmHelper(CSScript.Load("script.cs"));
helper.ProbingDirs = CSScript.GlobalSettings.SearchDirs.Split(';');
helper.Invoke("Script.Report""Hello!");

Using GlobalSettings is convenient but it will not work with the remote script execution. Remote execution model implies that the script assembly after execution is unloaded. In order to do this the script assembly is executed in a different AppDomain thus even if GlobalSettings.SearchDirs is set it's value will be different in the actual execution domain. The following code will produce the error if script.cs uses any assembly form the Lib directory.

CSScript.GlobalSettings.AddSearchDir(Path.GetFullPath("Lib"));
CSScript.AssemblyResolvingEnabled = true;  

string asmFile = CSScript.Compile("script.cs", null, false);
using (AsmHelper helper = new AsmHelper(asmFile, "tempDomain"true))
{
    helper.Invoke("Script.Report""Hello!"); //ERROR
}

In order to fix the problem the code has to be modified as following:

CSScript.GlobalSettings.AddSearchDir(Path.GetFullPath("Lib"));

string asmFile = CSScript.Compile("script.cs", null, false);
using (AsmHelper helper = new AsmHelper(asmFile, "tempDomain"true))
{
    helper.ProbingDirs = CSScript.GlobalSettings.SearchDirs.Split(';');
    helper.Invoke("Script.Report""Hello!");
}

Script specific probing directories

As an alternative to the GlobalSettings approach you may specify probing directories for particular script only. This can be accomplished by passing Settings object to the script compiler and setting the AsmHelper.ProbingDirs property.

Settings settings = new Settings();
settings.AssSearchDir(Path.GetFullPath("Lib"));

string asmFile = CSScript.CompileWithConfig("script.cs", null, false, settings, "");

AsmHelper helper = new 
AsmHelper (Assembly.LoadFrom(asmFile));
helper.ProbingDirs = settings.SearchDirs.Split(';');        
helper.Invoke("Script.Report""Hello!");
The code above will work with remote script execution too.

PInvoke probing directories

When using PInvoke to call unmanaged functions that are implemented in a DLL CLR searches the DLLs in the local (with respect to main application) directory and in all directories of the system PATH environment variables. CS-Script automatically adds all SearchDirs to the system PATH thus native DLL probing directories can be managed in the same way as assembly probingdirectories.  


Script caching

Script caching is available durings script execution from the command-prompt (see /c switch). The same script caching mechanism is engaged while executing by the engine hosted by an other application. You can enable/disable the caching by setting the CSScript.CacheEnabled property (true by default). Practically it means that if you are executing the script from the application it will not be recompiled every time unless it is changes since the last execution.


Script Compiling Errors
If the script to be executed generated compiling it is possible now to extract the detailed information about the error. The iformation is stored in the Data dictionary of CompilerException class. This technique is also useful when it is required to distinguish compiling Errors and Warnings.


try

{

    CSScript.LoadCode(code);

}

catch (CompilerException e)

{

    CompilerErrorCollection errors = (CompilerErrorCollection)e.Data["Errors"];

 

    foreach (CompilerError err in errors)

    {

        Console.WriteLine("{0}({1},{2}): {3} {4}: {5}",

                            err.FileName,

                            err.Line,

                            err.Column,

                            err.IsWarning ? "warning" : "error",

                            err.ErrorNumber,

                            err.ErrorText);

    }

}


Concurrency Control
It is important to understand implications of concurrent script execution. The script execution is always tyhread-unsafe and it is (as with any dynamically loaded assembly) a resoponsibility of the host application to synchronise access to the sharted resources. However the script Loading/Compiling is always thread-safe.

The fine/precise control over the concurrency is possible with the runtime setting OptimisticConcurrencyModel.  it is set to true (default value) the script loading (not the execution) is globally thread-safe. If it is set to false the script loading is thread-safe only among loading operations for the same script file.

CSScript.GlobalSettings.OptimisticConcurrencyModel = false;
AsmHelper script = new AsmHelper(CSScript.Load("myScript.cs"));
...

This setting is to be used with the caution. While it can bring some performance benefits when the list of probing directories is large it also may be wrong to assume that if the assembly in not found in a particular directory it still will not be there if the probing is repeated.

Note: standalone script engine (cece.exe/csws.exe) always internally sets CacheProbingResults to true before the execution as it is safe to assume no chages in the assembly locations during a single script execution.

See Also

Reference | Tutorial (Text Processor) | Image Processor