Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Compile and execute C# code from string at run time
#1
Code:
Copy      Help
// class "CsScript.cs"
/*/ r Roslyn\Microsoft.CodeAnalysis.CSharp.dll; r Roslyn\Microsoft.CodeAnalysis.dll; /*/ //C# compiler dlls used by LA
/*/ nuget Roslyn\Microsoft.CodeAnalysis.CSharp; /*/ //or use this instead (swap lines)

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
using System.Runtime.Loader;
using System.Reflection;

/// <summary>
///
Compiles and executes C# code at run time.
/// </summary>
class CsScript {
    readonly Assembly _asm;
    static List<MetadataReference> s_refs = _GetRefs();
    
    CsScript(Assembly assembly) {
        _asm = assembly;
    }

    
    /// <summary>
    ///
Compiles C# code.
    /// </summary>
    ///
<param name="code">C# code. In code can be used everything from .NET, Au.dll and <i>r</i> libraries.</param>
    ///
<param name="library">The code contains only classes and no entry point.</param>
    ///
<param name="c">C# files (if end with ".cs") or codes to add to the compilation as separate files. For example can contain `using global` directives. Example: <c>c: new[] { @"%folders.Workspace%\files\Classes\global.cs" }</c>.</param>
    ///
<param name="r">Additional reference assemblies (dll files). Example: <c>r: new[] { @"%folders.Workspace%\.nuget\-\Markdig.dll" }</c>.</param>
    ///
<param name="errors">If not null, the function appends error descriptions.</param>
    ///
<returns>A <b>CsScript</b> object containing the compiled assembly. It's an in-memory collectible assembly. If fails to compile, prints errors and returns null.</returns>
    ///
<remarks>
    ///
<para>
    ///
Slow when used the first time in current process, because JIT-compiles the Roslyn compiler. Can be 1 or several seconds. Later less than 100 ms (if code is small).
    /// </para>
    ///
<para>
    ///
File paths must be full or relative to <see cref="folders.ThisApp"/>. The function calls <see cref="pathname.normalize"/>.
    /// </para>
    ///
</remarks>
    public static CsScript Compile(string code, bool library = false, string[] c = null, string[] r = null, List<string> errors = null) {
        IEnumerable<MetadataReference> mr = s_refs;
        if (r != null) {
            try {
                r = r.Select(o => pathname.normalize(o, folders.ThisApp)).ToArray();
                mr = s_refs.Concat(r.Select(o => MetadataReference.CreateFromFile(o))).ToArray();
            }

            catch (Exception e1) {
                var e = $"Failed to load r. {e1}";
                if (errors == null) { print.it("FAILED TO COMPILE"); print.it(e); } else errors.Add(e);
                return null;
            }
        }

        
        CSharpParseOptions parseOpt = new(LanguageVersion.Preview, DocumentationMode.None);
        var tree = CSharpSyntaxTree.ParseText(code, parseOpt, "code");
        var trees = c == null
            ? new SyntaxTree[] { tree }
            : c.Select((o, i) => o.Ends(".cs", true) ? CSharpSyntaxTree.ParseText(filesystem.loadText(pathname.normalize(o, folders.ThisApp)), parseOpt, pathname.getName(o)) : CSharpSyntaxTree.ParseText(o, parseOpt, $"c[{i}]")).Append(tree).ToArray();
        
        var compOpt = new CSharpCompilationOptions(library ? OutputKind.DynamicallyLinkedLibrary : OutputKind.WindowsApplication, allowUnsafe: true);
        var compilation = CSharpCompilation.Create("script", trees, mr, compOpt);
        var memStream = new MemoryStream();
        var emitResult = compilation.Emit(memStream);
        if (!emitResult.Success) {
            var e = emitResult.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).Select(d => d.ToString());
            if (errors == null) { print.it("FAILED TO COMPILE"); print.it(e); } else foreach (var v in e) errors.Add(v);
            return null;
        }

        
        memStream.Position = 0;
        var alc = new AssemblyLoadContext(null, isCollectible: true);
        if (r != null) {
            var d = r.ToDictionary(o => pathname.getNameNoExt(o), StringComparer.OrdinalIgnoreCase);
            alc.Resolving += (alc, k) => d.TryGetValue(k.Name, out var path) ? alc.LoadFromAssemblyPath(path) : null;
        }

        return new(alc.LoadFromStream(memStream));
    }

    
    /// <summary>
    ///
Creates <b>MetadataReference</b> for all .NET assemblies and Au.dll.
    /// </summary>
    static List<MetadataReference> _GetRefs() {
        var r = new List<MetadataReference>();
#if true //use ref.db if exists. The process uses less memory, eg 165 MB -> 65 MB. And slightly faster.
        var rdb = folders.ThisAppBS + "ref.db"; //if role exeProgram, copy it to the exe folder from LA folder. Else exists.
        if (filesystem.exists(rdb)) {
            using var db = new sqlite(rdb, SLFlags.SQLITE_OPEN_READONLY);
            using var stat = db.Statement("SELECT * FROM ref");
            while (stat.Step()) r.Add(MetadataReference.CreateFromImage(stat.GetArray<byte>(1), filePath: stat.GetText(0)));
            r.Add(MetadataReference.CreateFromFile(folders.ThisAppBS + "Au.dll"));
        }
else {
            var s = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string;
            foreach (var v in s.Split(';', StringSplitOptions.RemoveEmptyEntries)) {
                if (v.Starts(folders.ThisAppBS, true) && !v.Ends(@"\Au.dll", true)) continue;
                r.Add(MetadataReference.CreateFromFile(v));
            }
        }

#else
        var s = AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string;
        foreach (var v in s.Split(';', StringSplitOptions.RemoveEmptyEntries)) {
            if (v.Starts(folders.ThisAppBS, true) && !v.Ends(@"\Au.dll", true)) continue;
            r.Add(MetadataReference.CreateFromFile(v));
        }
#endif
        return r;
    }

    
    /// <summary>
    ///
Gets the compiled assembly.
    /// </summary>
    public Assembly Assembly => _asm;
    
    /// <summary>
    ///
Executes the entry point of the script program (top-level statements or Main).
    /// </summary>
    ///
<param name="args">Command line arguments.</param>
    ///
<returns>If the script returns int, returns it. Else 0.</returns>
    ///
<exception cref="Exception">Exceptions of <see cref="MethodBase.Invoke(object, object[])"/>.</exception>
    public int Run(params string[] args) {
        var main = _asm.EntryPoint;
        var o = main.Invoke(null, new object[] { args });
        return o is int i ? i : 0;
    }

    
    /// <summary>
    ///
Creates a delegate of type <i>T</i> for a public static method.
    /// </summary>
    ///
<param name="type">Class name.</param>
    ///
<param name="method">Method name.</param>
    ///
<exception cref="Exception">Not found type; not found method; ambiguous match (several overloads of the method exist); the method type does not match <i>T</i>.</exception>
    public T GetMethod<T>(string type, string method) where T : Delegate {
        var t = _asm.GetType(type) ?? throw new ArgumentException("class not found: " + type);
        var m = t.GetMethod(method, BindingFlags.Public | BindingFlags.Static) ?? throw new ArgumentException("public static method not found: " + method);
        return m.CreateDelegate<T>();
    }

    
    /// <summary>
    ///
Calls a public static method using reflection (<see cref="MethodBase.Invoke(object, object[])"/>).
    /// </summary>
    ///
<param name="type">Class name.</param>
    ///
<param name="method">Method name.</param>
    ///
<param name="parameters">Method parameters.</param>
    ///
<exception cref="Exception">Not found type; not found method; ambiguous match (several overloads of the method exist); etc; exceptions thrown by the called method.</exception>
    public object Call(string type, string method, params object[] parameters) {
        var t = _asm.GetType(type) ?? throw new ArgumentException("class not found: " + type);
        var m = t.GetMethod(method, BindingFlags.Public | BindingFlags.Static) ?? throw new ArgumentException("public static method not found: " + method);
        return m.Invoke(null, parameters);
    }

    
    /// <summary>
    ///
Creates an instance of a type defined in the compiled code.
    /// </summary>
    ///
<param name="type">Class name. The class must be public.</param>
    ///
<param name="args">Arguments for the constructor.</param>
    ///
<returns>Because the type is defined only in the compiled code and is unknown in the caller code, returns the instance object as dynamic type. You can call its public functions, but there is no intellisense, and errors are detected at run time.</returns>
    ///
<exception cref="Exception">Not found type; can't create instance.</exception>
    public dynamic CreateInstance(string type, params object[] args) {
        var t = _asm.GetType(type) ?? throw new ArgumentException("class not found: " + type);
        return Activator.CreateInstance(t, args);
        
        //GetType gets public and internal types. But if internal, error when calling public methods through dynamic. OK when using reflection.
    }
}
#2
Examples.
 
Code:
Copy      Help
// script ""
/*/ c CsScript.cs; /*/

string code = """
using System;
using Au;

foreach (var v in args) print.it(v);
"""
;

var c = CsScript.Compile(code);
if (c == null) return;

//print.redirectConsoleOutput = true; //need this if the script contains Console.WriteLine and the caller app isn't console
c.Run("command", "line", "arguments");
 
Code:
Copy      Help
// script ""
/*/ c CsScript.cs; /*/
var c = CsScript.Compile("""class Class1 { public static int Add(int a, int b) { return a + b; } }""", true);

print.it(c.Call("Class1", "Add", 1, 4));

var add = c.GetMethod<Func<int, int, int>>("Class1", "Add");
print.it(add(2, 5));
 
Code:
Copy      Help
// script ""
/*/ c CsScript.cs; /*/

var globals = new[] { """
global using System;
global using Au;
"""

};

string code = """
public class Class1 {
public Class1() { print.it("ctor"); }
public int Method(int k) { print.it("Method", k); return k*2; }
}
"""
;

var c = CsScript.Compile(code, true, globals);
if (c == null) return;

var k = c.CreateInstance("Class1");
print.it(k.Method(5));
 
Code:
Copy      Help
// script ""
/// Compile code with a class and call static methods.

/*/ c CsScript.cs; /*/

string code2 = """
using Au;
public class Class1 {
    public static void Print(string s) { print.it(s); }
    public static int Add(int a, int b) { return a + b; }
    public static void F3(out string s, int i = 0) { s = "OUT " + i; }
}
"""
;

var c2 = CsScript.Compile(code2, library: true);
if (c2 == null) return;

var prt = c2.GetMethod<Action<string>>("Class1", "Print");
prt("TEST");
var add = c2.GetMethod<Func<int, int, int>>("Class1", "Add");
print.it(add(2, 5), add(1000, 1));
var f3 = c2.GetMethod<Delegates.D1>("Class1", "F3");
f3(out var s1); print.it(s1);

/// <summary>
///
Examples of delegates for methods where cannot be used <b>Action</b> or <b>Func</b>, for example with in/ref/out/params/optional parameters.
/// </summary>
class Delegates {
    public delegate void D1(out string s, int i = 0);
}
 
Code:
Copy      Help
// script ""
/// Compile code with a class, create a class instance as dynamic, and call functions.

/*/ c CsScript.cs; /*/

string code3 = """
using Au;
public class Class1 {
    int _value;
    public Class1() { }
    public Class1(int value) { _value = value; }
    public void Print(string s) { print.it(s); }
    public int GetValue() { return _value; }
    public void F3(out string s, int i = 0) { s = "OUT " + i; }
    public int Prop { get; set; }
}
"""
;

var c3 = CsScript.Compile(code3, library: true);
if (c3 == null) return;
//var d = c3.CreateInstance("Class1"); //use constructor with 0 paramaters
var d = c3.CreateInstance("Class1", 3); //use constructor with 1 paramater
d.Print("test");
print.it(d.GetValue());
d.F3(out string s2); print.it(s2);
d.Prop = 4; print.it(d.Prop);


Forum Jump:


Users browsing this thread: 1 Guest(s)