讓C#語言充當自身腳本!——.NET中的動態編譯

原文: 讓C#語言充當自身腳本!——.NET中的動態編譯

代碼的動態編譯並執行是.NET平臺提供給咱們的很強大的一個工具,用以靈活擴展(固然是面對內部開發人員)複雜而沒法估算的邏輯,並經過一些額外的代碼來擴展咱們已有 的應用程序。這在很大程度上給咱們提供了另一種擴展的方式(固然這並不能算是嚴格意義上的擴展,但至少爲咱們提供了一種思路)。
動態代碼執行能夠應用在諸如模板生成,外加邏輯擴展等一些場合。一個簡單的例子,爲了網站那的響應速度,HTML靜態頁面每每是咱們最好的選擇,但基於數據驅動的網站每每又很難用靜態頁面實現,那麼將動態頁面生成html的工做或許就是一個很好的應用場合。另外,對於一些模板的套用,咱們一樣能夠用它來作。另外這自己也是插件編寫的方式。


最基本的動態編譯css

.Net爲咱們提供了很強大的支持來實現這一切咱們能夠去作的基礎,主要應用的兩個命名空間是:System.CodeDom.Compiler和Microsoft.CSharp或Microsoft.VisualBasic。另外還須要用到反射來動態執行你的代碼。動態編譯並執行代碼的原理其實在於將提供的源代碼交予CSharpCodeProvider來執行編譯(其實和CSC沒什麼兩樣),若是沒有任何編譯錯誤,生成的IL代碼會被編譯成DLL存放于于內存並加載在某個應用程序域(默認爲當前)內並經過反射的方式來調用其某個方法或者觸發某個事件等。之因此說它是插件編寫的一種方式也正是由於與此,咱們能夠經過預先定義好的藉口來組織和擴展咱們的程序並將其交還給主程序去觸發。一個基本的動態編譯並執行代碼的步驟包括:
將要被編譯和執行的代碼讀入並以字符串方式保存
聲明CSharpCodeProvider對象實例
調用CSharpCodeProvider實例的CompileAssemblyFromSource方法編譯
用反射生成被生成對象的實例(Assembly.CreateInstance)
調用其方法

如下代碼片斷包含了完整的編譯和執行過程:html

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;

namespace DynamicCompileBase
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //get the code to compile
            string strSourceCode = this.txtSource.Text;

            // 1.Create a new CSharpCodePrivoder instance
            CSharpCodeProvider objCSharpCodePrivoder = new CSharpCodeProvider();

            // 2.Sets the runtime compiling parameters by crating a new CompilerParameters instance
            CompilerParameters objCompilerParameters = new CompilerParameters();
            objCompilerParameters.ReferencedAssemblies.Add("System.dll");
            objCompilerParameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
            objCompilerParameters.GenerateInMemory = true;

            // 3.CompilerResults: Complile the code snippet by calling a method from the provider
            CompilerResults cr = objCSharpCodePrivoder.CompileAssemblyFromSource(objCompilerParameters, strSourceCode);

            if (cr.Errors.HasErrors)
            {
                string strErrorMsg = cr.Errors.Count.ToString() + " Errors:";

                for (int x = 0; x < cr.Errors.Count; x++)
                {
                    strErrorMsg = strErrorMsg + "/r/nLine: " +
                                 cr.Errors[x].Line.ToString() + " - " +
                                 cr.Errors[x].ErrorText;
                }

                this.txtResult.Text = strErrorMsg;
                MessageBox.Show("There were build erros, please modify your code.", "Compiling Error");
                return;
            }

            // 4. Invoke the method by using Reflection
            Assembly objAssembly = cr.CompiledAssembly;
            object objClass = objAssembly.CreateInstance("Dynamicly.HelloWorld");

            if (objClass == null)
            {
                this.txtResult.Text = "Error: " + "Couldn't load class.";
                return;
            }

            object[] objCodeParms = new object[1];
            objCodeParms[0] = "Allan.";

            string strResult = (string)objClass.GetType().InvokeMember(
                       "GetTime", BindingFlags.InvokeMethod, null, objClass, objCodeParms);
            this.txtResult.Text = strResult;
        }
    }
}
須要解釋的是,這裏咱們在傳遞編譯參數時設置了GenerateInMemory爲true,這代表生成的DLL會被加載在內存中(隨後被默認引用入當前應用程序域)。在調用GetTime方法時咱們須要加入參數,傳遞object類型的數組並經過Reflection的InvokeMember來調用。在建立生成的Assembly中的對象實例時,須要注意用到的命名空間是你輸入代碼的真實命名空間。如下是咱們輸入的測試代碼(爲了方便,全部的代碼都在外部輸入,動態執行時不作調整):
using System;

namespace Dynamicly
{
    public class HelloWorld
    {
        public string GetTime(string strName)
        {
            return  "Welcome " + strName + ", Check in at " + System.DateTime.Now.ToString();
        }
    }
}

運行附件中提供的程序,能夠很容易獲得如下結果:
數組



改進的執行過程

如今一切看起來很好,咱們能夠編譯代碼並把代碼加載到當前應用程序域中來參與咱們的活動,但你是否想過去卸載掉這段程序呢?更好的去控制程序呢?另外,當你運行這個程序不少遍的時候,你會發現佔用內存很大,並且每次執行都會增大內存使用。是否須要來解決這個問題呢?固然須要,不然你會發現這個東西根本沒用,我須要執行的一些大的應用會讓個人服務器crzay,不堪重負而瘋掉的。
要解決這個問題咱們須要來了解一下應用程序域。.NET Application Domain是.NET提供的運行和承載一個活動的進程(Process)的容器,它將這個進程運行所需的代碼和數據,隔離到一個小的範圍內,稱爲Application Domain。當一個應用程序運行時,Application Domains將全部的程序集/組件集加載到當前的應用程序域中,並根據須要來調用。而對於動態生成的代碼/程序集,咱們看起來好像並無辦法去管理它。其實否則,咱們能夠用Application Domain提供的管理程序集的辦法來動態加載和移除Assemblies來達到咱們的提升性能的目的。具體怎麼作呢,在前邊的基礎上增長如下步驟:
建立另一個Application Domain
動態建立(編譯)代碼並保存到磁盤
建立一個公共的遠程調用接口
建立遠程調用接口的實例。並經過這個接口來訪問其方法。
換句話來說就是將對象加載到另一個AppDomain中並經過遠程調用的方法來調用。所謂遠程調用其實也就是跨應用程序域調用,因此這個對象(動態代碼)必須繼承於MarshalByRefObject類。爲了複用,這個接口被單獨提到一個工程中,並提供一個工廠來簡化每次的調用操做:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace RemoteAccess
{
    /// <summary>
    /// Interface that can be run over the remote AppDomain boundary.
    /// </summary>
    public interface IRemoteInterface
    {
        object Invoke(string lcMethod, object[] Parameters);
    }

    /// <summary>
    /// Factory class to create objects exposing IRemoteInterface
    /// </summary>
    public class RemoteLoaderFactory : MarshalByRefObject
    {
        private const BindingFlags bfi = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;
        public RemoteLoaderFactory() { }
        public IRemoteInterface Create(string assemblyFile, string typeName, object[] constructArgs)
        {
            return (IRemoteInterface)Activator.CreateInstanceFrom(
                     assemblyFile, typeName, false, bfi, null, constructArgs,
                     null, null, null).Unwrap();
        }
    }
}
接下來在原來基礎上須要修改的是:
將編譯成的DLL保存到磁盤中。
建立另外的AppDomain。
得到IRemoteInterface接口的引用。(將生成的DLL加載到額外的AppDomain)
調用InvokeMethod方法來遠程調用。
能夠經過AppDomain.Unload()方法卸載程序集。
如下是完整的代碼,演示瞭如何應用這一方案。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
using RemoteAccess;

namespace DynamicCompileAppDomain
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // get the code to compile
            string strSourceCode = this.txtSource.Text;

            // 0. Create an addtional AppDomain
            AppDomainSetup objSetup = new AppDomainSetup();
            objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
            AppDomain objAppDomain = AppDomain.CreateDomain("MyAppDomain", null, objSetup);

            // 1.Create a new CSharpCodePrivoder instance
            CSharpCodeProvider objCSharpCodePrivoder = new CSharpCodeProvider();

            // 2.Sets the runtime compiling parameters by crating a new CompilerParameters instance
            CompilerParameters objCompilerParameters = new CompilerParameters();
            objCompilerParameters.ReferencedAssemblies.Add("System.dll");
            objCompilerParameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");

            // Load the remote loader interface
            objCompilerParameters.ReferencedAssemblies.Add("RemoteAccess.dll");

            // Load the resulting assembly into memory
            objCompilerParameters.GenerateInMemory = false;
            objCompilerParameters.OutputAssembly = "DynamicalCode.dll";

            // 3.CompilerResults: Complile the code snippet by calling a method from the provider
            CompilerResults cr = objCSharpCodePrivoder.CompileAssemblyFromSource(objCompilerParameters, strSourceCode);

            if (cr.Errors.HasErrors)
            {
                string strErrorMsg = cr.Errors.Count.ToString() + " Errors:";

                for (int x = 0; x < cr.Errors.Count; x++)
                {
                    strErrorMsg = strErrorMsg + "/r/nLine: " +
                                 cr.Errors[x].Line.ToString() + " - " +
                                 cr.Errors[x].ErrorText;
                }

                this.txtResult.Text = strErrorMsg;
                MessageBox.Show("There were build erros, please modify your code.", "Compiling Error");
                return;
            }

            // 4. Invoke the method by using Reflection
            RemoteLoaderFactory factory = (RemoteLoaderFactory)objAppDomain.CreateInstance("RemoteAccess", "RemoteAccess.RemoteLoaderFactory").Unwrap();

            // with help of factory, create a real 'LiveClass' instance
            object objObject = factory.Create("DynamicalCode.dll", "Dynamicly.HelloWorld", null);

            if (objObject == null)
            {
                this.txtResult.Text = "Error: " + "Couldn't load class.";
                return;
            }

            // *** Cast object to remote interface, avoid loading type info
            IRemoteInterface objRemote = (IRemoteInterface)objObject;

            object[] objCodeParms = new object[1];
            objCodeParms[0] = "Allan.";
            string strResult = (string)objRemote.Invoke("GetTime", objCodeParms);
            this.txtResult.Text = strResult;

            //Dispose the objects and unload the generated DLLs.
            objRemote = null;
            AppDomain.Unload(objAppDomain);
            System.IO.File.Delete("DynamicalCode.dll");
        }
    }
}
對於客戶端的輸入程序,咱們須要繼承於MarshalByRefObject類和IRemoteInterface接口,並添加對RemoteAccess程序集的引用。如下爲輸入:
using System;
using System.Reflection;
using RemoteAccess;

namespace Dynamicly
{
    public class HelloWorld : MarshalByRefObject,IRemoteInterface
    {
        public object Invoke(string strMethod,object[] Parameters)
        {
            return this.GetType().InvokeMember(strMethod, BindingFlags.InvokeMethod,null,this,Parameters);
        }

        public string GetTime(string strName)
        {
            return  "Welcome " + strName + ", Check in at " + System.DateTime.Now.ToString();
        }
    }
}
這樣,你能夠經過適時的編譯,加載和卸載程序集來保證你的程序始終處於一個可控消耗的過程,而且達到了動態編譯的目的,並且由於在不一樣的應用程序域中,讓你的自己的程序更加安全和健壯。


最後附上示例程序源代碼:安全

http://pan.baidu.com/s/1skSPQ3b
服務器

示例程序共有4個項目:ide

DynamicCompile是http://blog.csdn.net/clb929/article/details/51371363這篇文章的練習程序工具

DynamicCompileBase是本文無應用程序域動態編譯的例子(沒法釋放內存)
性能

RemoteAccess是遠程調用應用程序域的庫
測試

DynamicCompileAppDomain是遠程調用應用程序域動態編譯的示例程序(可以將C#代碼編譯爲臨時DLL,動態加載並執行,而後釋放,最後刪除臨時DLL)網站

相關文章
相關標籤/搜索