代码改变世界

详解.NET中的动态编译

2008-11-03 08:19  TTlive  阅读(237)  评论(0编辑  收藏  举报

    代码的动态编译并执行是一个.NET平台提供给我们的很强大的工具用以灵活扩展(当然是面对内部开发人员)复杂而无法估算的逻辑,并通过一些额外的代码来扩展我们已有 的应用程序。这在很大程度上给我们提供了另外一种扩展的方式(当然这并不能算是严格意义上的扩展,但至少为我们提供了一种思路)。

    动态代码执行可以应用在诸如模板生成,外加逻辑扩展等一些场合。一个简单的例子,为了网站那的响应速度,HTML静态页面往往是我们最好的选择,但基于数据驱动的网站往往又很难用静态页面实现,那么将动态页面生成html的工作或许就是一个很好的应用场合。另外,对于一些模板的套用,我们同样可以用它来做。另外这本身也是插件编写的方式。

    最基本的动态编译

    .Net为我们提供了很强大的支持来实现这一切我们可以去做的基础,主要应用的两个命名空间是:System.CodeDom.Compiler和Microsoft.CSharp或Microsoft.VisualBasic。另外还需要用到反射来动态执行你的代码。动态编译并执行代码的原理其实在于将提供的源代码交予CSharpCodeProvider来执行编译(其实和CSC没什么两样),如果没有任何编译错误,生成的IL代码会被编译成DLL存放于于内存并加载在某个应用程序域(默认为当前)内并通过反射的方式来调用其某个方法或者触发某个事件等。之所以说它是插件编写的一种方式也正是因为与此,我们可以通过预先定义好的借口来组织和扩展我们的程序并将其交还给主程序去触发。一个基本的动态编译并执行代码的步骤包括:

    ·         将要被编译和执行的代码读入并以字符串方式保存

    ·         声明CSharpCodeProvider对象实例

    ·         调用CSharpCodeProvider实例的CompileAssemblyFromSource方法编译

    ·         用反射生成被生成对象的实例(Assembly.CreateInstance)

    ·         调用其方法

    以下代码片段包含了完整的编译和执行过程:

  1. //get the code to compile
  2. string strSourceCode = this.txtSource.Text;
  3. // 1.Create a new CSharpCodePrivoder instance
  4. CSharpCodeProvider objCSharpCodePrivoder = new CSharpCodeProvider();
  5. // 2.Sets the runtime compiling parameters by crating a new CompilerParameters instance
  6. CompilerParameters objCompilerParameters = new CompilerParameters();
  7. objCompilerParameters.ReferencedAssemblies.Add("System.dll");
  8. objCompilerParameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
  9. objCompilerParameters.GenerateInMemory = true;
  10. // 3.CompilerResults: Complile the code snippet by calling a method from the provider
  11. CompilerResults cr = objCSharpCodePrivoder.CompileAssemblyFromSource(objCompilerParameters, strSourceCode);
  12. if (cr.Errors.HasErrors)
  13. {
  14.     string strErrorMsg = cr.Errors.Count.ToString() + " Errors:";
  15.     for (int x = 0; x < cr.Errors.Count; x++)
  16.     {
  17.         strErrorMsg = strErrorMsg + "/r/nLine: " +
  18.                      cr.Errors[x].Line.ToString() + " - " +
  19.                      cr.Errors[x].ErrorText;
  20.     }
  21.     this.txtResult.Text = strErrorMsg;
  22.     MessageBox.Show("There were build erros, please modify your code.""Compiling Error");
  23.     return;
  24. }
  25. // 4. Invoke the method by using Reflection
  26. Assembly objAssembly = cr.CompiledAssembly;
  27. object objClass = objAssembly.CreateInstance("Dynamicly.HelloWorld");
  28. if (objClass == null)
  29. {
  30.     this.txtResult.Text = "Error: " + "Couldn't load class.";
  31.     return;
  32. }
  33. object[] objCodeParms = new object[1];
  34. objCodeParms[0] = "Allan.";
  35. string strResult = (string)objClass.GetType().InvokeMember(
  36.            "GetTime", BindingFlags.InvokeMethod, null, objClass, objCodeParms);
  37. this.txtResult.Text = strResult;
    需要解释的是,这里我们在传递编译参数时设置了GenerateInMemory为true,这表明生成的DLL会被加载在内存中(随后被默认引用入当前应用程序域)。在调用GetTime方法时我们需要加入参数,传递object类型的数组并通过Reflection的InvokeMember来调用。在创建生成的Assembly中的对象实例时,需要注意用到的命名空间是你输入代码的真实命名空间。以下是我们输入的测试代码(为了方便,所有的代码都在外部输入,动态执行时不做调整):
  1. namespace Dynamicly
  2. {
  3.     public class HelloWorld
  4.     {
  5.         public string GetTime(string strName)
  6.         {
  7.             return  "Welcome " + strName + ", Check in at " + System.DateTime.Now.ToString();
  8.         }
  9.     }
  10. }

    运行附件中提供的程序,可以很容易得到一下结果:

    改进的执行过程

 

    现在一切看起来很好,我们可以编译代码并把代码加载到当前应用程序域中来参与我们的活动,但你是否想过去卸载掉这段程序呢?更好的去控制程序呢?另外,当你运行这个程序很多遍的时候,你会发现占用内存很大,而且每次执行都会增大内存使用。是否需要来解决这个问题呢?当然需要,否则你会发现这个东西根本没用,我需要执行的一些大的应用会让我的服务器crzay,不堪重负而疯掉的。

    要解决这个问题我们需要来了解一下应用程序域。.NET Application Domain是.NET提供的运行和承载一个活动的进程(Process)的容器,它将这个进程运行所需的代码和数据,隔离到一个小的范围内,称为Application Domain。当一个应用程序运行时,Application Domains将所有的程序集/组件集加载到当前的应用程序域中,并根据需要来调用。而对于动态生成的代码/程序集,我们看起来好像并没有办法去管理它。其实不然,我们可以用Application Domain提供的管理程序集的办法来动态加载和移除Assemblies来达到我们的提高性能的目的。具体怎么做呢,在前边的基础上增加以下步骤:

    ·         创建另外一个Application Domain

    ·         动态创建(编译)代码并保存到磁盘

    ·         创建一个公共的远程调用接口

    ·         创建远程调用接口的实例。并通过这个接口来访问其方法。

    换句话来讲就是将对象加载到另外一个AppDomain中并通过远程调用的方法来调用。所谓远程调用其实也就是跨应用程序域调用,所以这个对象(动态代码)必须继承于MarshalByRefObject类。为了复用,这个接口被单独提到一个工程中,并提供一个工厂来简化每次的调用操作:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Reflection;
  6. namespace RemoteAccess
  7. {
  8.           /// <summary>
  9.           /// Interface that can be run over the remote AppDomain boundary.
  10.           /// </summary>
  11.           public interface IRemoteInterface
  12.           {
  13.                    object Invoke(string lcMethod,object[] Parameters);
  14.           }
  15.           /// <summary>
  16.           /// Factory class to create objects exposing IRemoteInterface
  17.           /// </summary>
  18.           public class RemoteLoaderFactory : MarshalByRefObject
  19.           {
  20.                    private const BindingFlags bfi = BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance;
  21.                    public RemoteLoaderFactory() {}
  22.                    public IRemoteInterface Create( string assemblyFile, string typeName, object[] constructArgs )
  23.                    {
  24.                              return (IRemoteInterface) Activator.CreateInstanceFrom(
  25.                                       assemblyFile, typeName, false, bfi, null, constructArgs,
  26.                                       nullnullnull ).Unwrap();
  27.                    }
  28.           }        
  29. }

    接下来在原来基础上需要修改的是:

    ·         将编译成的DLL保存到磁盘中。

    ·         创建另外的AppDomain。

    ·         获得IRemoteInterface接口的引用。(将生成的DLL加载到额外的AppDomain)

    ·         调用InvokeMethod方法来远程调用。

    ·         可以通过AppDomain.Unload()方法卸载程序集。

    以下是完整的代码,演示了如何应用这一方案。

  1. //get the code to compile
  2. string strSourceCode = this.txtSource.Text;
  3. //1. Create an addtional AppDomain
  4. AppDomainSetup objSetup = new AppDomainSetup();
  5. objSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
  6. AppDomain objAppDomain = AppDomain.CreateDomain("MyAppDomain"null, objSetup);
  7. // 1.Create a new CSharpCodePrivoder instance
  8. CSharpCodeProvider objCSharpCodePrivoder = new CSharpCodeProvider();
  9. // 2.Sets the runtime compiling parameters by crating a new CompilerParameters instance
  10. CompilerParameters objCompilerParameters = new CompilerParameters();
  11. objCompilerParameters.ReferencedAssemblies.Add("System.dll");
  12. objCompilerParameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
  13. // Load the remote loader interface
  14. objCompilerParameters.ReferencedAssemblies.Add("RemoteAccess.dll");
  15. // Load the resulting assembly into memory
  16. objCompilerParameters.GenerateInMemory = false;
  17. objCompilerParameters.OutputAssembly = "DynamicalCode.dll";
  18. // 3.CompilerResults: Complile the code snippet by calling a method from the provider
  19. CompilerResults cr = objCSharpCodePrivoder.CompileAssemblyFromSource(objCompilerParameters, strSourceCode);
  20. if (cr.Errors.HasErrors)
  21. {
  22.     string strErrorMsg = cr.Errors.Count.ToString() + " Errors:";
  23.     for (int x = 0; x < cr.Errors.Count; x++)
  24.     {
  25.         strErrorMsg = strErrorMsg + "/r/nLine: " +
  26.                      cr.Errors[x].Line.ToString() + " - " +
  27.                      cr.Errors[x].ErrorText;
  28.     }
  29.     this.txtResult.Text = strErrorMsg;
  30.     MessageBox.Show("There were build erros, please modify your code.""Compiling Error");
  31.     return;
  32. }
  33. // 4. Invoke the method by using Reflection
  34. RemoteLoaderFactory factory = (RemoteLoaderFactory)objAppDomain.CreateInstance("RemoteAccess","RemoteAccess.RemoteLoaderFactory").Unwrap();
  35. // with help of factory, create a real 'LiveClass' instance
  36. object objObject = factory.Create("DynamicalCode.dll""Dynamicly.HelloWorld"null);
  37. if (objObject == null)
  38. {
  39.     this.txtResult.Text = "Error: " + "Couldn't load class.";
  40.     return;
  41. }
  42. // *** Cast object to remote interface, avoid loading type info
  43. IRemoteInterface objRemote = (IRemoteInterface)objObject;
  44. object[] objCodeParms = new object[1];
  45. objCodeParms[0] = "Allan.";
  46. string strResult = (string)objRemote.Invoke("GetTime", objCodeParms);
  47. this.txtResult.Text = strResult;
  48. //Dispose the objects and unload the generated DLLs.
  49. objRemote = null;
  50. AppDomain.Unload(objAppDomain);
  51. System.IO.File.Delete("DynamicalCode.dll");
    对于客户端的输入程序,我们需要继承于MarshalByRefObject类和IRemoteInterface接口,并添加对RemoteAccess程序集的引用。以下为输入:

 

  1. using System;
  2. using System.Reflection;
  3. using RemoteAccess;
  4. namespace Dynamicly
  5. {
  6.     public class HelloWorld : MarshalByRefObject,IRemoteInterface
  7.     {
  8.         public object Invoke(string strMethod,object[] Parameters) 
  9.         {
  10.             return this.GetType().InvokeMember(strMethod, BindingFlags.InvokeMethod,null,this,Parameters);
  11.         }
  12.         public string GetTime(string strName)
  13.         {
  14.             return  "Welcome " + strName + ", Check in at " + System.DateTime.Now.ToString();
  15.         }
  16.     }
  17. }

 

    这样,你可以通过适时的编译,加载和卸载程序集来保证你的程序始终处于一个可控消耗的过程,并且达到了动态编译的目的,而且因为在不同的应用程序域中,让你的本身的程序更加安全和健壮。示例代码下载:https://files.cnblogs.com/zlgcool/DynamicCompiler.rar