CS-Script Evaluator LoadCode: How to compile and reference a second script (reusable library)

4.9k views Asked by At

The question in short is: How do you reference a second script containing reusable script code, under the constraints that you need to be able to unload and reload the scripts when either of them changes without restarting the host application?

I'm trying to compile a script class using the CS-Script "compiler as service" (CSScript.Evaluator), while referencing an assembly that has just been compiled from a second "library" script. The purpose is that the library script should contain code that can be reused for different scripts.

Here is a sample code that illustrates the idea but also causes a CompilerException at runtime.

using CSScriptLibrary;
using NUnit.Framework;

[TestFixture]
public class ScriptReferencingTests
{
    private const string LibraryScriptCode = @"
public class Helper
{
    public static int AddOne(int x)
    {
        return x + 1;
    }
}
";

    private const string ScriptCode = @"
using System;
public class Script
{
    public int SumAndAddOne(int a, int b)
    {
        return Helper.AddOne(a+b);
    }
}
";

    [Test]
    public void CSScriptEvaluator_CanReferenceCompiledAssembly()
    {
        var libraryEvaluator = CSScript.Evaluator.CompileCode(LibraryScriptCode);
        var libraryAssembly = libraryEvaluator.GetCompiledAssembly();
        var evaluatorWithReference = CSScript.Evaluator.ReferenceAssembly(libraryAssembly);
        dynamic scriptInstance = evaluatorWithReference.LoadCode(ScriptCode);

        var result = scriptInstance.SumAndAddOne(1, 2);

        Assert.That(result, Is.EqualTo(4));
    }
}

To run the code you need NuGet packages NUnit and cs-script.

This line causes a CompilerException at runtime:

dynamic scriptInstance = evaluatorWithReference.LoadCode(ScriptCode);

{interactive}(7,23): error CS0584: Internal compiler error: The invoked member is not supported in a dynamic assembly.

{interactive}(7,9): error CS0029: Cannot implicitly convert type '<fake$type>' to 'int'

Again, the reason for using CSScript.Evaluator.LoadCode instead of CSScript.LoadCode is so that the script can be reloaded at any time without restarting the host application when either of the scripts changes. (CSScript.LoadCode already supports including other scripts according to http://www.csscript.net/help/Importing_scripts.html)

Here is the documentation on the CS-Script Evaluator: http://www.csscript.net/help/evaluator.html

The lack of google results for this is discouraging, but I hope I'm missing something simple. Any help would be greatly appreciated.

(This question should be filed under the tag cs-script which does not exist.)

2

There are 2 answers

0
wezzix On BEST ANSWER

The quick solution to the CompilerException appears to be not use Evaluator to compile the assembly, but instead just CSScript.LoadCode like so

var compiledAssemblyName = CSScript.CompileCode(LibraryScriptCode);
var evaluatorWithReference = CSScript.Evaluator.ReferenceAssembly(compiledAssemblyName);
dynamic scriptInstance = evaluatorWithReference.LoadCode(ScriptCode);

However, as stated in previous answer, this limits the possibilities for dependency control that the CodeDOM model offers (like css_include). Also, any change to the LibraryScriptCode are not seen which again limits the usefulness of the Evaluator method.

The solution I chose is the AsmHelper.CreateObject and AsmHelper.AlignToInterface<T> methods. This lets you use the regular css_include in your scripts, while at the same time allowing you at any time to reload the scripts by disposing the AsmHelper and starting over. My solution looks something like this:

AsmHelper asmHelper = new AsmHelper(CSScript.Compile(filePath), null, false);
object obj = asmHelper.CreateObject("*");
IMyInterface instance = asmHelper.TryAlignToInterface<IMyInterface>(obj);
// Any other interfaces you want to instantiate...
...
if (instance != null)
    instance.MyScriptMethod();

Once a change is detected (I use FileSystemWatcher), you just call asmHelper.Dispose and run the above code again.

This method requires the script class to be marked with the Serializable attribute, or simply inherit from MarshalByRefObject.
Note that your script class does not need to inherit any interface. The AlignToInterface works both with and without it. You could use dynamic here, but I prefer having a strongly typed interface to avoid errors down the line.

I couldn't get the built in try-methods to work, so I made this extension method for less clutter when it is not known whether or not the interface is implemented:

public static class InterfaceExtensions
{
    public static T TryAlignToInterface<T>(this AsmHelper helper, object obj) where T : class
    {
        try
        {
            return helper.AlignToInterface<T>(obj);
        }
        catch
        {
            return null;
        }
    }
}

Most of this is explained in the hosting guidelines http://www.csscript.net/help/script_hosting_guideline_.html, and there are helpful samples mentioned in previous post.

I feel I might have missed something regarding script change detection, but this method works solidly.

1
user3032112 On

There is some slight confusion here. Evaluator is not the only way to achieve reloadable script behavior. CSScript.LoadCode allows reloading as well.

I do indeed advise to consider CSScript.Evaluator.LoadCode as a first candidate for the hosting model as it offers less overhead and arguably more convenient reloading model. However it comes with the cost. You have very little control over reloading and dependencies inclusion (assemblies, scripts). Memory leaks are not 100% avoidable. And it also makes script debugging completely impossible (Mono bug).

In your case I would really advice you to move to the more conventional hosting model: CodeDOM.

Have look at "[cs-script]\Samples\Hosting\CodeDOM\Modifying script without restart" sample.

And "[cs-script]\Samples\Hosting\CodeDOM\InterfaceAlignment" will also give you an idea how to use interfaces with reloading.

CodeDOM was for years a default CS-Script hosting mode and it is in fact very robust, intuitive and manageable. The only real drawback is the fact that all object you pass to (or get from) the script will need to be serializable or inherited from MarshalByRef. This is the side effect of the script being executed in the "automatic" separate domain. Thus one have to deal with the all "pleasures" of Remoting. BTW this is the only reason why I implemented Mono-based evaluator.

CodeDOM model will also automatically manage the dependencies and recompile them when needed. But it looks like you are aware about this anyway.

CodeDOM also allows you to define precisely the mechanism of checking dependencies for changes:

//the default algorithm "recompile if script or dependency is changed"
CSScript.IsOutOfDateAlgorithm = CSScript.CachProbing.Advanced; 

or

//custom algorithm "never recompile script" 
CSScript.IsOutOfDateAlgorithm = (s, a) => false;