当前位置: 首页 > 工具软件 > Roslyn > 使用案例 >

使用Roslyn将代码编译成单独的网络模块并将它们组装成动态库

龚德本
2023-12-01

目录

介绍

代码

代码说明

主程序说明

实用方法

CreateCompilationWithMscorelib(...)方法

EmitToArray(...) 方法

总结


介绍

MS Roslyn是一个很棒的工具,虽然它很新,而且没有很好的文档记录,互联网上的例子很少。我决定写这篇文章是为了填补这个文档/样本空白。

在过去的一年里,我参与了几个项目,这些项目涉及使用MS Roslyn编译器作为服务平台的动态代码生成、编译和创建动态程序集。要创建动态程序集,我会将每个单独的组件分别编译为网络模块,然后将它们组合到程序集(内存DLL)中。采用这种方法是为了避免对尚未修改的组件进行代价高昂的重新编译。

最近花了几个小时试图回答关于为什么将模块加载到动态DLL中会产生错误的github问题。关于为什么尝试加载模块会产生错误的问题,我决定在网上发布解决方案,以便其他人没有以后也要经历同样的痛苦。

代码

RoslynAssembly解决方案是使用VS 2017创建为一个简单的控制台应用程序项目。然后我向其中添加了一个NuGetMicrosoft.CodeAnalysis.CSharp。由于存在对NuGet包的依赖——它会显示它在您的Visual Studio中遗漏了一些引用,但是一旦您编译它(假设您有可用的互联网连接),应该下载并安装NuGet包以及所有引用应该填写。

该代码仅包含单个文件Program.cs 中的一个Program类。

这是示例的代码:

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,BProgramAB编译成网络模块。类Program被编译成可运行的程序集。我们加载A.netmoduleB.netmodule到主组件,然后通过运行测试static方法Program.Main(),其调用这两个类ABstatic方法。

这是A类的代码:

// code for class A
var classAString = 
    @"public class A 
        {
            public static string Print() 
            { 
                return ""Hello "";
            }
        }";  

它的static方法A.Print()打印Hello ” string

这是B类代码:

var classBString = 
    @"public class B : A
        {
            public static string Print()
            { 
                return ""World!"";
            }
        }";

它的static方法B.Print()打印stringWorld!" 请注意,为了增加趣味性,我将 B继承类 A。这将需要AB编译期间传递一个引用(如下所示)。

这是Program类的main方法:

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、将编译发送到字节数组:

byte[] compilationAResult = compilationA.EmitToArray();

该数组是模块代码的二进制表示。EmitToArray是另一个效用函数,将在下面详细讨论。

3、创建A.netmodule的引用,用于创建B.netmodule(因为类B取决于类A)以及用于创建主程序代码(因为它也取决于A)。

MetadataReference referenceA = 
    ModuleMetadata
        .CreateFromImage(compilationAResult)
        .GetReference(display: "A.netmodule");

非常重要的注意事项:每次发出编译结果时(在我们的例子中,它发生在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、将编译结果发送到字节数组中:

byte[] result = mainCompilation.EmitToArray();  

3、将程序集加载到领域中:

Assembly assembly = Assembly.Load(result));  

4、将两个模块加载到程序集中:

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

注意:在这个阶段,如果引用和模块代码的哈希值不匹配,则会抛出异常。

最后,这里是使用网络模块测试程序集功能的代码:

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

Type programType = assembly.GetType("Program");     

2、从类型为static方法Program.Main()获取MethodInfo

MethodInfo method = programType.GetMethod("Main");  

3、调用static方法Program.Main()

method.Invoke(null, null); 

这个程序的结果应该是Hello World!打印在控制台上。

实用方法

有两种简单的static实用方法:

  • CreateCompilationWithMscorelib(...)——创建roslyn  Compilation
  • EmitToArray(...)——发出一个字节数组,表示编译的.NET代码。

CreateCompilationWithMscorelib(...)方法

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

该方法采用以下参数:

  1. string assemblyOrModuleName——生成的程序集或模块的名称
  2. string code——string包含要编译的代码
  3. CSharpCompilationOptions compilerOptions——应为模块包含new CSharpCompilationOptions(OutputKind.NetModule)或为应用程序包含new CSharpCompilationOptions(OutputKind.ConsoleApplication)
  4. IEnumerable<MetadataReference> references——在引用mscore库之后添加的额外引用

首先,它将代码解析成语法树(Roslyn语法树会转换反映C#语法的string代码对象,为编译做准备):

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类型的编译” 

首先,我们创建MemoryStream以容纳字节数组。然后,我们将编译结果发送到stream

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

最后(如果没有错误),我们从stream返回字节数组:

return stream.ToArray();  

总结

Roslyn是一个非常强大且未充分利用的框架,由于缺乏文档和示例,大多数公司并未完全实现其全部功能。

在本文中,我将解释如何使用Roslyn在运行时将动态生成的代码编译和组装成可执行的动态程序集。

我尝试解释编译和组装的每个阶段,并详细提到可能的陷阱,这样本文的读者就不必像我一样花太多时间试图使事情正常工作。

https://www.codeproject.com/Articles/1215168/Using-Roslyn-for-Compiling-Code-into-Separate-Net、

 类似资料: