Il motore di scripting

Creare un sistema di script C# utente
Il diagramma delle classi per gli script
Il diagramma delle classi per gli script

Le classi per gli script utente riprendono lo schema con una interfaccia, una implementazione comune, e quattro classi specializzate, una per ogni modulo (scatole e cartelline, essendo molto simili la condividono).

Come per le altre sezioni dal diagramma all'inizio di questa pagina potete accedere ai listati di tutte le classi cliccando sui riquadri.

Queste classi hanno tre metodi: il primo restituisce un testo con un template per gli script, questo metodo è utilizzato dalla gestione della command line per generare l'output del comando --helpscript.

Il secondo metodo inserisce lo script all'interno di una classe dove c'è una proprietà di tipo IEngine nella quale si troverà una istanza della classe a cui viene applicato lo script: è utilizzabile per prelevare le impostazioni raccolte dalla command line o dalla GUI.

Viene aggiunto anche un costruttore che, oltre ad inizializzare la proprietà vista prima chiama, se è stato definito, un metodo Init utilizzabile per particolari inizializzazioni.

Questo il codice:

    public virtual string WrapScript(string script, string engine) => $@"{Compiler.Usings}
namespace Casasoft.CCDV.Scripts;

public class UserScript
{{
    private {engine} engine;
    public UserScript(IEngine eng)
    {{
        engine = ({engine})eng;
        System.Reflection.MethodInfo m = this.GetType().GetMethod("
"Init"");
        if (m != null) m.Invoke(this, null);
    }}
{script}
}}"
;

Infine il terzo metodo: restituisce un assembly con l'eseguibile compilato della classe creata con il wrapper-

Il codice è all'apparenza banale:

public virtual Assembly Compile(string script) => Compiler.Compile(WrapScript(script));

La compilazione

Il "trucco" sta tutto nella classe statica Compiler, riportata nel Listato S7 (Nuova pagina), che effettua la compilazione a run-time.

La classe Compiler crea una sorta di progetto ed inizia proprio con l'aggiungere le reference alle varie librerie e pacchetti:

MetadataReference[] references = new MetadataReference[]
{
        MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
        MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),
        MetadataReference.CreateFromFile(FromTrustedPlatformAssembly("System.Linq.dll")),
        MetadataReference.CreateFromFile(FromTrustedPlatformAssembly("System.Linq.Expressions.dll")),
        MetadataReference.CreateFromFile(FromTrustedPlatformAssembly("System.Linq.Queryable.dll")),
        MetadataReference.CreateFromFile(FromTrustedPlatformAssembly("System.Core.dll")),
        MetadataReference.CreateFromFile(FromTrustedPlatformAssembly("System.Runtime.dll")),
        MetadataReference.CreateFromFile(FromTrustedPlatformAssembly("System.Runtime.Loader.dll")),
        MetadataReference.CreateFromFile(FromTrustedPlatformAssembly("System.Console.dll")),
        MetadataReference.CreateFromFile(FromTrustedPlatformAssembly("System.Collections.dll")),
        MetadataReference.CreateFromFile(FromTrustedPlatformAssembly("System.Collections.Concurrent.dll")),
        MetadataReference.CreateFromFile(FromTrustedPlatformAssembly("System.Memory.dll")),
        MetadataReference.CreateFromFile(FromTrustedPlatformAssembly("netstandard.dll")),
        MetadataReference.CreateFromFile(Path.Combine(WorkPath, "Magick.NET-Q16-AnyCPU.dll")),
        MetadataReference.CreateFromFile(Path.Combine(WorkPath, "Magick.NET.Core.dll")),
        MetadataReference.CreateFromFile(Path.Combine(WorkPath, "Casasoft.CCDV.Common.dll"))
};

Fatto questo inizia la fase di parsing del codice, la compilazione e l'estrazione dell'assembly virtuale che non viene scritto du disco, ma rimane in una variabile in memoria.

SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
// analyse and generate IL code from syntax tree
CSharpCompilation compilation = CSharpCompilation.Create(
        assemblyName,
        syntaxTrees: new[] { syntaxTree },
        references: references,
        options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

// write IL code into memory
EmitResult result = compilation.Emit(ms);

// load this 'virtual' DLL so that we can use
ms.Seek(0, SeekOrigin.Begin);
return Assembly.Load(ms.ToArray());

Per rendere più leggibile questa fase è stata omessa la parte che riporta gli eventuali errori riscontrati in fase di compilazione.

Negli engine la compilazione avviene assegnando la proprietà Script nella classe BaseEngine come si vede in questo frammento di codice:

private string _script;
/// <summary>
/// c# script for custom processing
/// </summary>
public string Script
{
        get => _script;
        set
        {
                _script = value;
                if (!string.IsNullOrWhiteSpace(value))
                {
                        CustomCode = ScriptingClass.Compile(value);
                }
        }
}
/// <summary>
/// compiled script for custom processing
/// </summary>
public Assembly CustomCode { get; set; }
/// <summary>
/// Class that handles user scripts
/// </summary>
public IScripting ScriptingClass { get; set; }

L'esecuzione

La classe Compiler contiene anche i metodi per inizializzare la classe dello script ed eseguirne i metodi.

Il metodo New, in analogia con l'omonimo operatore, crea una istanza della classe:

/// <summary>
/// Creates an istance of the class
/// </summary>
/// <param name="assembly">Memory assembly with compiled code</param>
/// <param name="ClassName">Fully qualified class name</param>
/// <param name="eng">Engine passed to constructor</param>
/// <returns></returns>
public static object New(Assembly assembly, string ClassName, IEngine eng)
{
        Type type = assembly.GetType(ClassName);
        if (type is not null)
        {
                return Activator.CreateInstance(type, new object[] { eng });
        }
        else return null;
}

Come si evince anche dal commento il metodo prende come parametri la variabile contenente l'assembly virtuale, il nome della classe ed il parametro da passare al costruttore (una classe con intefaccia IEngine).

Viene ottenuto il tipo corrispondente al nome della classe e poi Activator.CreateInstance è l'equivalente del new, mentre il costruttore viene scelto in base alla lista di parametri presente nell'array di oggetti.

Questo metodo viene richiamato dal GetResult della classe BaseEngine e di conseguenza ereditato da tutte le classi derivate.

Il codice:

/// <summary>
/// Does the dirty work
/// </summary>
/// <param name="quiet">suppress messages when running</param>
/// <returns>Image to print</returns>
public virtual MagickImage GetResult(bool quiet)
{
        if (CustomCode is not null)
        {
                ScriptInstance = Compiler.New(CustomCode, this);
        }
        return null;
}

Creata l'istanza della classe entra in gioco il metodo Run che, come si può intuire, esegue un metodo della classe:

/// <summary>
/// Executes a method of an istanced object
/// </summary>
/// <param name="obj">instance</param>
/// <param name="Method">Method to execute</param>
/// <param name="args">Array of arguments</param>
/// <returns></returns>
public static object Run(object obj, string Method, object[] args)
{
        Type type = obj.GetType();
        if (type is not null && type.GetMethod(Method) is not null)
        {

                return type.InvokeMember(Method,
                BindingFlags.Default | BindingFlags.InvokeMethod,
                null,
                obj,
                args);
        }
        else
                return null;
}

Al metodo viene passato l'oggetto precedentemente creato, il nome del metodo da eseguire ed una lista di parametri; ritorna un oggetto generico che è il risultato dell'esecuzione.

Un tipico esempio di utilizzo è questo frammento di codice preso dall'engine di MontaggioDorsi:

if (ScriptInstance is not null)
{
        var im = Compiler.Run(ScriptInstance, "ProcessOnLoad", new object[] { image });
        if (im is not null)
        {
                image = (MagickImage)im;
        }
}

Gli esempi

Esempi degli script utilizzabili sono riportati nel manuale di MontaggioDorsi e nel manuale di MontaggioFoto.

Inizio pagina
 
Precedente
Sommario
Successivo