如何在.NET中实现脚本引擎 (CodeDom篇)

        .NET 本身提供了强大的脚本引擎,可以直接使用.NET CLR的任何编程语言作为脚本语言,如VB.NET、C#、JScript, J#等等。使用脚本引擎,我们可以动态生成任意表达式、或动态导入任意脚本文件,并在任意时候执行。
        经实践发现,我们可以使用至少两种不同的方式在.NET中使用脚本引擎:VsaEngine和CodeDom。
        其实,CodeDom不能算是真正的脚本引擎,它实际上是编译器。但是我们完全可以利用CodeDom来模拟脚本引擎。
        使用Emit方法也能达到动态生成可执行代码的目的,而且Emit生成的代码不需要编译,因此速度更快。但是Emit插入的实际上是汇编代码,不能算是脚本语言。
        本文介绍如何以CodeDom方式来动态生成可执行代码。

如何在.NET中实现脚本引擎 (CodeDom篇)    沐枫网志

    1.     构造一个编译器

  • 设置编译参数
    编译参数需要在CompilerParameters设置: 

CompilerOptions 用于设置编译器命令行参数
IncludeDebugInformation 用于指示是否在内存在生成Assembly
GenerateInMemory 用于指示是否在内存在生成Assembly
GenerateExecutable 用于指示生成的Assembly类型是exe还是dll
OutputAssembly 用于指示生成的程序文件名(仅在GenerateInMemory为false的情况)
ReferencedAssemblies 用于添加引用Assembly

例如:

theParameters.ReferencedAssemblies.Add("System.dll");
  • 创建指定语言的编译器
    编译需要由指定语言的CodeDomProvider生成。

这里列举一些.NET的CodeDomProvider:        

vb.net  Microsoft.VisualBasic.VBCodeProvider
C# Microsoft.CSharp.CSharpCodeProvider
jscript Microsoft.JScript.JScriptCodeProvider
J# Microsoft.VJSharp.VJSharpCodeProvider

以C#为例,要创建C#编译器,代码如下: 

CodeDomProvider theProvider= (ICodeCompiler) new Microsoft.CSharp.CSharpCodeProvider();

下面是完整的创建编译器的例子:

        /**//// <summary>
        
/// 创建相应脚本语言的编译器
        
/// </summary>

        private void createCompiler(string strLanguage, bool debugMode, string strAssemblyFileName)
        
{
            
this.theParameters = new CompilerParameters();
            
this.theParameters.OutputAssembly = System.IO.Path.Combine(System.IO.Path.GetTempPath(), strAssemblyFileName + ".dll");
            
this.theParameters.GenerateExecutable = false;
            
this.theParameters.GenerateInMemory = true;
            
if(debugMode)
            
{
                
this.theParameters.IncludeDebugInformation = true;
                
this.theParameters.CompilerOptions += "/define:TRACE=1 /define:DEBUG=1 ";
            }

            
else
            
{
                
this.theParameters.IncludeDebugInformation = false;
                
this.theParameters.CompilerOptions += "/define:TRACE=1 ";
            }


            AddReference(
"System.dll");
            AddReference(
"System.Data.dll");
            AddReference(
"System.Xml.dll");

            strLanguage 
= strLanguage.ToLower();
 
            
if("visualbasic" == strLanguage || "vb" == strLanguage)
            
{
                theProvider 
= new Microsoft.VisualBasic.VBCodeProvider();
                
if(debugMode)
                    theParameters.CompilerOptions 
+= "/debug:full /optimize- /optionexplicit+ /optionstrict+ /optioncompare:text /imports:Microsoft.VisualBasic,System,System.Collections,System.Diagnostics ";
                
else
                    theParameters.CompilerOptions 
+= "/optimize /optionexplicit+ /optionstrict+ /optioncompare:text /imports:Microsoft.VisualBasic,System,System.Collections,System.Diagnostics ";
                AddReference(
"Microsoft.VisualBasic.dll");
            }

            
else if("jscript" == strLanguage || "js" == strLanguage)
            
{
                theProvider 
= new Microsoft.JScript.JScriptCodeProvider();
                AddReference(
"Microsoft.JScript.dll");
            }

            
else if("csharp" == strLanguage || "cs" == strLanguage || "c#" == strLanguage)
            
{
                theProvider 
= new Microsoft.CSharp.CSharpCodeProvider();
                
if(!debugMode)
                    theParameters.CompilerOptions 
+= "/optimize ";
            }

//            else if("jsharp" == strLanguage || "vj" == strLanguage || "j#" == strLanguage)
//            {
//                theProvider = new Microsoft.VJSharp.VJSharpCodeProvider();
//                if(!debugMode)
//                    theParameters.CompilerOptions += "/optimize ";
//            }
            else
                
throw new System.Exception("指定的脚本语言不被支持。"); 
        }

        
/**//// <summary>
        
/// 添加引用对象。
        
/// </summary>
        
/// <param name="__strAssemblyName">引用的文件名</param>

        public void AddReference(string __strAssemblyName)
        
{
            theParameters.ReferencedAssemblies.Add(__strAssemblyName);
        }



2.     编译源代码 

        编译源代码相当简单,只需一条语句就搞定了:

CompilerResults compilerResults  = compiler.CompileAssemblyFromSource(this.theParameters, this.SourceText); 

执行后,可以从compilerResults取得以下内容: 

NativeCompilerReturnValue 编译结果,用于检查是否成功
Errors 编译时产生的错误和警告信息
CompiledAssembly 如果编译成功,则返回编译生成的Assembly

  
示例函数: 

        /**//// <summary>
        
/// 编译脚本。编译前将清空以前的编译信息。
        
/// CompilerInfo将包含编译时产生的错误信息。
        
/// </summary>
        ///
 <returns>成功时返回True。不成功为False。</returns>

        public bool Compile()
        
{
            
this.theCompilerInfo = "";
            
this.isCompiled = false;
            
this.theCompiledAssembly = null;
            // 下面一行代码仅用于.NET 2.0
            this.theCompilerResults = this.theProvider.CompileAssemblyFromSource(this.theParameters, this.SourceText);
            // 下面一行代码用于.NET1.1(与.NET2.0的区别是,.NET2.0不需要调用CreateCompiler)
            // this.theCompilerResults = this.theProvider.CreateCompiler().CompileAssemblyFromSource(this.theParameters, this.SourceText);

            if
(this.theCompilerResults.NativeCompilerReturnValue == 0)
            
{
                
this.isCompiled = true;
                
this.theCompiledAssembly = this.theCompilerResults.CompiledAssembly;
            }


            System.Text.StringBuilder compilerInfo =
 new System.Text.StringBuilder();

            
foreach(CompilerError err in this.theCompilerResults.Errors)
            
{
                compilerInfo.Append(err.ToString());
                compilerInfo.Append(
"\r\n");
            }


            theCompilerInfo 
= compilerInfo.ToString();

            
return isCompiled;
        }



    3.     执行代码

使用Reflection机制就可以很方便的执行Assembly中的代码。
我们假设编译时使用的脚本代码 this.SourceText 内容如下:

namespace test 

    
public class script 
    

        public 
static void Main() 
        

            MessageBox.Show(
"Hello"); 
        }
 
    }
 
}
 

则相应的执行代码为:

scriptEngine.Invoke("test.script""Main"null);

注意:Invoke调用的函数必须是静态的。

Invoke函数内容:

        /**//// <summary>
        
/// 执行指定的脚本函数(Method)。
        
/// 如果指定的类或模块名,以及函数(Method)、或参数不正确,将会产生VsaException/VshException例外。
        
/// </summary>
        ///
 <param name="__strModule">类或模块名</param>
        ///
 <param name="__strMethod">要执行的函数(Method)名字</param>
        ///
 <param name="__Arguments">参数(数组)</param>
        ///
 <returns>返回执行的结果</returns>

        public object Invoke(string __strModule, string __strMethod, object[] __Arguments)
        
{
            
if(!this.IsCompiled || this.theCompiledAssembly == null)
                
throw new System.Exception("脚本还没有成功编译");

            Type __ModuleType 
= this.theCompiledAssembly.GetType(__strModule);
            
if(null == __ModuleType)
                
throw new System.Exception(string.Format("指定的类或模块 ({0}) 未定义。", __strModule));

            MethodInfo __MethodInfo 
= __ModuleType.GetMethod(__strMethod);
            
if(null == __MethodInfo)
                
throw new System.Exception(string.Format("指定的方法 ({0}::{1}) 未定义。", __strModule, __strMethod));

            
try
            
{
                
return __MethodInfo.Invoke(null, __Arguments);
            }

            
catch( TargetParameterCountException )
            
{
                
throw new System.Exception(string.Format("指定的方法 ({0}:{1}) 参数错误。", __strModule, __strMethod));
            }

            
catch(System.Exception e)
            
{
                System.Diagnostics.Trace.WriteLine(
string.Format("执行({0}:{1})错误: {2}", __strModule, __strMethod, e.ToString()));
                
return null;
            }

        }


总结:

        CodeDom可以很方便的随时编译源代码,并动态执行。虽然作为脚本引擎,它没有VsaEngine正规和方便,但作为一般应用,也够用了。并且结合Reflection机制,它的功能比VsaEngine更强大:它可以编译任何提供CompilerProvider的CLR语言(目前.NET自带的语言中都有)。 
        当然,它也有一些缺点:它生成的Assembly不能动态卸载。这在一般情况下不成问题,因为一个源代码只需编译一次,并载入执行,并不需要动态卸载。 
        假如你需要做脚本编辑器时,就要考虑这个问题,因为有可能一个脚本会因为修修改改而不停的重新编译,从而造成不停的产生新的Assembly,最后将导致内存被大量占用。要解决这个问题,需要将编译器加载到独立的AppDomain中,通过卸载AppDomain达到卸载所需的Assembly的目的。

附件为完整的源代码,以供测试:
http://files.cnblogs.com/ly4cn/netScript.rar

posted @ 2005-11-03 14:52 沐枫 阅读(3435) 评论(9)  编辑 收藏 网摘 所属分类: .NET

  回复  引用  查看    
#1楼 2005-11-03 15:23 | Jason.NET      
辛苦了。去年的《程序员》上面有讲过,也很详细
  回复  引用  查看    
#2楼 [楼主]2005-11-03 15:31 | 沐枫      
晕!我去年没订程序员。
早两年就想搞一篇上来了,就是有时忙,有时忘了,有时懒了……

  回复  引用    
#3楼 2005-11-04 19:42 | hesicong [未注册用户]
不知道.net cf 2.0里面提供没有,赫赫:)
想想在PDA里面直接提供c#和vb.net编译器是多么舒服的事情。

  回复  引用  查看    
#4楼 [楼主]2005-11-04 23:20 | 沐枫      
在.NET Framework 2.0中,由于CreateCompiler方法被标记作废。为避免产生编译警告,可直接返回CodeDomProvider作为编译器:

this.theCompiler = (ICodeCompiler)theProvider;

  回复  引用  查看    
#5楼 [楼主]2005-11-04 23:25 | 沐枫      
在.NET Framework 2.0中,Microsoft.Vsa 命名空间下的所有类都被标记为作废,因此,我倒也不必整理关于VsaEngine的脚本引擎的资料了。:)
  回复  引用  查看    
#6楼 2006-02-09 19:35 | bluse      
this.theCompiler = (ICodeCompiler)theProvider;

编译警告躲过了,但是执行时候会出无法强制类型转换的错误的。

还是不要用这个ICodeCompiler最好了。


  回复  引用  查看    
#7楼 [楼主]2006-03-09 11:43 | 沐枫      
@bluse
谢谢提醒,我改在上文中了。

  回复  引用    
#8楼 2006-08-22 01:31 | 抗美援朝 [未注册用户]
一个问题,就是我用正则取得了一段html的javascript,这段js是一个函数,通过计算返回一个数,但是计算方法和初时数据是不一样的,有时是乘有时是除,但是所有计算都在这段js里,必须每次都运行一下。在c#winform中,使用这个方法好吗,还有什么好方法么。
  回复  引用  查看    
#9楼 [楼主]2006-08-22 09:16 | 沐枫      
能不能具体的说明一下呢?
假如,js的函数是一直要用的,那直接写在程序中如何呢?


发表评论



姓名 [登录] [注册] 
主页
Email (仅博主可见) 
验证码 *  验证码看不清,换一张
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论   新用户注册   返回页首      

导航: 网站首页 社区 新闻 博问 闪存 网摘 招聘 .NET频道 知识库 找找看 Google站内搜索



China-pub 计算机图书网上专卖店!6.5万品种 2-8折!
China-Pub 计算机绝版图书按需印刷服务

相关文章:

相关链接: