Compiling and Executing Code in a C# App
Since the release of Roslyn, the complete C# compiler pipeline is available as a NuGet package and we can include it in our own application. I was wondering how difficult it would be to use it to compile some C# source code into an executable and run it.
Being able to compile the following source code into a console application would be a good start:
using System;
namespace ConsoleApp1
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello world!");
Console.ReadLine();
}
}
}
.NET Framework
The basic setup is pretty straightforward. I just needed to find the correct sequence of API calls to invoke the compiler:
var syntaxTree = SyntaxFactory.ParseSyntaxTree(SourceText.From(sourceCode));
var assemblyPath = Path.ChangeExtension(Path.GetTempFileName(), "exe");
var compilation = CSharpCompilation.Create(Path.GetFileName(assemblyPath))
.WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication))
.AddSyntaxTrees(syntaxTree);
var result = compilation.Emit(assemblyPath);
if (result.Success)
{
Process.Start(assemblyPath);
}
else
{
Debug.Write(string.Join(
Environment.NewLine,
result.Diagnostics.Select(diagnostic => diagnostic.ToString())
));
}
Unfortunately, the build failed with a rather long list of diagnostic errors:
(1,7): error CS0246: The type or namespace name 'System' could not be found (are you missing a using directive or an assembly reference?)
(5,18): error CS0518: Predefined type 'System.Object' is not defined or imported
(7,26): error CS0518: Predefined type 'System.String' is not defined or imported
(7,16): error CS0518: Predefined type 'System.Void' is not defined or imported
(9,13): error CS0518: Predefined type 'System.Object' is not defined or imported
(9,13): error CS0103: The name 'Console' does not exist in the current context
(9,27): error CS0518: Predefined type 'System.String' is not defined or imported
(10,13): error CS0518: Predefined type 'System.Object' is not defined or imported
(10,13): error CS0103: The name 'Console' does not exist in the current context
(5,18): error CS1729: 'object' does not contain a constructor that takes 0 arguments
It turned out that all of them were caused by a missing reference to the mscorlib.dll
assembly. Adding the assembly containing one of the above-mentioned types was enough to fix it:
var compilation = CSharpCompilation.Create(Path.GetFileName(assemblyPath))
.WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication))
.AddReferences(
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location)
)
.AddSyntaxTrees(syntaxTree);
In .NET framework, that was all I needed to get it working. My code built the executable and ran it.
.NET Core
.NET Core was a different story. For starters, there was another missing assembly reference:
(9,13): error CS0103: The name 'Console' does not exist in the current context
(10,13): error CS0103: The name 'Console' does not exist in the current context
I used the same approach as before to add it:
var compilation = CSharpCompilation.Create(Path.GetFileName(assemblyPath))
.WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication))
.AddReferences(
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).GetTypeInfo().Assembly.Location)
)
.AddSyntaxTrees(syntaxTree);
This simply resulted in a different set of diagnostic errors:
(9,21): error CS0012: The type 'Object' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.
(9,13): error CS0012: The type 'Decimal' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.
(10,21): error CS0012: The type 'Object' is defined in an assembly that is not referenced. You must add a reference to assembly 'System.Runtime, Version=4.2.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.
I had to take a different approach for adding the System.Runtime
assembly. I assumed that it's in the same folder as the other .NET Core assemblies and hardcoded the assembly name:
var dotNetCoreDir = Path.GetDirectoryName(typeof(object).GetTypeInfo().Assembly.Location);
var compilation = CSharpCompilation.Create(Path.GetFileName(assemblyPath))
.WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication))
.AddReferences(
MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).GetTypeInfo().Assembly.Location),
MetadataReference.CreateFromFile(Path.Combine(dotNetCoreDir, "System.Runtime.dll"))
)
.AddSyntaxTrees(syntaxTree);
The code finally compiled, but the generated assembly wasn't a working Windows executable:
Unhandled Exception: System.IO.FileNotFoundException: Could not load file or assembly 'System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e' or one of its dependencies. The system cannot find the file specified.
It should be started using the dotnet
CLI command instead:
Process.Start("dotnet", assemblyPath);
That helped, but the console application still didn't start:
A fatal error was encountered. The library 'hostpolicy.dll' required to execute the application was not found in 'C:\Users\Damir\AppData\Local\Temp\'.
Failed to run as a self-contained app. If this should be a framework-dependent app, add the C:\Users\Damir\AppData\Local\Temp\tmp8FD4.runtimeconfig.json file specifying the appropriate framework.
No problem, I can generate the *.runtimeconfig.json
file. A sample can be found in the output folder of any .NET Core project:
{
"runtimeOptions": {
"tfm": "netcoreapp3.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "3.0.0-preview6-27804-01"
}
}
}
Generating a JSON file is a great reason to use the new System.Text.JSON APIs in .NET Core. I hardcoded everything except the runtime version. To hopefully keep the code working as new versions of .NET Core are released, I used the improved version API available since .NET Core Preview 4:
File.WriteAllText(
Path.ChangeExtension(assemblyPath, "runtimeconfig.json"),
GenerateRuntimeConfig()
);
private string GenerateRuntimeConfig()
{
using (var stream = new MemoryStream())
{
using (var writer = new Utf8JsonWriter(
stream,
new JsonWriterOptions() { Indented = true }
))
{
writer.WriteStartObject();
writer.WriteStartObject("runtimeOptions");
writer.WriteStartObject("framework");
writer.WriteString("name", "Microsoft.NETCore.App");
writer.WriteString(
"version",
RuntimeInformation.FrameworkDescription.Replace(".NET Core ", "")
);
writer.WriteEndObject();
writer.WriteEndObject();
writer.WriteEndObject();
}
return Encoding.UTF8.GetString(stream.ToArray());
}
}
Finally, the compiled .NET Core console application started. Although the core code is the same in .NET framework and .NET Core, the latter required a lot more plumbing to get it working.