From 1e694f4800ee25fb2e8b5420ca16f50f744796ad Mon Sep 17 00:00:00 2001 From: "Stief, Sebastian" Date: Tue, 2 Apr 2024 10:36:25 +0200 Subject: [PATCH 1/3] Reference Loading for Class-Files /Scripts --- .../ClassFiles/TestClass.csx | 14 ++++ .../SimpleCodeExecutionTests.cs | 32 +++++++ .../Westwind.Scripting.Test.csproj | 6 ++ Westwind.Scripting/CSharpScriptExecution.cs | 84 ++++++++++++++++++- 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 Westwind.Scripting.Test/ClassFiles/TestClass.csx diff --git a/Westwind.Scripting.Test/ClassFiles/TestClass.csx b/Westwind.Scripting.Test/ClassFiles/TestClass.csx new file mode 100644 index 0000000..1d5fc1d --- /dev/null +++ b/Westwind.Scripting.Test/ClassFiles/TestClass.csx @@ -0,0 +1,14 @@ +using System.IO; + +public static class TestClass { + + public static int DoMath(int n1, int n2) { + return n1 + n2; + } +} +public static class TestClass2 { + + public static int DoOtherMath(int n1, int n2) { + return n1 - n2; + } +} diff --git a/Westwind.Scripting.Test/SimpleCodeExecutionTests.cs b/Westwind.Scripting.Test/SimpleCodeExecutionTests.cs index dee4402..f5fdb05 100644 --- a/Westwind.Scripting.Test/SimpleCodeExecutionTests.cs +++ b/Westwind.Scripting.Test/SimpleCodeExecutionTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections; using System.Diagnostics; +using System.IO; +using System.Reflection; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -1037,6 +1039,36 @@ public class Customer Assert.IsNotNull(customer.CustomerInfo.Name, "Customer should not be null"); Console.WriteLine(customer.CustomerInfo.Name); } + + [TestMethod] + public void ExecuteCodeWithClassReference() + { + string cls = @"./ClassFiles/TestClass.csx"; + + var script = new CSharpScriptExecution() + { + SaveGeneratedCode = true, + AllowReferencesInCode = true + }; + script.AddDefaultReferencesAndNamespaces(); + + var code = $@" +#c {cls} +int result = TestClass.DoMath(5,6); +return result; + "; + + int result = script.ExecuteCode(code); + + Console.WriteLine($"Result: {result}"); + Console.WriteLine($"Error: {script.Error}"); + Console.WriteLine(script.ErrorMessage); + Console.WriteLine(script.GeneratedClassCodeWithLineNumbers); + + Assert.IsFalse(script.Error, script.ErrorMessage); + Assert.IsTrue(result == 11); + + } } diff --git a/Westwind.Scripting.Test/Westwind.Scripting.Test.csproj b/Westwind.Scripting.Test/Westwind.Scripting.Test.csproj index 9fd49ba..d85d7d7 100644 --- a/Westwind.Scripting.Test/Westwind.Scripting.Test.csproj +++ b/Westwind.Scripting.Test/Westwind.Scripting.Test.csproj @@ -59,8 +59,14 @@ + + PreserveNewest + PreserveNewest + + + \ No newline at end of file diff --git a/Westwind.Scripting/CSharpScriptExecution.cs b/Westwind.Scripting/CSharpScriptExecution.cs index f588d9c..a15816e 100644 --- a/Westwind.Scripting/CSharpScriptExecution.cs +++ b/Westwind.Scripting/CSharpScriptExecution.cs @@ -9,6 +9,7 @@ using System.Runtime.Loader; using System.Security.Cryptography; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -52,6 +53,10 @@ public class CSharpScriptExecution /// public ReferenceList References { get; } = new ReferenceList(); + /// + /// Dictionary of additional Class-File references that are read and inject into the namespace + /// + public Dictionary ReferenceClasses { get; } = new Dictionary(); /// /// Last generated code for this code snippet with line numbers @@ -98,6 +103,7 @@ public class CSharpScriptExecution /// /// If true parses references in code that are referenced with: /// #r assembly.dll + /// #c Class-File /// public bool AllowReferencesInCode { get; set; } = false; @@ -1183,10 +1189,21 @@ private StringBuilder GenerateClass(string classBody) sb.AppendLine(classBody); sb.AppendLine(); + /* sb.AppendLine("} " + Environment.NewLine + "}"); // Class and namespace closed + */ + + sb.Append("} ").AppendLine(Environment.NewLine);// close class + + foreach (var entry in ReferenceClasses) + { + sb.Append(entry.Value).AppendLine(Environment.NewLine); + } + + sb.AppendLine("} ");// close namespace if (SaveGeneratedCode) GeneratedClassCode = sb.ToString(); @@ -1463,7 +1480,21 @@ public void AddAssemblies(params string[] assemblies) AddAssembly(file); } + /// + /// Add Code from a ClassFile with Class-Definition inside + /// + /// + public void AddReferenceClass(string classname, string classcode) + { + if (string.IsNullOrEmpty(classcode)) + { + ReferenceClasses.Clear(); + return; + } + // we override classes of the same name + ReferenceClasses[classname] = classcode; + } /// /// Adds a namespace to the referenced namespaces @@ -1548,7 +1579,7 @@ private string ParseReferencesInCode(string code, bool referencesOnly = false) { if (string.IsNullOrEmpty(code)) return code; - if (!code.Contains("#r ") && ( !referencesOnly && !code.Contains("using ") ) ) + if (!code.Contains("#r ") && !code.Contains("#c ") && ( !referencesOnly && !code.Contains("using ") ) ) return code; StringBuilder sb = new StringBuilder(); @@ -1569,6 +1600,57 @@ private string ParseReferencesInCode(string code, bool referencesOnly = false) continue; } + if (line.Trim().StartsWith("#c ")) + { + if (AllowReferencesInCode) + { + string scriptClass = line.Replace("#c ", "").Trim(); + string scriptClassCode = File.ReadAllText(scriptClass); + var class_lines = GetLines(scriptClassCode); + Regex classline = new Regex(".* class ([a-zA-Z0-9]*) .*", RegexOptions.Compiled); + StringBuilder aNewClass = new StringBuilder(); + string cls_name = "?"; + bool allUsing = false;// all Using until first Class Definition + foreach (var c_line in class_lines) + { + if (!referencesOnly && !allUsing && c_line.Trim().Contains("using ")) + { + string ns = c_line.Replace("using ", "").Replace(";", "").Trim(); + AddNamespace(ns); + aNewClass.AppendLine("// " + line); + continue; + } + else + { + Match cls_match = classline.Match(c_line); + if (cls_match.Success) + { + if (allUsing){ // first class already found + AddReferenceClass(cls_name, aNewClass.ToString()); + aNewClass.Clear(); + } + cls_name = cls_match.Groups[1].Value; + allUsing = true; + } + aNewClass.AppendLine(c_line); + + } + + /* TODO csx should not include a namespace + if (line.Trim().Contains("namespace ")) + { + + } + */ + } + AddReferenceClass(cls_name, aNewClass.ToString()); + sb.AppendLine("// " + line); + continue; + } + sb.AppendLine("// not allowed: " + line); + continue; + } + if (!referencesOnly && line.Trim().Contains("using ") && !line.Contains("(")) { string ns = line.Replace("using ", "").Replace(";", "").Trim(); From 65b2375ff3a852a18f0b4d87953912c268040c74 Mon Sep 17 00:00:00 2001 From: "Stief, Sebastian" Date: Tue, 2 Apr 2024 16:15:55 +0200 Subject: [PATCH 2/3] Clear Assembly Index-Cache --- Westwind.Scripting/CSharpScriptExecution.cs | 22 +++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Westwind.Scripting/CSharpScriptExecution.cs b/Westwind.Scripting/CSharpScriptExecution.cs index a15816e..1af6114 100644 --- a/Westwind.Scripting/CSharpScriptExecution.cs +++ b/Westwind.Scripting/CSharpScriptExecution.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; @@ -1223,9 +1224,26 @@ private string ParseCodeWithParametersArray(string code, object[] parameters) return code; } -#endregion + /// + /// Removes all hash-indexes of Assembly Cache + /// + /// + public static bool ClearCachedAssemblies() + { + try + { + CachedAssemblies.Clear(); + return true; + } + catch + { + return false; + } + } + + #endregion -#region References and Namespaces + #region References and Namespaces /// From a19371b2bd80995cf1f26b095d4308dab14b4032 Mon Sep 17 00:00:00 2001 From: "Stief, Sebastian" Date: Wed, 3 Apr 2024 08:40:30 +0200 Subject: [PATCH 3/3] cache class for custom caching --- Westwind.Scripting/CSharpScriptExecution.cs | 55 +++++++++++------- Westwind.Scripting/Cache/ICache.cs | 24 ++++++++ .../Cache/MemoryAssemblyCache.cs | 56 +++++++++++++++++++ 3 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 Westwind.Scripting/Cache/ICache.cs create mode 100644 Westwind.Scripting/Cache/MemoryAssemblyCache.cs diff --git a/Westwind.Scripting/CSharpScriptExecution.cs b/Westwind.Scripting/CSharpScriptExecution.cs index 1af6114..a495126 100644 --- a/Westwind.Scripting/CSharpScriptExecution.cs +++ b/Westwind.Scripting/CSharpScriptExecution.cs @@ -1,22 +1,18 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Reflection.Emit; using System.Reflection.Metadata; using System.Runtime.Loader; -using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; - +using Westwind.Scripting.Cache; namespace Westwind.Scripting { @@ -39,15 +35,15 @@ public class CSharpScriptExecution /// List holds a list of cached assemblies with a hash code for the code executed as /// the key. /// - protected static ConcurrentDictionary CachedAssemblies = - new ConcurrentDictionary(); + //protected static ConcurrentDictionary CachedAssemblies = + // new ConcurrentDictionary(); + protected static ICache CachedAssemblies { get; private set; } /// /// List of additional namespaces to add to the script /// public NamespaceList Namespaces { get; } = new NamespaceList(); - /// /// List of additional assembly references that are added to the /// compiler parameters in order to execute the script code. @@ -193,6 +189,13 @@ public class CSharpScriptExecution #endregion + public CSharpScriptExecution() : this(new MemoryAssemblyCache()) { } + + public CSharpScriptExecution(ICache cache) + { + CachedAssemblies = cache; + } + /// /// Creates a default Execution Engine which has: /// @@ -268,22 +271,25 @@ public object ExecuteMethod(string code, string methodName, params object[] para { int hash = GenerateHashCode(code); - if (!CachedAssemblies.ContainsKey(hash)) + if (!CachedAssemblies.Contains(hash)) { var sb = GenerateClass(code); if (!CompileAssembly(sb.ToString())) return null; - CachedAssemblies[hash] = Assembly; + CachedAssemblies.Set(hash, Assembly); } else { - Assembly = CachedAssemblies[hash]; + if(CachedAssemblies.TryGet(hash, out var tmp_Assembly)) + { + Assembly = tmp_Assembly; - // Figure out the class name - var type = Assembly.ExportedTypes.First(); - GeneratedClassName = type.Name; - GeneratedNamespace = type.Namespace; + // Figure out the class name + var type = Assembly.ExportedTypes.First(); + GeneratedClassName = type.Name; + GeneratedNamespace = type.Namespace; + } } } @@ -1102,16 +1108,20 @@ public Type CompileClassToType(string code) { int hash = code.GetHashCode(); - if (!CachedAssemblies.ContainsKey(hash)) + if (!CachedAssemblies.Contains(hash)) { if (!CompileAssembly(code)) return null; - CachedAssemblies[hash] = Assembly; + CachedAssemblies.Set(hash, Assembly); } else { - Assembly = CachedAssemblies[hash]; + if(CachedAssemblies.TryGet(hash, out Assembly? tmp_assembly)) + { + Assembly = tmp_assembly; + } + } } @@ -1139,17 +1149,20 @@ public Type CompileClassToType(Stream codeStream) else { int hash = codeStream.GetHashCode(); - if (!CachedAssemblies.ContainsKey(hash)) + if (!CachedAssemblies.Contains(hash)) { if (!CompileAssembly(codeStream)) return null; - CachedAssemblies[hash] = Assembly; + CachedAssemblies.Set(hash, Assembly); } else { - Assembly = CachedAssemblies[hash]; + if (CachedAssemblies.TryGet(hash, out Assembly tmp_assembly)) + { + Assembly = tmp_assembly; + } } } diff --git a/Westwind.Scripting/Cache/ICache.cs b/Westwind.Scripting/Cache/ICache.cs new file mode 100644 index 0000000..cdca6bd --- /dev/null +++ b/Westwind.Scripting/Cache/ICache.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Westwind.Scripting.Cache +{ + public interface ICache + { + + void Set(T_KEY key, T_VALUE value); + + bool TryGet(T_KEY key, out T_VALUE? value); + + new IEnumerable Keys(); + + new IEnumerable Values(); + + void Clear(); + + bool Contains(T_KEY key); + } +} diff --git a/Westwind.Scripting/Cache/MemoryAssemblyCache.cs b/Westwind.Scripting/Cache/MemoryAssemblyCache.cs new file mode 100644 index 0000000..4b4517f --- /dev/null +++ b/Westwind.Scripting/Cache/MemoryAssemblyCache.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; + +namespace Westwind.Scripting.Cache +{ + internal class MemoryAssemblyCache : ICache + { + private readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); + + public void Set(int key, Assembly value) { + if (Cache.ContainsKey(key)) + { + if (Cache.TryRemove(key, out _)) + { + Cache.TryAdd(key, value); + } + } + else + { + Cache.TryAdd(key, value); + } + } + + public bool TryGet(int key, out Assembly? value) { + if (Cache.ContainsKey(key)) + { + return Cache.TryGetValue(key, out value); + } + value = default; + return false; + } + + public void Clear() + { + Cache.Clear(); + } + + public IEnumerable Keys() + { + return Cache.Keys; + } + + public IEnumerable Values() + { + return Cache.Values; + } + + public bool Contains(int key) + { + return Cache.ContainsKey(key); + } + } +}