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 7998ec1..f9a9ef8 100644 --- a/Westwind.Scripting.Test/Westwind.Scripting.Test.csproj +++ b/Westwind.Scripting.Test/Westwind.Scripting.Test.csproj @@ -64,4 +64,16 @@ PreserveNewest + + + + PreserveNewest + + + PreserveNewest + + + + + \ No newline at end of file diff --git a/Westwind.Scripting/CSharpScriptExecution.cs b/Westwind.Scripting/CSharpScriptExecution.cs index 0da20aa..d90ef78 100644 --- a/Westwind.Scripting/CSharpScriptExecution.cs +++ b/Westwind.Scripting/CSharpScriptExecution.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -7,12 +6,13 @@ using System.Reflection.Metadata; using System.Runtime.Loader; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; - +using Westwind.Scripting.Cache; namespace Westwind.Scripting { @@ -35,21 +35,25 @@ 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. /// 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 @@ -96,6 +100,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; @@ -201,6 +206,13 @@ public class CSharpScriptExecution #endregion + public CSharpScriptExecution() : this(new MemoryAssemblyCache()) { } + + public CSharpScriptExecution(ICache cache) + { + CachedAssemblies = cache; + } + /// /// Creates a default Execution Engine which has: /// @@ -276,22 +288,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; + } } } @@ -1110,16 +1125,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; + } + } } @@ -1147,17 +1166,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; + } } } @@ -1198,10 +1220,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(); @@ -1221,9 +1254,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 /// @@ -1478,7 +1528,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 @@ -1563,7 +1627,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(); @@ -1584,6 +1648,56 @@ 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().StartsWith("using ") && line.Trim().EndsWith(";")) { string ns = line.Replace("using ", "").Replace(";", "").Trim(); 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); + } + } +}