Skip to content

Reference Loading for Class-Files /Scripts #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Westwind.Scripting.Test/ClassFiles/TestClass.csx
Original file line number Diff line number Diff line change
@@ -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;
}
}
32 changes: 32 additions & 0 deletions Westwind.Scripting.Test/SimpleCodeExecutionTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<int>(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);

}
}


Expand Down
12 changes: 12 additions & 0 deletions Westwind.Scripting.Test/Westwind.Scripting.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,16 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<None Update="ClassFiles\TestClass.csx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="ReferenceTest.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="ClassFiles\" />
</ItemGroup>
</Project>
156 changes: 135 additions & 21 deletions Westwind.Scripting/CSharpScriptExecution.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
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
{
Expand All @@ -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.
/// </summary>
protected static ConcurrentDictionary<int, Assembly> CachedAssemblies =
new ConcurrentDictionary<int, Assembly>();
//protected static ConcurrentDictionary<int, Assembly> CachedAssemblies =
// new ConcurrentDictionary<int, Assembly>();
protected static ICache<int, Assembly> CachedAssemblies { get; private set; }

/// <summary>
/// List of additional namespaces to add to the script
/// </summary>
public NamespaceList Namespaces { get; } = new NamespaceList();


/// <summary>
/// List of additional assembly references that are added to the
/// compiler parameters in order to execute the script code.
/// </summary>
public ReferenceList References { get; } = new ReferenceList();

/// <summary>
/// Dictionary of additional Class-File references that are read and inject into the namespace
/// </summary>
public Dictionary<string, string> ReferenceClasses { get; } = new Dictionary<string, string>();

/// <summary>
/// Last generated code for this code snippet with line numbers
Expand Down Expand Up @@ -96,6 +100,7 @@ public class CSharpScriptExecution
/// <summary>
/// If true parses references in code that are referenced with:
/// #r assembly.dll
/// #c Class-File
/// </summary>
public bool AllowReferencesInCode { get; set; } = false;

Expand Down Expand Up @@ -201,6 +206,13 @@ public class CSharpScriptExecution

#endregion

public CSharpScriptExecution() : this(new MemoryAssemblyCache()) { }

public CSharpScriptExecution(ICache<int, Assembly> cache)
{
CachedAssemblies = cache;
}

/// <summary>
/// Creates a default Execution Engine which has:
///
Expand Down Expand Up @@ -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;
}
}
}

Expand Down Expand Up @@ -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;
}

}
}

Expand Down Expand Up @@ -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;
}
}

}
Expand Down Expand Up @@ -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();

Expand All @@ -1221,9 +1254,26 @@ private string ParseCodeWithParametersArray(string code, object[] parameters)
return code;
}

#endregion
/// <summary>
/// Removes all hash-indexes of Assembly Cache
/// </summary>
/// <returns></returns>
public static bool ClearCachedAssemblies()
{
try
{
CachedAssemblies.Clear();
return true;
}
catch
{
return false;
}
}

#endregion

#region References and Namespaces
#region References and Namespaces


/// <summary>
Expand Down Expand Up @@ -1478,7 +1528,21 @@ public void AddAssemblies(params string[] assemblies)
AddAssembly(file);
}

/// <summary>
/// Add Code from a ClassFile with Class-Definition inside
/// </summary>
/// <param name="nameSpace"></param>
public void AddReferenceClass(string classname, string classcode)
{
if (string.IsNullOrEmpty(classcode))
{
ReferenceClasses.Clear();
return;
}

// we override classes of the same name
ReferenceClasses[classname] = classcode;
}

/// <summary>
/// Adds a namespace to the referenced namespaces
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand Down
24 changes: 24 additions & 0 deletions Westwind.Scripting/Cache/ICache.cs
Original file line number Diff line number Diff line change
@@ -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<T_KEY, T_VALUE>
{

void Set(T_KEY key, T_VALUE value);

bool TryGet(T_KEY key, out T_VALUE? value);

new IEnumerable<T_KEY> Keys();

new IEnumerable<T_VALUE> Values();

void Clear();

bool Contains(T_KEY key);
}
}
Loading