使用 CodeDom 编译和运行 VB.NET 代码
演示使用 CodeDom 进行 VB.NET 代码的“运行时”编译和执行

不是又一篇“脚本运行器”文章!
本文与其他文章的主要区别在于目标受众和使用场景。您会注意到,在上面显示的屏幕截图中,没有函数声明。没有 namespace
。没有 class
或 include
语句。此技术的目标受众是接触编程经验极少但技能多样的支持人员。如果他们有过编程经验,那也只是 DOS 批处理文件,也许还有 VBScript。
这些人宁愿不编写脚本;无论脚本有多么容易。但他们经常在设置新系统或修复支持问题时修改现有的批处理文件和配置文件。设计目标是提供“VBScript”的简洁性,但具备“VB.NET”的错误处理能力。
引言
这是一个简短的延迟编译演示。通常,.NET Framework 的 CodeDom 方面会与代码生成能力相关联。通过将 CodeDom 用作一个功能强大的代码片段集合,您可以 Add(new codebit) 直到您的程序完成,然后将生成的代码序列化到文件中,或者进行编译和运行。
尽管代码生成功能非常有趣且强大,但本文主要关注 CodeDom 程序集将动态代码编译为存在于内存中的程序集的能力。然后创建一个程序集实例,并执行包含动态代码的方法。
十多年前,我写了一个运行 VBScript 宏的 VB6 应用程序。去年,我再次面临需要允许动态运行时代码执行的需求。这次,我没有使用 VBScript,而是决定使用实际的 VB.NET 代码。抱歉这么晚才发布……
淘汰旧的,迎接新的……
此处提供的代码是该解决方案的一部分,已升级为使用 2.0 Framework。在升级过程中,我注意到 Microsoft 已将 ICodeCompiler
接口标记为过时,指示我改为直接使用 CodeProvider
对象的方法。我已将 CreateCompiler()
函数注释掉了。希望在 1.1 Framework 中使用此代码的人可以取消注释创建实例并调用接口的代码行。
Dim c As VBCodeProvider = New VBCodeProvider
' Obsolete in 2.0 framework
' Dim icc As ICodeCompiler = c.CreateCompiler
在运行时调用编译器
使用 cEvalProvider
类非常简单:创建一个字符串生成器,将 VB.NET 代码追加到字符串生成器中,然后将字符串生成器传递给 CompileAndRunCode()
函数。
Dim oRetVal As Object = CompileAndRunCode(Me.RichTextBox1.Text)
MsgBox(oRetVal)
在上方的代码片段中,结果如下所示

CompileAndRunCode() 函数
CompileAndRunCode()
函数是对 cVBEvalProvider
对象的包装,它本身是对 Microsoft.VisualBasic.VBCodeProvider
对象的包装,而这个对象又是……您懂的。总有一天,我会写出终极函数:DoIt()
,它将包装一切并做所有事情,或者反过来:“在硅的深处,有一个微小的函数……”
我们首先创建一个我们的 VB Eval Provider 类的实例
' Instance our CodeDom wrapper
Dim ep as New cVBEvalProvider
然后我们调用 Eval()
方法,传入一个包含我们的 VB.NET 语句的 string
' Compile and run, getting the results back as an object
Dim objResult as Object = ep.Eval(VBCodeToExecute)
VB 代码提供程序对象在 CompilerResults
对象中返回所有编译时错误,我的包装类通过 CompilerErrors
属性公开此错误集合。因此,下一步是检查 CompilerErrors
的计数是否为零。如果我们有错误,则向调试输出写入几行信息,然后退出函数。
If ep.CompilerErrors.Count <> 0 Then
Diagnostics.Debug.WriteLine("CompileAndRunCode: Compile Error Count = _
" & ep.CompilerErrors.Count)
Diagnostics.Debug.WriteLine(ep.CompilerErrors.Item(0))
Return "ERROR" ' Forget it
End If
注意:这并不是将错误状态冒泡回客户端的好方法,因为在实时编译和执行的函数可能会正常返回 string
“ERROR
”。但这只是一个快速而粗糙的例子。
如果我们没有编译错误,我们可以预期我们的动态编译和执行代码的返回结果在 objResult
对象中。
由于运行时编译的开放式性质,我将结果作为弱类型对象返回,然后展示一个使用 GetType()
方法转换为更强类型对象的示例。在我的示例代码中,假设返回类型是 System.String
,尽管更复杂的类型也可以轻松处理。
Dim t As Type = objResult.GetType()
If t.ToString() = "System.String" Then
sReturn_DataType = t.ToString
sReturn_Value = Convert.ToString(objResult)
Else
' Some other type of data - not really handled at
' this point. rwd
'ToDo: Add handlers for other data return types, if needed
' Here is an example to handle a dataset...
'Dim ds As DataSet = DirectCast(objResult, DataSet)
'DataGrid1.Visible = True
'DataGrid1.DataSource = ds.Tables(0)
End If
cVBEvalProvider 对象

cVBEvalProvider
对象有一个简单的模型。它公开一个属性 CompilerErrors
,这是一个在 Eval()
方法期间生成的编译时错误的集合。Eval()
方法接收一个包含要执行或“评估”的 VB.NET 代码的 string
,并返回一个通用的 Object
。New()
方法只是实例化内部的 CompilerErrorCollection
成员变量。
实际工作发生在 Eval()
方法中,该方法会创建 VBCodeProvider
对象的一个实例。尽管 VBCodeProvider
对象继承自 System.CodeDom.Compiler.CodeDomProvider
,但不要在 CodeDom namespace
中寻找它,它实际上是 Microsoft.VisualBasic
程序集的一个成员。
注意:对我来说这似乎很明显,但无论如何我还是要提一下。如果 VB.NET 不是您喜欢的语言,那么有一个 Microsoft.CSharp.CSharpCodeProvider
类,它也继承自 System.CodeDom.Compiler.CodeDomProvider
对象。切换语言的第一步是切换这些对象。或者,我可以看到一个 else
情况,该情况会根据某种 switch
(类似于 WScript)实例化适当的运行时编译器。
Eval() 方法
Eval()
方法会创建 VBCodeProvider
的一个实例,以及 CompilerParameters
和 CompilerResults
的变量。同样值得注意的是,MethodInfo
类型的一个变量被创建。MethodInfo
对象公开一个 Invoke()
,它执行我们代码的实际执行。
Dim oCodeProvider As VBCodeProvider = New VBCodeProvider
' Obsolete in 2.0 framework
' Dim oICCompiler As ICodeCompiler = oCodeProvider.CreateCompiler
Dim oCParams As CompilerParameters = New CompilerParameters
Dim oCResults As CompilerResults
Dim oAssy As System.Reflection.Assembly
Dim oExecInstance As Object = Nothing
Dim oRetObj As Object = Nothing
Dim oMethodInfo As MethodInfo
Dim oType As Type
如前所述,在 1.1 Framework 模型中,您通过 VBCodeProvider
的 CreateCompiler()
方法创建编译器实例。CreateCompiler()
方法返回一个 ICodeCompiler
。对 ICodeCompiler
对象进行的唯一调用是调用 CompileAssemblyFromSource()
。在 2.0 Framework 中,您可以直接从 VBCodeProvider
调用 CompileAssemblyFromSource()
。我已将代码内联并注释掉了,以便那些可能希望将其与 1.1 Framework 一起使用的人。
编译器参数
CompileAssemblyFromSource()
方法将 CompilerParameter
对象作为第一个参数。因此,我们的首要任务是设置我们打算使用的 CompilerParameters
。
ReferencedAssemblies Collection (引用的程序集集合)
在我们的代码中,我们添加了对任何必需程序集的引用。作为使 cVBEvalProvider
对象更灵活的练习,我们可以将 CompilerParameters
公开为一个属性,或者提供一个可选参数,允许调用进程将它们传递给我们,并在调用者未传递任何内容时使用默认值。
CompilerOptions 属性
CompilerOptions
属性是我们可以在其中指定附加命令行参数给编译器的地方。在这里,我们使用了 /t:library switch
,它告诉编译器目标输出类型是库(*.dll)文件。有四种目标类型 {exe, library, module, winexe}。
我犹豫是否要包含此链接,因为 Microsoft 似乎每六到十二个月就会更改一次内容……但 此处是 MSDN 集合中 Visual Basic.NET 编译器的编译器选项。您的使用情况可能会有所不同。
GenerateInMemory 属性
最后,我们将 CompilerParameters
对象的 GenerateInMemory
属性设置为 true
,以便在成功编译后,生成的程序集将包含在 CompilerResults.CompiledAssembly
属性中。
注意:作为替代方案,可以使用 /out:filename 命令行参数指定输出文件名,并且可以从文件名加载/实例化程序集,而不是使用 CompilerResults.CompiledAssembly
属性。
构建代码“三明治”
代码“三明治”?好吧,这是我试图吸引您注意力的俗气尝试。另外,我觉得我在本文中过度使用了“包装器”这个词。我做出的一个设计决定是让最终用户免受 Import
s、Namespace
s、Class
es 和 Function
声明等事物的困扰。为了实现这一点,我将用户动态提供的代码插入一个“代码三明治”中。
' Generate the Code Framework
Dim sb As StringBuilder = New StringBuilder("")
sb.Append("Imports System" & vbCrLf)
sb.Append("Imports System.Xml" & vbCrLf)
sb.Append("Imports System.Data" & vbCrLf)
' Build a little wrapper code, with our passed in code in the middle
sb.Append("Namespace dValuate" & vbCrLf)
sb.Append("Class EvalRunTime " & vbCrLf)
sb.Append("Public Function EvaluateIt() As Object " & vbCrLf)
' Insert our dynamic code
sb.Append(vbCode & vbCrLf)
sb.Append("End Function " & vbCrLf)
sb.Append("End Class " & vbCrLf)
sb.Append("End Namespace" & vbCrLf)
CompileAssemblyFromSource()
调用 CompileAssemblyFromSource()
会传递我们的 CompilerParameters
对象以及我们要编译的代码。如果遇到错误,CompilerResults.Errors
属性的计数将大于 0
,并且可以对其进行迭代。
' Compile and get results
' 2.0 Framework - Method called from Code Provider
oCResults = oCodeProvider.CompileAssemblyFromSource(oCParams, sb.ToString)
' 1.1 Framework - Method called from CodeCompiler Interface
' cr = oICCompiler.CompileAssemblyFromSource (cp, sb.ToString)
' Check for compile time errors
If oCResults.Errors.Count <> 0 Then
Me.CompilerErrors = oCResults.Errors
Throw New Exception("Compile Errors")
Else
' No Errors On Compile, so continue to process...
oAssy = oCResults.CompiledAssembly
oExecInstance = oAssy.CreateInstance("dValuate.EvalRunTime")
oType = oExecInstance.GetType
oMethodInfo = oType.GetMethod("EvaluateIt")
oRetObj = oMethodInfo.Invoke(oExecInstance, Nothing)
Return oRetObj
End If
如果没有遇到错误,那么从我们的代码生成的程序集可以在 CompiledAssembly
属性中找到,并像其他任何程序集一样进行操作。我通过使用 CreateInstance("dValuate.EvalRunTime")
方法来创建我的包装器类(Sandwich)的一个实例。
要获取实际方法,需要执行一个两步操作:我们必须对程序集实例执行 GetType()
,然后执行 GetMethod("EvaluateIt")
来获取 MethodInfo
对象。一旦我们有了 MethodInfo
对象,就可以调用 Invoke()
方法来实际“运行”代码。返回值是 Object
类型,然后会被冒泡到调用进程。
最终想法
将此解决方案与我使用 VBScript 编写的解决方案进行比较,可以得出显而易见的几点。
编译时错误处理是此实现的一个优势。此外,我认为运行时错误报告也会得到极大的改进。
总的来说,.NET 语言的错误处理和报告远优于 Microsoft 早期的一些努力。任何有过用户指向 COM “-214xxxx”错误对话框的人都知道,弱错误报告如何使诊断偶发性运行时错误变得几乎不可能。由于我住在达拉斯地区,该地区使用 214 区号,所以有过用户(不止一次)告诉我有一个错误“带有一个电话号码”,并问我是否应该打电话。
修订历史
日期 | 作者 | 注释 |
1-27-2006 | rwd | Original |
1-30-2006 | rwd | 根据读者评论更新。在主窗体中添加了一个 RichTextBox 控件来输入动态代码。在查看了类似的文章后,还添加了一个设计定位说明。 |