using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; // ReSharper disable once CheckNamespace public class EdgeCompiler { private static readonly Regex ReferenceRegex = new Regex(@"^[\ \t]*(?:\/{2})?\#r[\ \t]+""([^""]+)""", RegexOptions.Multiline); private static readonly Regex UsingRegex = new Regex(@"^[\ \t]*(using[\ \t]+[^\ \t]+[\ \t]*\;)", RegexOptions.Multiline); private static readonly bool DebuggingEnabled = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("EDGE_CS_DEBUG")); private static readonly bool DebuggingSelfEnabled = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("EDGE_CS_DEBUG_SELF")); private static readonly bool CacheEnabled = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("EDGE_CS_CACHE")); private static readonly Dictionary>> FuncCache = new Dictionary>>(); private static Func _assemblyLoader; public static void SetAssemblyLoader(Func assemblyLoader) { _assemblyLoader = assemblyLoader; } public void DebugMessage(string format, params object[] args) { if (DebuggingSelfEnabled) { //Console.WriteLine(format, args); } } public Func> CompileFunc(IDictionary parameters, IDictionary compileAssemblies) { DebugMessage("EdgeCompiler::CompileFunc (CLR) - Starting"); string source = (string) parameters["source"]; string lineDirective = string.Empty; string fileName = null; int lineNumber = 1; // Read source from file if (source.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) || source.EndsWith(".csx", StringComparison.OrdinalIgnoreCase)) { // Retain filename for debugging purposes if (DebuggingEnabled) { fileName = source; } source = File.ReadAllText(source); } DebugMessage("EdgeCompiler::CompileFunc (CLR) - Func cache size: {0}", FuncCache.Count); string originalSource = source; if (FuncCache.ContainsKey(originalSource)) { DebugMessage("EdgeCompiler::CompileFunc (CLR) - Serving func from cache"); return FuncCache[originalSource]; } DebugMessage("EdgeCompiler::CompileFunc (CLR) - Func not found in cache, compiling"); // Add assembly references provided explicitly through parameters, along with some default ones List references = new List { "System.Runtime", "System.Threading.Tasks", "System.Dynamic.Runtime", "Microsoft.CSharp" }; object providedReferences; if (parameters.TryGetValue("references", out providedReferences)) { foreach (object reference in (object[]) providedReferences) { references.Add((string)reference); } } // Add assembly references provided in code as [//]#r "assembly name" lines Match match = ReferenceRegex.Match(source); while (match.Success) { references.Add(match.Groups[1].Value); source = source.Substring(0, match.Index) + source.Substring(match.Index + match.Length); match = ReferenceRegex.Match(source); } if (DebuggingEnabled) { object jsFileName; if (parameters.TryGetValue("jsFileName", out jsFileName)) { fileName = (string) jsFileName; lineNumber = (int) parameters["jsLineNumber"]; } if (!string.IsNullOrEmpty(fileName)) { lineDirective = string.Format("#line {0} \"{1}\"\n", lineNumber, fileName.Replace("\\", "\\\\")); } } // Try to compile source code as a class library Assembly assembly; string errorsClass; if (!TryCompile(lineDirective + source, references, compileAssemblies, out errorsClass, out assembly)) { // Try to compile source code as an async lambda expression // Extract using statements first string usings = ""; match = UsingRegex.Match(source); while (match.Success) { usings += match.Groups[1].Value; source = source.Substring(0, match.Index) + source.Substring(match.Index + match.Length); match = UsingRegex.Match(source); } string errorsLambda; source = usings + @" using System; using System.Threading.Tasks; public class Startup { public async Task Invoke(object ___input) { " + lineDirective + @" Func> func = " + source + @"; #line hidden return await func(___input); } }"; DebugMessage("EdgeCompiler::CompileFunc (CLR) - Trying to compile async lambda expression:{0}{1}", Environment.NewLine, source); if (!TryCompile(source, references, compileAssemblies, out errorsLambda, out assembly)) { throw new InvalidOperationException("Unable to compile C# code.\n----> Errors when compiling as a CLR library:\n" + errorsClass + "\n----> Errors when compiling as a CLR async lambda expression:\n" + errorsLambda); } } // Extract the entry point to a class method Type startupType = assembly.GetType((string) parameters["typeName"]); if (startupType == null) { throw new TypeLoadException("Could not load type '" + (string) parameters["typeName"] + "'"); } object instance = Activator.CreateInstance(startupType); MethodInfo invokeMethod = startupType.GetMethod((string) parameters["methodName"], BindingFlags.Instance | BindingFlags.Public); if (invokeMethod == null) { throw new InvalidOperationException("Unable to access CLR method to wrap through reflection. Make sure it is a public instance method."); } // Ereate a Func> delegate around the method invocation using reflection Func> result = input => (Task) invokeMethod.Invoke(instance, new object[] { input }); if (CacheEnabled) { FuncCache[originalSource] = result; } return result; } private bool TryCompile(string source, List references, IDictionary compileAssemblies, out string errors, out Assembly assembly) { assembly = null; errors = null; string projectDirectory = Environment.GetEnvironmentVariable("EDGE_APP_ROOT") ?? Directory.GetCurrentDirectory(); SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); List metadataReferences = new List(); DebugMessage("EdgeCompiler::TryCompile (CLR) - Resolving {0} references", references.Count); // Search the NuGet package repository for each reference foreach (string reference in references) { DebugMessage("EdgeCompiler::TryCompile (CLR) - Searching for {0}", reference); // If the reference looks like a filename, try to load it directly; if we fail and the reference name does not contain a path separator (like // System.Data.dll), we fall back to stripping off the extension and treating the reference like a NuGet package if (reference.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) { if (reference.Contains(Path.DirectorySeparatorChar.ToString())) { metadataReferences.Add(MetadataReference.CreateFromFile(Path.IsPathRooted(reference) ? reference : Path.Combine(projectDirectory, reference))); continue; } if (File.Exists(Path.Combine(projectDirectory, reference))) { metadataReferences.Add(MetadataReference.CreateFromFile(Path.Combine(projectDirectory, reference))); continue; } } string referenceName = reference.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ? reference.Substring(0, reference.Length - 4) : reference; if (!compileAssemblies.ContainsKey(referenceName)) { throw new Exception(String.Format("Unable to resolve reference to {0}.", referenceName)); } DebugMessage("EdgeCompiler::TryCompile (CLR) - Reference to {0} resolved to {1}", referenceName, compileAssemblies[referenceName]); metadataReferences.Add(MetadataReference.CreateFromFile(compileAssemblies[referenceName])); } CSharpCompilationOptions compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: DebuggingEnabled ? OptimizationLevel.Debug : OptimizationLevel.Release); DebugMessage("EdgeCompiler::TryCompile (CLR) - Starting compilation"); CSharpCompilation compilation = CSharpCompilation.Create(Guid.NewGuid() + ".dll", new SyntaxTree[] { syntaxTree }, metadataReferences, compilationOptions); using (MemoryStream memoryStream = new MemoryStream()) { EmitResult compilationResults = compilation.Emit(memoryStream); if (!compilationResults.Success) { IEnumerable failures = compilationResults.Diagnostics.Where( diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error || diagnostic.Severity == DiagnosticSeverity.Warning); foreach (Diagnostic diagnostic in failures) { if (errors == null) { errors = String.Format("{0}: {1}", diagnostic.Id, diagnostic.GetMessage()); } else { errors += String.Format("\n{0}: {1}", diagnostic.Id, diagnostic.GetMessage()); } } DebugMessage("EdgeCompiler::TryCompile (CLR) - Compilation failed with the following errors: {0}{1}", Environment.NewLine, errors); return false; } memoryStream.Seek(0, SeekOrigin.Begin); assembly = _assemblyLoader(memoryStream); DebugMessage("EdgeCompiler::TryCompile (CLR) - Compilation completed successfully"); return true; } } }