3

【.net 深呼吸】细说CodeDom(9):动态编译

 3 years ago
source link: https://www.cnblogs.com/tcjiaan/p/6277618.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【.net 深呼吸】细说CodeDom(9):动态编译

知道了如果构建代码文档,知道了如何生成代码,那么编译程序集就很简单了。

CodeDomProvider 类提供了三个可以执行编译的方法:

1、CompileAssemblyFromSource——这个好懂,也好办,就是用字符串直接构建代码,然后传给这个方法,就可以把源代码编译了。

2、CompileAssemblyFromFile——这个是把一个代码文件传给方法进行编译,文件中包含源代码。

3、CompileAssemblyFromDom——这个重载版本跟我们之前所学的内容关联性最大,因为它是把 CodeCompileUnit 实例传进去来编译的。

以上几个重载,尽管代码的来源不同,但都有一个共同点:支持多个源。

咱们知道,代码文档结构的根是命名空间,然后是类型,类型下是成员。一个程序集是可以包括多个命名空间的,假设编译的代码源自文件,而每个文件的代码都包含一个命名空间,那么要将多个命名空间合到一个程序集中,就可以把多个文件同时进行编译。当然,你也可以把所有的代码都放到一个文件中,然后只编译这个文件就行了。随你怎么弄,这样做只是为了灵活。

大伙应该也发现了,这些方法都有一个参数,是 CompilerParameters 类型的,它的作用是设置编译选项。老周大概总结这么几点,以供大家参考,其他的大家不妨自己摸索,放心,不会很复杂的。

1、如果你要生成可直接运行的程序集,即.exe,那就得把GenerateExecutable属性设置为true,默认它是为false的,即生成dll文件。所有可执行文件,不管你用啥语言写,都必须有入口点的,所以,如果要生成exe,就必须设置MainClass属性,它指的是包含Main方法的类,类名必须完整,要写上命名空间的名字,如my.Program。

2、设置OutputAssembly属性,指定输出文件名,可以是绝对路径,也可以是相对路径。如dddd.exe、kkkk.dll等。当然你可以用其他名字,如comm.ft,但是,如果要生成exe,后缀必须是.exe,这样才能双击运行。如果这个属性没有指定,它会生成一个随机的文件名,并且输出临时文件目录下。注意:输出文件是设置OutputAssembly属性,不是CoreAssemblyFileName属性,千万不要弄错,CoreAssemblyFileName是设置核心类库的位置,即常见的 mscorlib.dll,主要是包含.net基本类型的程序集,一般我们不用设置它,由编译器自行选择合适的版本。

3、如果GenerateInMemory设置为true,则可以不设置OutputAssembly,因为GenerateInMemory属性表示把程序集生成到内存中,而不是文件中。

4、TempFiles设置编译时所产生的临时文件的路径,默认是临时文件夹,这个一般不用改。

5、编译过程实际上是调用.net的命令行工具的,对于VB.NET语言,调用vbc命令,对于C#语言,调用csc命令。如果要指定一些编译器选项,可以设置CompilerOptions属性,它是一个字符串。关于编译选项,可以在开发工具的命令行工具中输入csc /?或vbc /?查看。

下面,先举一个最简单的例子,就直接用代码源文件来编译。

假设我在【文档】下建了一个demo.cs文件,在里面输入了以下代码:

using System;

namespace Sample
{
    public class Demo
    {
        // 成员列表
    }
}

然后保存。

接下来咱们要在程序中动态编译这个代码文件。

            // 文件路径
            string doclib = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
            string srccodePath = Path.Combine(doclib, "demo.cs");

            CodeDomProvider provider = CodeDomProvider.CreateProvider("cs");
            // 编译参数
            CompilerParameters p = new CompilerParameters();
            // 输出文件
            p.OutputAssembly = "DemoLib.dll";
            // 添加引用的程序集
            // 其实我们这里用不上,只是作为演示
            // mscorLib.dll是不用添加的,它是默认库
            p.ReferencedAssemblies.Add("System.dll");
            // 编译
            CompilerResults res = provider.CompileAssemblyFromFile(p, srccodePath);
            // 检查编译结果
            if (res.Errors.Count == 0)
            {
                // 没有出错
                Console.WriteLine("编译成功。");
                // 获取刚刚编译的程序集信息
                Assembly outputAss = res.CompiledAssembly;
                // 全名
                Console.WriteLine($"程序集全名:{outputAss.FullName}");
                // 位置
                Console.WriteLine($"程序集位置:{outputAss.Location}");
                // 程序集中的类型
                Type[] types = outputAss.GetTypes();
                Console.WriteLine("----------------------\n类型列表:");
                foreach (Type t in types)
                {
                    Console.WriteLine(t.FullName);
                }
            }
            else
            {
                // 如果编译出错
                Console.WriteLine("发生错误,详见以下内容:");
                foreach (CompilerError er in res.Errors)
                {
                    Console.WriteLine($"行{er.Line},列{er.Column},错误号{er.ErrorNumber},错误信息:{er.ErrorText}");
                }
            }

代码虽长,但不难懂。注意编译完成后,会返回一个表示编译结果的CompilerResults实例,如果其中的Errors集合中没有元素,说明编译成功,如果里面有东西,表明其间发生了错误。每个错误都用CompilerError类封装。

如果成功编译,通过结果的CompiledAssembly属性就可以获取到刚刚编译的程序集信息。

示例输出结果如下图。

下面提供一个用 CodeDom 来编译的例子。

            CodeCompileUnit unit = new CodeCompileUnit();
            // 命名空间
            CodeNamespace ns = new CodeNamespace("MyApp");
            unit.Namespaces.Add(ns);
            ns.Imports.Add(new CodeNamespaceImport(nameof(System)));
            ns.Imports.Add(new CodeNamespaceImport($"{nameof(System)}.{nameof(System.Windows)}.{nameof(System.Windows.Forms)}"));
            // 类型
            CodeTypeDeclaration typedec = new CodeTypeDeclaration("Program");
            ns.Types.Add(typedec);
            typedec.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            // 入口点
            CodeEntryPointMethod main = new CodeEntryPointMethod();
            typedec.Members.Add(main);
            // 创建窗口实例
            CodeVariableDeclarationStatement newwindow = new CodeVariableDeclarationStatement();
            main.Statements.Add(newwindow);
            newwindow.Name = "mainWindow";
            newwindow.Type = new CodeTypeReference(nameof(System.Windows.Forms.Form));
            newwindow.InitExpression = new CodeObjectCreateExpression(nameof(System.Windows.Forms.Form));
            // 设置窗口标题栏
            CodeAssignStatement settitle = new CodeAssignStatement();
            main.Statements.Add(settitle);
            settitle.Left = new CodePropertyReferenceExpression(new CodeVariableReferenceExpression(newwindow.Name), nameof(System.Windows.Forms.Form.Text));
            settitle.Right = new CodePrimitiveExpression("我的应用程序");
            // 调用 Application.Run 方法
            CodeMethodInvokeExpression invokeexp = new CodeMethodInvokeExpression(new CodeTypeReferenceExpression(nameof(System.Windows.Forms.Application)), nameof(System.Windows.Forms.Application.Run), new CodeVariableReferenceExpression(newwindow.Name));
            CodeExpressionStatement invrunstatem = new CodeExpressionStatement(invokeexp);
            main.Statements.Add(invrunstatem);

            // 生成代码
            CodeDomProvider provider = CodeDomProvider.CreateProvider("cs");
            provider.GenerateCodeFromCompileUnit(unit, Console.Out, null);

            // 编译
            CompilerParameters p = new CompilerParameters();
            p.GenerateExecutable = true; //生成exe
            p.CompilerOptions = "/t:winexe"; //非控制台应用程序
            p.OutputAssembly = "testapp.exe";
            // 包含入口点的类
            p.MainClass = $"{ns.Name}.{typedec.Name}";
            // 引用的程序集
            p.ReferencedAssemblies.Add("System.dll");
            p.ReferencedAssemblies.Add("System.Windows.Forms.dll");

            CompilerResults res = provider.CompileAssemblyFromDom(p, unit);
            if (res.Errors.Count == 0)
            {
                Console.WriteLine("编译成功。");
                // 启动它
                System.Diagnostics.Process.Start(res.CompiledAssembly.Location);
            }
            else
            {
                Console.WriteLine("错误信息:");
                foreach (CompilerError er in res.Errors)
                {
                    Console.WriteLine(er.ErrorText);
                }
            }

代码虽然很是TMD的长,但你别紧张,其实就做了三件事。

1、构建代码逻辑。这个示例生成一个Windows Form程序,所以,需要定义一个类,在类中必须有Main方法,在Main方法中实例化窗口类,然后用Application.Run方法显示窗口。

2、生成代码,这个大家已经熟悉,前面N篇文章中就用到多次。

3、编译。

我们重点放在编译上,请大家注意,尽管GenerateExecutable属性已经被设置为true,不过,你懂的,exe程序有两类,一类是控制台,一类是常见的win窗口,所以,这里还得借助编译器命令行选项,加一个/t:winexe,表示生成的是Windows标准窗口程序。而正因为生成的是exe文件,所以,不要忘了MainClass属性,指定我们刚刚用CodeDom构建的那个类,它包含了入口点方法。

运行之后,会在当前程序的同一目录下,生成一个.exe文件,并且执行后,显示一个空白的窗口。如下图所示。

好了,说到了编译部分,CodeDom的这一系列文章也写得差不多了,不过后面还会加一篇补充的,把一些零碎的内容过一下。

之所以前面的文章中,一些评论老周没有回复,是因为老周又发现,又有人把CodeDom和Emit搞混了,动态发出程序集是基于指令的,而CodeDom是基于代码文档,CodeDom既可用于生成代码源文件,也可用于动态编译。这个老周前面是强调过的,希望大家注意。

动态发出程序集和IL的内容比较复杂,也不算太常用,改天有空,老周也写一写动态程序集方面的内容吧。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK