Introduction
Sometimes it is useful to add some programmability to your projects, so that a user can change or add logic. This can be done with VBScript and the like, but what fun is that when .NET allows us to play with the compiler? Obviously, your compiled "script" is going to be much faster than interpreted VBScript or Jscript.
I'll show you how to compile VB.NET into an assembly programmatically, in memory, then use that code right away.
Using the code
The demo project is a simple windows application. Here in the article I'll describe how to call a static function; the included project also has example of creating an instance of an object and accessing that instance's properties and methods.
Set up your project and form
The namespaces we'll need for compiling are in System.dll, so they'll be available in a default project in Visual Studio.
Now drag some controls onto the form - you'll need a textbox for the code, a compile button, and a listbox to show your compile errors. They're called txtCode
, btnCompile
, and lbErrors
, respectively. I know, you never get compile errors, but your users might. :-)
Add some code to be compiled
For this demo I'll just put a sample class in the form when it loads. Here is the part of the class definition that I'll use in this article; the demo project has more functionality.
Public Class Sample
Public Shared Function StaticFunction(ByVal Arg As String) As String
Return Arg.ToUpper()
End Function
...
End Class
Implement the compiler
Now we get to the fun part, and it's surprisingly easy. In the compile button's click handler, the following bit of code will compile an assembly from the sample code.
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
params.TreatWarningsAsErrors = False
params.WarningLevel = 4
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
MessageBox.Show(ex.Message)
Exit Sub
End Try
That's it, we're ready to compile! First, though, I want to see any compile errors that my - ahem - user's incorrect code has generated. The CompilerResults
object gives me plenty of information, including a list of CompilerError
objects, complete with the line and character position of the error. This bit of code adds the errors to my listbox:
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
Use the compiled assembly
Now I want to do something with my compiled assembly. This is where things start to get a little tricky, and the MSDN sample code doesn't help as much. Here I'll describe how to call the the static (shared) function StaticFunction
. Sorry about the semantic confusion, I transitioned from MFC...
A member variable in the form class will hold the compiled assembly:
Private mAssembly As System.Reflection.Assembly
The assembly is retrieved from the CompilerResults
object, at the end of the btnCompile_Click
function:
...
If results.Errors.Count = 0 Then
mAssembly = results.CompiledAssembly
End If
I put a couple of text boxes on my form for the function argument and result. To call the static is called by the following code in the test button's click
handler:
Dim scriptType As Type
Dim instance As Object
Dim rslt As Object
Try
scriptType = mAssembly.GetType("Sample")
Dim args() As Object = {txtArgument.Text}
rslt = scriptType.InvokeMember("StaticFunction", _
System.Reflection.BindingFlags.InvokeMethod Or _
System.Reflection.BindingFlags.Public Or _
System.Reflection.BindingFlags.Static, _
Nothing, Nothing, args)
If Not rslt Is Nothing Then
txtResult.Text = CType(rslt, String)
End If
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
The key thing here is the InvokeMember
call. You can find the definition in MSDN, so I won't go into too much detail. The arguments are as follows:
- The first argument is the name of the function, property, or member variable we want to access.
- The second argument is a combination of bit flags that defines what we want to do (
BindingFlags.InvokeMethod
,) and what type of thing we're looking for - BindingFlags.Public
Or'd with BindingFlags.Static
, which is a function declared as Public Shared
in VB.NET. Be careful to get these flags right; if they don't accurately describe the desired function, InvokeMember
will throw a MissingMethod
exception.
- Next is a
Binder
object; this can be used to perform type conversion for arguments, among other things, but you can get by without it.
- Fourth is the target object - that is, the instance of our class. For this static function we don't need the object, so we pass a
Nothing
.
- Finally, we pass the arguments for our function as an array of objects. We can pass by value if we want; just cast the array element back to the right type after calling the function.
The demo code adds buttons for creating an instance of the Sample
class and accessing a property and method of that instance. Have fun with it!
Points of Interest
In this example I keep the assembly in a member variable, but that's not strictly necessary. If you use it to create an instance of the class you want to use, and hang onto the Type
object and your instance, you can let the assembly go out of scope.
The framework also includes CSharpCodeProvider
and JScriptCodeProvider
classes, which can be used to compile code written in those languages. The latter is in Microsoft.JScript.dll.
I think I remember reading somewhere that only the JScript compiler was implemented in the 1.0 version of the framework, and the MSDN documentation of these classes says "Syntax based on .NET Framework version 1.1." However, I had no trouble dropping this code into a VS 2002 project and running it. If anyone has a problem doing that or can clarify what the differences are between the two framework versions, it would be nice to note these in a revision to this article.
History
- 2003.11.19 - Submitted to CodeProject