65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (31投票s)

2017 年 11 月 13 日

CPOL

6分钟阅读

viewsIcon

39218

downloadIcon

352

动态编译和组装代码片段到动态程序集中。

引言

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();
        }
    }
}

代码描述

主程序解析

代码演示了如何编译和组装三个类:ABProgramAB 被编译成 .NET 模块。Program 类被编译成可运行的程序集。我们将 A.netmoduleB.netmodule 加载到主程序集中,然后通过运行一个调用了 AB 类中静态方法的静态方法 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 及其引用的方式。

  1. A.netmodule 创建 Roslyn Compilation 对象

    var compilationA = 
        CreateCompilationWithMscorlib
        (
            "A", 
            classAString, 
            compilerOptions: new CSharpCompilationOptions(OutputKind.NetModule)
        );      
    方法 CreateCompilationWithMscorlib 是一个工具方法,用于创建编译对象,下文将进行讨论。
  2. 将编译结果输出(Emit)到一个字节数组中

    byte[] compilationAResult = compilationA.EmitToArray();
    这个数组是模块代码的二进制表示。EmitToArray 是另一个工具函数,下面将详细讨论。
  3. 创建一个对 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 类)。

以下是我们如何创建主程序集

  1. 为主程序集创建 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. 
  2. 将编译结果输出(Emit)到一个字节数组中

    byte[] result = mainCompilation.EmitToArray();  
  3. 将程序集加载到当前域中

    Assembly assembly = Assembly.Load(result));  
  4. 将两个模块加载到程序集中

    assembly.LoadModule("A.netmodule", compilationAResult);
    assembly.LoadModule("B.netmodule", compilationBResult);  

    注意:如果引用和模块代码的哈希值不匹配,程序将在此阶段抛出异常。

最后,这是测试带有 .NET 模块的程序集功能的代码

  1. 从程序集中获取 C# 类型 Program

    Type programType = assembly.GetType("Program");      
  2. 从类型中获取静态方法 Program.Main()MethodInfo

    MethodInfo method = programType.GetMethod("Main");  
  3. 调用静态方法 Program.Main()

    method.Invoke(null, null); 

该程序的输出结果应该是在控制台上打印出 “Hello World!”。

工具方法

有两个简单的静态工具方法

  • CreateCompilationWithMscorelib(...) - 创建 Roslyn Compilation 对象
  • EmitToArray(...) - 输出(Emit)一个表示编译的 .NET 代码的字节数组。

CreateCompilationWithMscorelib(...) 方法

该方法的目的是创建一个 Roslyn Compilation 对象,并向其中添加对包含基本 .NET 功能的 mscore 库的引用。除此之外,它还可以添加作为其最后一个参数 'references' 传入的模块引用(如果需要的话)。

该方法接受以下参数

  1. string assemblyOrModuleName - 生成的程序集或模块的名称
  2. string code - 包含要编译的代码的字符串
  3. CSharpCompilationOptions compilerOptions - 对于模块,应包含 new CSharpCompilationOptions(OutputKind.NetModule);对于应用程序,应包含 new CSharpCompilationOptions(OutputKind.ConsoleApplication)
  4. 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 链接
© . All rights reserved.