【.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的內容比較復雜,也不算太常用,改天有空,老周也寫一寫動態程序集方面的內容吧。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM