Roslyn scripting on CoreCLR (.NET CLI and DNX) and in memory assemblies

· 831 words · 4 minutes to read

For a while now, the Roslyn C# scripting APIs (Microsoft.CodeAnalysis.CSharp.Scripting) have been portable, and supported cross platform usage.

However, I recently ran into a few difficulties regarding using the Roslyn Scripting APIs in .NET CLI (which is replacing DNX) context. The solution was to use a lower level unmanaged CoreCLR API - and since they it’s not that well documented, I thought it would be beneficial to document it in a blog post.

The Problem 🔗

The scripting APIs generally work just fine when you try to use them against .NET Core targets.

The problem arises when you are trying to pass in a reference to the current application assembly to your script. This can be needed for a number of reasons - for example you may want to use the results of the scripting operations in your main program or simply use the types from your main program inside the scripting code.

Roslyn provides you two ways of doing this if you have an instance of an Assembly object:

  • using the regular MetadataReference.CreateFromAssembly API - unfortunately that will only work on assemblies that have a physical, accessible location (the Location property of an assembly object is not null or empty)
  • for in memory assemblies you can use the InteractiveAssemblyLoader APIs, but that only supports dekstop at the moment (not CoreCLR)

The example below shows the approach with InteractiveAssemblyLoader in play (suitable for desktop usage, i.e. DNX against Desktop CLR):

using (var interactiveLoader = new InteractiveAssemblyLoader())  
{  
interactiveLoader.RegisterDependency(assembly); //assembly exists in memory only  
var script = CSharpScript.Create("var foo = new Foo();", opts, interactiveLoader); // Foo exists in the referenced in memory assembly  
var result = await script.RunAsync(); //runs fine  
}  

Now, a DNX program, or a .NET CLI program, running against CoreCLR, will potentially exist only in memory with no physical Assembly.Location - making neither of the above approaches usable.

DNX also offers an ILibraryExporter platform service, which can be used for grabbing MetadataReferences. There is however a more general solution, that can be generally applied to obtain MetadataReference from any Assembly instance - no matter who emitted it, and whether it exists on disk or just in memory.

The Solution 🔗

Last summer CoreCLR introduced a new API, surfaced via System.Runtime.Loader package, allowing us to grab the metadata section (the format is defined here in ECMA-335) of an assembly via an instance of that assembly. It is just the metadata, not the full PE image, but Roslyn doesn’t need full PE image to provide a reference to the scripting APIs.

The example for .NET CLI is shown below. Since the CoreCLR API uses unmanaged code, it’s necessary to use unsafe modifier from the C# code (unsafe compilation needs to be enabled in your project.json file too).

namespace ConsoleApplication  
{  
public class Foo  
{  
public int Bar {get; set;}  
}

public class Program //whole Program exists in memory only  
{  
public unsafe static void Main(string[] args)  
{  
byte* b;  
int length;  
var assembly = typeof (Foo).GetTypeInfo().Assembly; //let's grab the current in-memory assembly  
if (assembly.TryGetRawMetadata(out b, out length))  
{  
var moduleMetadata = ModuleMetadata.CreateFromMetadata((IntPtr) b, length);  
var assemblyMetadata = AssemblyMetadata.Create(moduleMetadata);  
var reference = assemblyMetadata.GetReference();

var opts = ScriptOptions.Default.AddImports("ConsoleApplication").AddReferences(reference);  
var script = CSharpScript.Create("var foo = new Foo();", opts);  
var result = script.RunAsync().Result; //runs fine, possible to use main application types in the script  
}

Console.WriteLine("Hello World!");  
}  
}  
}  

So we grab the current assembly, then use the new CoreCLR TryGetRawMetadata method to get the metadata from it, and then we build up a MetadataReference object which Roslyn can use in its scripting APIs. This is very powerful as it opens up a lot of interesting avenues in .NET CLI scripting - allowing your script to interact with the host application, even if the host application exists only in memory or consume in memory assemblies which can be dynamically emitted by anyone, at any point!

You can even use the object from the .NET CLI in memory application as a host (global) scripting type and exchange the information between the script and the main app this way. Here is the above code modified to use a host object from the parent in memory application:

var foo = new Foo();  
var opts = ScriptOptions.Default.AddImports("ConsoleApplication").AddReferences(reference);  
var script = CSharpScript.Create("Bar = 1", opts, typeof(Foo));  
var result = script.RunAsync(foo).Result;

Console.WriteLine(foo.Bar); //prints 1  

For the record, here is the project.json used against the sample code used here:

{  
“version”: “1.0.0-*”,  
“compilationOptions”:  
{  
“emitEntryPoint”: true,  
“allowUnsafe”: true  
},  
dependencies": {  
"NETStandard.Library": "1.0.0-rc2-23704",  
"Microsoft.CodeAnalysis.CSharp.Scripting": "1.1.1",  
"System.Runtime.Loader": "4.0.0-rc2-23819",  
"System.Dynamic.Runtime": "4.0.11-beta-23516",  
"System.Threading.Tasks.Parallel": "4.0.1-beta-23516",  
"System.Xml.XDocument": "4.0.11-beta-23516"  
},  
"frameworks": {  
"dnxcore50": {  
"imports": "portable-net451+win8"  
}  
}  
}  

UWP Limitations 🔗

While it’s not really related to the rest of this post, it is worth noting that Roslyn scripting APIs will not work in UWP (Universal Windows Platform) applications. That’s just the limitation of the platform itself - UWP apps use .NET Native, which is ahead of time compilation. Since there is no JIT at runtime (no runtime code generation), there is no way to support C# scripting there.

About


Hi! I'm Filip W., a cloud architect from Zürich 🇨🇭. I like Toronto Maple Leafs 🇨🇦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁢󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP