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.
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.
- AppDomain
initialization and unloading as well as types serialization
can
yield some significant performance penalties.
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.
class CSScript |
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"); |
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.
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:
|
IProcess script = instance.Alternativelly you can pass the exact location of the dependency assembly:AlignToInterface<IProcess>(true);
IProcess script = instance.AlignToInterface<IProcess>(<dependency assembly path>);
//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..."); } |
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 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; |
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); |
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(); |
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); |
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!"); |
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; |
Simplified Hosting Model probing directories
//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) { ...... } |
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!"); |
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 } |
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!"); } |
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!"); |
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.
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); } } |
... |
Reference | Tutorial (Text Processor) | Image Processor