使用Roslyn将代码编译成独立的Net模块并将其组装成动态库





5.00/5 (31投票s)
动态编译和组装代码片段到动态程序集中。
引言
MS Roslyn 是一个很棒的工具,尽管它还很新,文档也不完善,网上可用的示例也很少。为了填补这个文档/示例的空白,我决定写这篇文章。
在过去的一年里,我参与了几个涉及动态代码生成、编译和使用 MS Roslyn 编译器即服务平台创建动态程序集的项目。为了创建一个动态程序集,我会将每个独立组件分别编译成 .NET 模块,然后将它们组合到程序集中(内存中的 DLL)。采用这种方法是为了避免对未修改的组件进行代价高昂的重新编译。
最近,我花了好几个小时试图回答一个GitHub 上的问题:为什么将模块加载到动态 DLL 中会产生错误。我决定将解决方案发布在 CodeProject 上,以便其他人将来不必经历同样的痛苦。
代码
RoslynAssembly
解决方案是使用 VS 2017 创建的一个简单的“控制台应用”项目。然后,我向其中添加了一个 NuGet 包 Microsoft.CodeAnalysis.CSharp
。由于存在对 NuGet 包的依赖,它会在你的 Visual Studio 中显示缺少一些引用,但一旦你编译它(前提是你已连接到互联网),NuGet 包就会被下载和安装,所有的引用都将被补全。
代码仅包含一个 Program
类,位于单个文件 Program.cs 中。
以下是示例代码
public static class Program
{
public static void Main()
{
try
{
// code for class A
var classAString =
@"public class A
{
public static string Print()
{
return ""Hello "";
}
}";
// code for class B (to spice it up, it is a
// subclass of A even though it is almost not needed
// for the demonstration)
var classBString =
@"public class B : A
{
public static string Print()
{
return ""World!"";
}
}";
// the main class Program contain static void Main()
// that calls A.Print() and B.Print() methods
var mainProgramString =
@"public class Program
{
public static void Main()
{
System.Console.Write(A.Print());
System.Console.WriteLine(B.Print());
}
}";
#region class A compilation into A.netmodule
// create Roslyn compilation for class A
var compilationA =
CreateCompilationWithMscorlib
(
"A",
classAString,
compilerOptions: new CSharpCompilationOptions(OutputKind.NetModule)
);
// emit the compilation result to a byte array
// corresponding to A.netmodule byte code
byte[] compilationAResult = compilationA.EmitToArray();
// create a reference to A.netmodule
MetadataReference referenceA =
ModuleMetadata
.CreateFromImage(compilationAResult)
.GetReference(display: "A.netmodule");
#endregion class A compilation into A.netmodule
#region class B compilation into B.netmodule
// create Roslyn compilation for class A
var compilationB =
CreateCompilationWithMscorlib
(
"B",
classBString,
compilerOptions: new CSharpCompilationOptions(OutputKind.NetModule),
// since class B extends A, we need to
// add a reference to A.netmodule
references: new[] { referenceA }
);
// emit the compilation result to a byte array
// corresponding to B.netmodule byte code
byte[] compilationBResult = compilationB.EmitToArray();
// create a reference to B.netmodule
MetadataReference referenceB =
ModuleMetadata
.CreateFromImage(compilationBResult)
.GetReference(display: "B.netmodule");
#endregion class B compilation into B.netmodule
#region main program compilation into the assembly
// create the Roslyn compilation for the main program with
// ConsoleApplication compilation options
// adding references to A.netmodule and B.netmodule
var mainCompilation =
CreateCompilationWithMscorlib
(
"program",
mainProgramString,
compilerOptions: new CSharpCompilationOptions
(OutputKind.ConsoleApplication),
references: new[] { referenceA, referenceB }
);
// Emit the byte result of the compilation
byte[] result = mainCompilation.EmitToArray();
// Load the resulting assembly into the domain.
Assembly assembly = Assembly.Load(result);
#endregion main program compilation into the assembly
// load the A.netmodule and B.netmodule into the assembly.
assembly.LoadModule("A.netmodule", compilationAResult);
assembly.LoadModule("B.netmodule", compilationBResult);
#region Test the program
// here we get the Program type and
// call its static method Main()
// to test the program.
// It should write "Hello world!"
// to the console
// get the type Program from the assembly
Type programType = assembly.GetType("Program");
// Get the static Main() method info from the type
MethodInfo method = programType.GetMethod("Main");
// invoke Program.Main() static method
method.Invoke(null, null);
#endregion Test the program
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
// a utility method that creates Roslyn compilation
// for the passed code.
// The compilation references the collection of
// passed "references" arguments plus
// the mscore library (which is required for the basic
// functionality).
private static CSharpCompilation CreateCompilationWithMscorlib
(
string assemblyOrModuleName,
string code,
CSharpCompilationOptions compilerOptions = null,
IEnumerable<MetadataReference> references = null)
{
// create the syntax tree
SyntaxTree syntaxTree = SyntaxFactory.ParseSyntaxTree(code, null, "");
// get the reference to mscore library
MetadataReference mscoreLibReference =
AssemblyMetadata
.CreateFromFile(typeof(string).Assembly.Location)
.GetReference();
// create the allReferences collection consisting of
// mscore reference and all the references passed to the method
IEnumerable<MetadataReference> allReferences =
new MetadataReference[] { mscoreLibReference };
if (references != null)
{
allReferences = allReferences.Concat(references);
}
// create and return the compilation
CSharpCompilation compilation = CSharpCompilation.Create
(
assemblyOrModuleName,
new[] { syntaxTree },
options: compilerOptions,
references: allReferences
);
return compilation;
}
// emit the compilation result into a byte array.
// throw an exception with corresponding message
// if there are errors
private static byte[] EmitToArray
(
this Compilation compilation
)
{
using (var stream = new MemoryStream())
{
// emit result into a stream
var emitResult = compilation.Emit(stream);
if (!emitResult.Success)
{
// if not successful, throw an exception
Diagnostic firstError =
emitResult
.Diagnostics
.FirstOrDefault
(
diagnostic =>
diagnostic.Severity == DiagnosticSeverity.Error
);
throw new Exception(firstError?.GetMessage());
}
// get the byte array from a stream
return stream.ToArray();
}
}
}
代码描述
主程序解析
代码演示了如何编译和组装三个类:A
、B
和 Program
。A
和 B
被编译成 .NET 模块。Program
类被编译成可运行的程序集。我们将 A.netmodule
和 B.netmodule
加载到主程序集中,然后通过运行一个调用了 A
和 B
类中静态
方法的静态
方法 Program.Main()
来进行测试。
这是 A
类的代码
// code for class A
var classAString =
@"public class A
{
public static string Print()
{
return ""Hello "";
}
}";
它的静态
方法 A.Print()
打印字符串 “Hello
”。
这是 B
类的代码
var classBString =
@"public class B : A
{
public static string Print()
{
return ""World!"";
}
}";
它的静态
方法 B.Print()
打印字符串 “World!
”。注意,为了增加点趣味,我让 B
类继承了 A
类。这将需要在编译 B
时传递对 A
的引用(如下所示)。
这是主 Program
类的代码
var mainProgramString =
@"public class Program
{
public static void Main()
{
System.Console.Write(A.Print());
System.Console.WriteLine(B.Print());
}
}";
以下是我们如何创建 A.netmodule
及其引用的方式。
-
为
A.netmodule
创建 RoslynCompilation
对象var compilationA = CreateCompilationWithMscorlib ( "A", classAString, compilerOptions: new CSharpCompilationOptions(OutputKind.NetModule) );
方法CreateCompilationWithMscorlib
是一个工具方法,用于创建编译对象,下文将进行讨论。 -
将编译结果输出(Emit)到一个字节数组中
byte[] compilationAResult = compilationA.EmitToArray();
这个数组是模块代码的二进制表示。EmitToArray
是另一个工具函数,下面将详细讨论。 -
创建一个对
A.netmodule
的引用,用于创建B.netmodule
(因为B
类依赖于A
类),也用于创建主程序代码(因为它也依赖于A
)。MetadataReference referenceA = ModuleMetadata .CreateFromImage(compilationAResult) .GetReference(display: "A.netmodule");
非常重要的提示:每次输出(Emit)编译结果时(在我们的例子中,发生在 EmitToArray()
方法内部),生成的字节码都会略有变化,可能是因为时间戳。因此,确保引用和模块代码来自同一次 Emit 的结果非常重要。否则,如果你为模块代码和引用使用了不同的 Emit(...)
调用,尝试将模块加载到程序集时会导致哈希不匹配的异常,因为用于构建程序集的引用的哈希值将与模块代码的哈希值不同。这就是我花了好几个小时才弄明白的问题,也是我写这篇文章的主要原因。
创建 B.netmodule
及其引用的过程与 A.netmodule
几乎相同,唯一的区别是我们需要将对 A.netmodule
的引用传递给 CreateCompilationWithMscorlib(...)
方法(因为 B
类依赖于 A
类)。
以下是我们如何创建主程序集
-
为主程序集创建 Roslyn
Compilation
对象var mainCompilation = CreateCompilationWithMscorlib ( "program", mainProgramString, // note that here we pass the OutputKind set to ConsoleApplication compilerOptions: new CSharpCompilationOptions(OutputKind.ConsoleApplication), references: new[] { referenceA, referenceB } ); note that we pass <code>OutputKind.ConsoleApplication</code> option since it is an assembly and not a net module.
-
将编译结果输出(Emit)到一个字节数组中
byte[] result = mainCompilation.EmitToArray();
-
将程序集加载到当前域中
Assembly assembly = Assembly.Load(result));
-
将两个模块加载到程序集中
assembly.LoadModule("A.netmodule", compilationAResult); assembly.LoadModule("B.netmodule", compilationBResult);
注意:如果引用和模块代码的哈希值不匹配,程序将在此阶段抛出异常。
最后,这是测试带有 .NET 模块的程序集功能的代码
-
从程序集中获取 C# 类型
Program
Type programType = assembly.GetType("Program");
-
从类型中获取
静态
方法Program.Main()
的MethodInfo
MethodInfo method = programType.GetMethod("Main");
-
调用
静态
方法Program.Main()
method.Invoke(null, null);
该程序的输出结果应该是在控制台上打印出 “Hello World!
”。
工具方法
有两个简单的静态
工具方法
CreateCompilationWithMscorelib(...)
- 创建 RoslynCompilation
对象EmitToArray(...)
- 输出(Emit)一个表示编译的 .NET 代码的字节数组。
CreateCompilationWithMscorelib(...) 方法
该方法的目的是创建一个 Roslyn Compilation
对象,并向其中添加对包含基本 .NET 功能的 mscore
库的引用。除此之外,它还可以添加作为其最后一个参数 'references' 传入的模块引用(如果需要的话)。
该方法接受以下参数
string assemblyOrModuleName
- 生成的程序集或模块的名称string code
- 包含要编译的代码的字符串
CSharpCompilationOptions compilerOptions
- 对于模块,应包含new CSharpCompilationOptions(OutputKind.NetModule)
;对于应用程序,应包含new CSharpCompilationOptions(OutputKind.ConsoleApplication)
IEnumerable<MetadataReference> references
- 在引用mscore
库之后要添加的额外引用
首先,它将代码解析为语法树(Roslyn 语法树将字符串
代码转换为反映 C# 语法的对象,为编译做准备)
SyntaxTree syntaxTree = SyntaxFactory.ParseSyntaxTree(code, null, "");
我们通过连接对 mscore
库的引用和传递给该方法的引用来构建 allReferences
集合
// get the reference to mscore library
MetadataReference mscoreLibReference =
AssemblyMetadata
.CreateFromFile(typeof(string).Assembly.Location)
.GetReference();
// create the allReferences collection consisting of
// mscore reference and all the references passed to the method
IEnumerable allReferences =
new MetadataReference[] { mscoreLibReference };
if (references != null)
{
allReferences = allReferences.Concat(references);
}
最后,我们通过 CSharpCompilation.Create(...)
方法构建并返回 Roslyn Compilation
对象
// create and return the compilation
CSharpCompilation compilation = CSharpCompilation.Create
(
assemblyOrModuleName,
new[] { syntaxTree },
options: compilerOptions,
references: allReferences
);
return compilation;
EmitToArray(...) 方法
EmitToArray(...)
方法的目的是输出字节码(真正的编译在此阶段发生),检查错误(如果输出不成功则抛出异常),并返回 .NET 代码的字节数组。
它只接受一个参数——Roslyn Compilation
类型的 "compilation"。
首先,我们创建 MemoryStream
来容纳字节数组。然后,我们将编译结果输出到该流
中
using (var stream = new MemoryStream())
{
// emit result into a stream
var emitResult = compilation.Emit(stream);
接着,我们检查编译结果是否有错误,如果发现错误,则抛出一个包含第一条错误消息的异常
if (!emitResult.Success)
{
// if not successful, throw an exception
Diagnostic firstError =
emitResult
.Diagnostics
.FirstOrDefault
(
diagnostic =>
diagnostic.Severity == DiagnosticSeverity.Error
);
throw new Exception(firstError?.GetMessage());
}
最后(如果没有错误),我们从流
中返回字节数组
return stream.ToArray();
摘要
Roslyn 是一个非常强大但未被充分利用的框架,由于缺乏文档和示例,大多数公司尚未完全认识到其全部威力。
在本文中,我将解释如何使用 Roslyn 在运行时将动态生成的代码编译并组装成一个可执行的动态程序集。
我尝试详细解释编译和组装的每个阶段,并提及可能的陷阱,以便本文的读者不必像我一样花费那么多时间来让它工作起来 :)。
历史
- 2017年11月13日:添加了 GITHUB 链接