65.9K
CodeProject 正在变化。 阅读更多。
Home

动态编译 .NET 代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (55投票s)

2003年11月20日

4分钟阅读

viewsIcon

333285

downloadIcon

2672

以编程方式在内存中编译 .NET 代码,然后使用生成的程序集来实例化一个类的对象、访问其属性和方法,以及调用一个静态函数。

引言

有时,为项目添加一些可编程性会很有用,这样用户就可以更改或添加逻辑。这可以通过 VBScript 等语言来实现,但当 .NET 允许我们玩转编译器时,那还有什么乐趣呢?显然,您编译的“脚本”会比解释型的 VBScript 或 Jscript 快得多。

我将向您展示如何以编程方式在内存中编译 VB.NET 代码到一个程序集中,然后立即使用该代码。

使用代码

演示项目是一个简单的 Windows 应用程序。在这里的文章中,我将描述如何调用一个静态函数;包含的项目还提供了创建对象实例以及访问该实例的属性和方法的示例。

设置您的项目和窗体

我们编译所需的命名空间位于 System.dll 中,因此在 Visual Studio 的默认项目中将可用。

现在,将一些控件拖到窗体上 - 您将需要一个用于代码的文本框、一个编译按钮和一个用于显示编译错误的列表框。它们分别被命名为 txtCodebtnCompilelbErrors。我知道,您永远不会出现编译错误,但您的用户可能会。:-)

添加一些要编译的代码

对于这个演示,我将在窗体加载时放入一个示例类。以下是我将在本文中使用的类的定义部分;演示项目具有更多功能。

Public Class Sample
    Public Shared Function StaticFunction(ByVal Arg As String) As String
        Return Arg.ToUpper()
    End Function
    
    ...
    
End Class

实现编译器

现在我们来做有趣的部分,而且它非常简单。在编译按钮的点击处理程序中,以下这段代码将从示例代码中编译一个程序集。

Dim provider As Microsoft.VisualBasic.VBCodeProvider
Dim compiler As System.CodeDom.Compiler.ICodeCompiler
Dim params As System.CodeDom.Compiler.CompilerParameters
Dim results As System.CodeDom.Compiler.CompilerResults

params = New System.CodeDom.Compiler.CompilerParameters
params.GenerateInMemory = True      'Assembly is created in memory
params.TreatWarningsAsErrors = False
params.WarningLevel = 4
'Put any references you need here - even you own dll's, if you want to use one
Dim refs() As String = {"System.dll", "Microsoft.VisualBasic.dll"}
params.ReferencedAssemblies.AddRange(refs)

Try
    provider = New Microsoft.VisualBasic.VBCodeProvider
    compiler = provider.CreateCompiler
    results = compiler.CompileAssemblyFromSource(params, txtCode.Text)
Catch ex As Exception
    'Compile errors don't throw exceptions; you've got some deeper problem...
    MessageBox.Show(ex.Message)
    Exit Sub
End Try

好了,我们准备编译了!不过,首先,我想看到我——嗯——用户不正确的代码生成的任何编译错误。CompilerResults 对象为我提供了大量信息,包括一个 CompilerError 对象列表,其中包含错误的行和字符位置。这段代码会将错误添加到我的列表框中。

lbErrors.Items.Clear()

Dim err As System.CodeDom.Compiler.CompilerError
For Each err In results.Errors
    lbErrors.Items.Add(String.Format( _
        "Line {0}, Col {1}: Error {2} - {3}", _
        err.Line, err.Column, err.ErrorNumber, err.ErrorText))
Next

使用编译后的程序集

现在我想用我的编译后的程序集做些事情。这是事情开始变得有点棘手的地方,而 MSDN 的示例代码并没有提供太多帮助。在这里,我将描述如何调用静态(共享)函数 StaticFunction。抱歉造成语义混淆,我从 MFC 过来的……

窗体类中的一个成员变量将保存编译后的程序集。

Private mAssembly As System.Reflection.Assembly

btnCompile_Click 函数的末尾,从 CompilerResults 对象中检索程序集。

...        

If results.Errors.Count = 0 Then        'No compile errors or warnings...
    mAssembly = results.CompiledAssembly
End If

我在窗体上放了几个文本框用于函数参数和结果。通过测试按钮的 click 处理程序中的以下代码调用静态函数。

Dim scriptType As Type
Dim instance As Object
Dim rslt As Object

Try
    'Get the type from the assembly.  This will allow us access to
    'all the properties and methods.
    scriptType = mAssembly.GetType("Sample")

    'Set up an array of objects to pass as arguments.
    Dim args() As Object = {txtArgument.Text}

    'And call the static function
    rslt = scriptType.InvokeMember("StaticFunction", _
        System.Reflection.BindingFlags.InvokeMethod Or _
        System.Reflection.BindingFlags.Public Or _
        System.Reflection.BindingFlags.Static, _
        Nothing, Nothing, args)

    'Return value is an object, cast it back to a string and display
    If Not rslt Is Nothing Then
        txtResult.Text = CType(rslt, String)
    End If
Catch ex As Exception
    MessageBox.Show(ex.Message)
End Try

这里关键的是 InvokeMember 调用。您可以在 MSDN 上找到其定义,因此我不会详细介绍。参数如下:

  1. 第一个参数是我们想要访问的函数、属性或成员变量的名称。
  2. 第二个参数是标志的组合,用于定义我们要执行的操作(BindingFlags.InvokeMethod),以及我们要查找的对象的类型——BindingFlags.PublicBindingFlags.Static,它对应于 VB.NET 中声明为 Public Shared 的函数。务必正确设置这些标志;如果它们不能准确描述所需的函数,InvokeMember 将抛出 MissingMethod 异常。
  3. 接下来是一个 Binder 对象;它可以用于执行参数的类型转换等操作,但如果没有它也可以工作。
  4. 第四个是目标对象——即我们类的实例。对于这个静态函数,我们不需要对象,所以传递 Nothing
  5. 最后,我们将函数的参数作为对象数组传递。如果需要,我们可以按值传递;调用函数后,只需将数组元素转换回正确的类型即可。

演示代码添加了创建 Sample 类实例以及访问该实例的属性和方法的按钮。尽情享受吧!

关注点

在这个例子中,我将程序集保存在一个成员变量中,但这并非绝对必要。如果您使用它来创建一个您想使用的类的实例,并保留 Type 对象和您的实例,那么您可以让程序集超出作用域。

该框架还包含 CSharpCodeProviderJScriptCodeProvider 类,可用于编译用这些语言编写的代码。后者位于 Microsoft.JScript.dll 中。

我记得好像在哪里读过,在框架的 1.0 版本中只实现了 JScript 编译器,而且这些类的 MSDN 文档说“基于 .NET Framework 1.1 版本的语法。” 不过,我毫无困难地将这段代码放入了一个 VS 2002 项目并运行了它。如果有人在执行此操作时遇到问题,或者能够说明两个框架版本之间的区别,那么在本文的修订版中注明这些会很有帮助。

历史

  • 2003.11.19 - 提交至 CodeProject
© . All rights reserved.