Is CompileAssemblyFromDom faster than CompileAssemblyFromSource?
It should be as it presumably bypasses the compiler front-end.
CompileAssemblyFromDom compiles to a .cs file which is then run through the normal C# compiler.
Example:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CSharp;
using System.CodeDom;
using System.IO;
using System.CodeDom.Compiler;
using System.Reflection;
namespace CodeDomQuestion
{
class Program
{
private static void Main(string[] args)
{
Program p = new Program();
p.dotest("C:\\fs.exe");
}
public void dotest(string outputname)
{
CSharpCodeProvider cscProvider = new CSharpCodeProvider();
CompilerParameters cp = new CompilerParameters();
cp.MainClass = null;
cp.GenerateExecutable = true;
cp.OutputAssembly = outputname;
CodeNamespace ns = new CodeNamespace("StackOverflowd");
CodeTypeDeclaration type = new CodeTypeDeclaration();
type.IsClass = true;
type.Name = "MainClass";
type.TypeAttributes = TypeAttributes.Public;
ns.Types.Add(type);
CodeMemberMethod cmm = new CodeMemberMethod();
cmm.Attributes = MemberAttributes.Static;
cmm.Name = "Main";
cmm.Statements.Add(new CodeSnippetExpression("System.Console.WriteLine('f'zxcvv)"));
type.Members.Add(cmm);
CodeCompileUnit ccu = new CodeCompileUnit();
ccu.Namespaces.Add(ns);
CompilerResults results = cscProvider.CompileAssemblyFromDom(cp, ccu);
foreach (CompilerError err in results.Errors)
Console.WriteLine(err.ErrorText + " - " + err.FileName + ":" + err.Line);
Console.WriteLine();
}
}
}
which shows errors in a (now nonexistent) temp file:
) expected - c:\Documents and Settings\jacob\Local Settings\Temp\x59n9yb-.0.cs:17
; expected - c:\Documents and Settings\jacob\Local Settings\Temp\x59n9yb-.0.cs:17
Invalid expression term ')' - c:\Documents and Settings\jacob\Local Settings\Tem p\x59n9yb-.0.cs:17
So I guess the answer is "no"
I've tried finding the ultimate compiler call earlier and I gave up. There's quite a number of layers of interfaces and virtual classes for my patience.
I don't think the source reader part of the compiler ends up with a DOM tree, but intuitively I would agree with you. The work necessary to transform the DOM to IL should be much less than reading C# source code.
Related
I am trying to use JSON in a dynamically compiled script. However, when I include the library and add a JObject, I get errors like: "The type 'System.Dynamic.IDynamicMetaObjectProvider' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'."
I included MathNet.Numerics just fine.
Here is my setup.
Console application .NET Framework 4 (so it matches the runtime
compile)
Nuget install MathNet.Numerics
Nuget install Newtonsoft
Point the runtime compile to the debug folder with the dlls.
Test Code
using System;
using System.IO;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using Microsoft.CSharp;
using System.Reflection;
using System.CodeDom.Compiler;
namespace ConsoleApp1
{
public class RunScript0
{
public bool Compile()
{
//Add using statements here
string source = "namespace UserScript\r\n{\r\nusing System;\r\n" +
"using System.IO;\r\n" +
"using System.Collections.Generic;\r\n" +
"using MathNet.Numerics.Distributions;\r\n" +
"using Newtonsoft.Json.Linq;\r\n" +
"using Newtonsoft.Json;\r\n" +
"public class RunScript\r\n{\r\n" +
"private const int x = 99;\r\n" +
"public int Eval()\r\n{\r\n" +
//Remove following line and all works fine
"JObject j = new JObject();\r\n" +
"return x; \r\n\r\n}\r\n}\r\n}";
Dictionary<string, string> provOptions =
new Dictionary<string, string>();
provOptions.Add("CompilerVersion", "v4.0");
CodeDomProvider compiler = new CSharpCodeProvider(provOptions);
CompilerParameters parameters = new CompilerParameters();
string appPath = System.IO.Directory.GetCurrentDirectory();
parameters.ReferencedAssemblies.Add(appPath + "\\MathNet.Numerics.dll");
parameters.ReferencedAssemblies.Add(appPath + "\\Newtonsoft.Json.dll");
//Tried adding but didn't help parameters.ReferencedAssemblies.Add(appPath + "\\System.Core.dll");
parameters.GenerateInMemory = true;
var results = compiler.CompileAssemblyFromSource(parameters, source);
// Check for compile errors / warnings
if (results.Errors.HasErrors || results.Errors.HasWarnings)
{
Console.WriteLine(results.Errors.Count.ToString() + " Erorrs");
for (int i = 0; i < results.Errors.Count; i++)
Console.WriteLine(results.Errors[i].ToString());
return false;
}
else
{
Console.WriteLine("Compiled Successfully");
return true;
}
}
}
class Program
{
static void Main(string[] args)
{
RunScript0 A = new RunScript0();
A.Compile();
}
}
}
You need add a reference to System and System.Core from the GAC (Global Assembly Cache) :
parameters.ReferencedAssemblies.Add("System.dll");
parameters.ReferencedAssemblies.Add("System.Core.dll");
I think it's dependances from Newtonsoft.Json.
We have an issue where simple c# code executed via CSharpCodeProvider doesn't work the same on running local command line and docker.
Code example is below, and it will not return any Types of the assembly when run on Roslyn, but works fine locally.
I have literally no idea how to debug this from here - any help welcome!
using System;
using Microsoft.CodeDom.Providers.DotNetCompilerPlatform;
using System.IO;
using System.Reflection;
using System.Text;
using System.CodeDom.Compiler;
namespace TestContainerIssue
{
class Program
{
static void Main(string[] args)
{
using (var codeProvider = new CSharpCodeProvider())
{
var compilerParameters = new CompilerParameters
{
GenerateExecutable = false,
GenerateInMemory = true
};
compilerParameters.CompilerOptions = String.Format("/lib:\"{0}\"", Path.GetDirectoryName(Uri.UnescapeDataString((new UriBuilder(Assembly.GetExecutingAssembly().CodeBase)).Path)));
Console.WriteLine(compilerParameters.CompilerOptions);
compilerParameters.ReferencedAssemblies.Add("System.dll");
compilerParameters.ReferencedAssemblies.Add("System.Core.dll");
CompilerResults compilerResults = codeProvider.CompileAssemblyFromSource(compilerParameters, ExecutionWrapperCode);
if (compilerResults.Errors.HasErrors)
{
StringBuilder errors = new StringBuilder();
foreach (CompilerError error in compilerResults.Errors)
errors.AppendLine(error.ErrorText);
throw new Exception(errors.ToString());
}
Console.WriteLine(compilerResults.PathToAssembly);
Assembly assembly = compilerResults.CompiledAssembly;
Console.WriteLine(assembly.FullName);
Console.WriteLine("Types:");
foreach (Type t in assembly.GetTypes())
{
Console.WriteLine(t);
}
Type type = assembly.GetType("Validation.Execution");
Console.WriteLine("Type:");
Console.WriteLine(type); //Empty when run in Docker (both mcr.microsoft.com/windows:1909 and mcr.microsoft.com/windows/servercore:ltsc2019
var methodInfo = type.GetMethod("Execute");
Console.WriteLine(methodInfo);
}
}
private const string ExecutionWrapperCode = #"
using System;
namespace Validation
{
public static class Execution
{
public static string Execute()
{
return ""test"";
}
}
}";
}
}
I tried below docker file (I tried two windows image: mcr.microsoft.com/windows:1909 and mcr.microsoft.com/windows/servercore:ltsc2019
FROM mcr.microsoft.com/windows/servercore:ltsc2019
ADD /bin/Debug /
ENTRYPOINT TestContainerIssue.exe
EDIT: I built the two dlls, and compared them in dotPeek - as you can see the one in Docker is missing the namespace. They are exactly the same bytes length though.
It turns out this line of code:
compilerParameters.CompilerOptions = String.Format("/lib:\"{0}\"", Path.GetDirectoryName(Uri.UnescapeDataString((new UriBuilder(Assembly.GetExecutingAssembly().CodeBase)).Path)));
was causing the problem. I don't know why it causes it and doesn't throw an error in compiler, but perhaps will help someone else.
Was
I'm trying to compile code at runtime in C#, then from the compiled code call a function or initialize a class which is defined in the original code.
The code I currently have:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
namespace CTFGame
{
class Program
{
static void Main(string[] args)
{
string code = #"
using System;
namespace CTFGame
{
public class MyPlayer
{
public static void Main ()
{
Console.WriteLine(""Hello world"");
}
/*public void DoTurn ()
{
Program.SayHello();
}*/
}
}
";
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters parameters = new CompilerParameters();
parameters.GenerateInMemory = true;
CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
if (results.Errors.HasErrors)
{
string errors = "";
foreach (CompilerError error in results.Errors)
{
errors += string.Format("Error #{0}: {1}\n", error.ErrorNumber, error.ErrorText);
}
Console.Write(errors);
}
else
{
Assembly assembly = results.CompiledAssembly;
Type program = assembly.GetType("CTFGame.MyPlayer");
MethodInfo main = program.GetMethod("Main");
main.Invoke(null, null);
}
}
public static void SayHello()
{
Console.WriteLine("I'm awesome ><");
}
}
}
Now, Running the runtime loaded method 'Main' is a success, and the message "Hello world" is printed. The problem starts here: in the original code I have a method called "SayHello". I want to call this method from my runtime loaded code.
If I uncomment the "DoTurn" method, a compiler error will show in runtime:
Error #CS0103: The name 'Program' does not exist in the current context
My question is - is this possible, and how?
Putting the runtime loaded code in the same namespace doesn't help (and that makes sense), so what is the correct way to do that?
Thanks.
Adding a reference to the current assembly solved the problem:
CSharpCodeProvider provider = new CSharpCodeProvider();
CompilerParameters parameters = new CompilerParameters();
parameters.GenerateInMemory = true;
//The next line is the addition to the original code
parameters.ReferencedAssemblies.Add(Assembly.GetEntryAssembly().Location);
More about:
Compiling c# at runtime with user defined functions
I have a string. For example
string str="if(a>b) {return a;} else {return b;}"
I want to evaluate or make function, say func(int a, int b) which will have the code of 'str'.
you may need to use CSharpCodeProvider as in this answer
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
class Program
{
static void Main(string[] args)
{
var csc = new CSharpCodeProvider(new Dictionary<string, string>() { { "CompilerVersion", "v3.5" } });
var parameters = new CompilerParameters(new[] { "mscorlib.dll", "System.Core.dll" }, "foo.exe", true);
parameters.GenerateExecutable = true;
CompilerResults results = csc.CompileAssemblyFromSource(parameters,
#"using System.Linq;
class Program {
public static void Main(string[] args) {
var q = from i in Enumerable.Range(1,100)
where i % 2 == 0
select i;
}
}");
results.Errors.Cast<CompilerError>().ToList().ForEach(error => Console.WriteLine(error.ErrorText));
}
}
In general, this is not an easy thing to do, but the System.CodeDom namespace is where your journey will start.
Look at the following CodeProject article on the matter as a start: http://www.codeproject.com/Articles/26312/Dynamic-Code-Integration-with-CodeDom
The basics of it is as follows (as taken from the codeproject article):
private static Assembly CompileSource( string sourceCode )
{
CodeDomProvider cpd = new CSharpCodeProvider();
CompilerParameters cp = new CompilerParameters();
cp.ReferencedAssemblies.Add("System.dll");
//cp.ReferencedAssemblies.Add("ClassLibrary1.dll");
cp.GenerateExecutable = false;
// Invoke compilation.
CompilerResults cr = cpd.CompileAssemblyFromSource(cp, sourceCode);
return cr.CompiledAssembly;
}
The resultant assembly will have the class/method/code you are interested in, and then you can use reflection to call your method. Since your example just uses a code fragment, you will probably have to wrap it in a class/method before passing it to this method.
I hope that helps, but dynamic code generation in C# is not easy and this is just a start.
I have this console type thing that accepts a line of C# code, wraps it up in some surrounding code and compiles it into an assembly. Then, I invoke the method from that assembly, output the result and that's it.
The problem is, the assembly needs to have a name so I can set it as a friend assembly so it could access the non-public classes. I named it "console".
Everything worked as expected but the problem is that I can't run a second script after one has finished because the file named "console" already exists in the directory and can't be overwritten.
I've tried Disposing of everything that has a Dispose method. I've tried manually deleting the file with File.Delete. Nothing helped.
So here's the code I'm using. I hope someone can help me.
CSharpCodeProvider provider = new CSharpCodeProvider();
var param = new CompilerParameters();
param.GenerateInMemory = false;
param.GenerateExecutable = false;
param.OutputAssembly = "console";
param.ReferencedAssemblies.Add("System.dll");
param.ReferencedAssemblies.Add("System.Core.dll");
param.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location);
var results = provider.CompileAssemblyFromSource(param,
#"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace FireflyGL
{
class DebugConsole
{
static void Run(StringWriter writer)
{
var stdOut = Console.Out;
Console.SetOut(writer);
" + input.Text + #"
Console.SetOut(stdOut);
}
}
}");
if (results.Errors.HasErrors)
{
for (int i = 0; i < results.Errors.Count; ++i)
{
PushText(results.Errors[i].ErrorText);
}
}
else
{
try
{
var writter = new StringWriter();
results.CompiledAssembly.GetType("FireflyGL.DebugConsole").GetMethod("Run", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { writter });
PushText(writter.ToString());
history.Add(input.Text);
currentHistory = history.Count;
input.Text = "";
writter.Dispose();
}
catch (Exception)
{
}
}
provider.Dispose();
File.Delete(results.CompiledAssembly.Location);
You have to unload the assembly to get rid of all the references. Unfortunately, you can't do that. You can however unload an AppDomain and provided you reference your assembly in that AppDomain, it will be unloaded as well.
If you don't care about creating a memory leak, you could also just create unique names for your assembly (console1, console2 etc.)...