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. The greatest benefit can be achieved when using interfaces as the types to be passed between the host and the script. Because they enforce good implementation/interface separation.
The script-assembly loading approach is not as critical as it seams. Whereas the remote loading obviously is the most flexible model it gives a little advantage, but requires all types, which are crossing AppDomain boundaries to be either sterilizable or descendant of MarshalByRefObject.
Use the following as a general guideline:
Use interfaces when you can and remote loading when you need 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:
|
| //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.
| //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