Not Another "Script Runner" Article!
The major differences between this article and others that I have seen presented here are the target audience and the usage scenario. You will notice in the screen shot displayed above that there is not a function declaration. No namespace
. No class
, or include
statements. The target audience for this technology is a diverse collection of support personnel with a minimal exposure to programming. If there was any exposure to programming, it would be in the form of DOS batch files, and maybe VBScript.
These are people who would prefer not to write scripts; no matter how easy it was. But they do often modify existing batch files and configuration files, either while setting up a new system, or fixing a support issue. The design goal was to offer "VBScript" simplicity but with "VB.NET" error handling.
Introduction
This is a small sample demonstration of late compile. Often the CodeDom aspect of the .NET Framework is discussed in relation to the ability to generate code. By using the CodeDom as a glorified collection of code bits, you can Add(new codebit) until your program is complete, then serialize the resultant code out to a file, or compile and run.
Although the code generation abilities are very interesting and powerful, this article is primarily focused on the ability of the CodeDom assembly to compile dynamic code to an assembly that exists in memory. An instance of the assembly is then created, and the method containing our dynamic code is executed.
More than 10 years ago, I wrote a VB6 application that ran VBScript macros. Last year, I was again presented with the need to allow dynamic run time code execution. This time, instead of using VBScript, I decided to use actual VB.NET code. Sorry it took me so long to post...
Out With the Old, In With the New...
The code presented here is a portion of that solution, upgraded to use the 2.0 Framework. During the upgrade, I noticed that Microsoft had marked the ICodeCompiler
interface obsolete, directing me to instead use methods directly from the CodeProvider
object. I have simply commented out the CreateCompiler()
function. Those wishing to use this code in the 1.1 Framework should be able to uncomment the code line instancing and calling the interface.
Dim c As VBCodeProvider = New VBCodeProvider
Calling the Compiler at Run Time
Using the cEvalProvider
class is as easy as creating a string builder, appending VB.NET code to the string builder, and then passing the string builder to the CompileAndRunCode()
function.
Dim oRetVal As Object = CompileAndRunCode(Me.RichTextBox1.Text)
MsgBox(oRetVal)
In the case of the code snippet above, the result looks like this:
The CompileAndRunCode() Function
The CompileAndRunCode()
function is a wrapper around the cVBEvalProvider
object, which is itself a wrapper around the Microsoft.VisualBasic.VBCodeProvider
object, which is a .... well you get the idea. One day, I am going to write the ultimate function: DoIt()
, which will wrap anything and do everything, or perhaps it's the other way around: "Somewhere, deep in the Silicon, there is a single tiny function..."
We start out by creating an instance of our VB Eval Provider class:
Dim ep as New cVBEvalProvider
We then call the Eval()
method, passing a string
containing our VB.NET statements:
Dim objResult as Object = ep.Eval(VBCodeToExecute)
The VB code provider object returns all the compile time errors in a CompilerResults
object, and my wrapper class exposes this collection of errors through the CompilerErrors
property. So the next thing to do is check and see if the count of CompilerErrors
is zero or not. If we have errors present, then write a few lines of information to the debug output, and exit the function.
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"
End If
Note: This is not really a good way to bubble the error state back to the client, since the function that is compiled and executed in real-time may return the string
"ERROR
" as a normal occurrence. But this is just a quick and dirty example.
If we have no compile errors, we can expect the return result of our dynamically compiled and executed code to be in the objResult
object.
Due to the open ended nature of run-time compilation, I return the results as a weakly typed object, then show an example of converting to a more strongly typed object by using the GetType()
method. In my sample code, the assumption is that the return type is a System.String
, although more complex types can be handled quite easily.
Dim t As Type = objResult.GetType()
If t.ToString() = "System.String" Then
sReturn_DataType = t.ToString
sReturn_Value = Convert.ToString(objResult)
Else
End If
The cVBEvalProvider Object
The cVBEvalProvider
object has a simple model. It exposes one property, CompilerErrors
which is a collection of compile time errors generated during the Eval()
method. The Eval()
method is passed a string
containing VB.NET code to be executed or "evaluated", and returns a generic Object
. The New()
method simply instances the internal CompilerErrorCollection
member variable.
The real meat of the work occurs in the Eval()
method, which creates an instance of the VBCodeProvider
object. Although the VBCodeProvider
object inherits from System.CodeDom.Compiler.CodeDomProvider
, don't bother looking for it inside the CodeDom namespace
, it is actually a member of the Microsoft.VisualBasic
assembly.
Note: This seems obvious to me, but I am going to mention it anyway. If VB.NET is not your thing, there is a Microsoft.CSharp.CSharpCodeProvider
class, which also descends from the System.CodeDom.Compiler.CodeDomProvider
object. The first step in switching languages would be to switch these objects. Or I could see an else
case that instanced the appropriate run-time compiler, based on some switch
, along the lines of WScript.
The Eval() Method
The Eval()
method creates an instance of the VBCodeProvider
, and variables for the CompilerParameters
and CompilerResults
. Also of interest, a variable of MethodInfo
is created. The MethodInfo
object exposes an Invoke()
, which performs the actual execution of our code.
Dim oCodeProvider As VBCodeProvider = New VBCodeProvider
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
As noted earlier, with the 1.1 Framework model, you created an instance of the compiler via the CreateCompiler()
method of the VBCodeProvider
. The CreateCompiler()
method returned an ICodeCompiler
. The only call made against the ICodeCompiler
object was a call to CompileAssemblyFromSource()
. In the 2.0 Framework, you can directly call CompileAssemblyFromSource()
from the VBCodeProvider
. I have left the code inline and commented it out for those who may wish to use this with the 1.1 Framework.
Compiler Parameters
The CompileAssemblyFromSource()
method takes an object of CompilerParameter
as the first argument. So our first order of business will be to setup the CompilerParameters
that we intend to use.
The ReferencedAssemblies Collection
In our code, we add references to any required assemblies. As an exercise to make the cVBEvalProvider
object more flexible, we could have exposed the CompilerParameters
as a property, or offered an optional parameter to allow the calling process to pass them to us, using defaults if the caller did not pass anything.
The CompilerOptions Property
The CompilerOptions
property is where we can specify additional command line arguments for the compiler. Here we are using the /t:library switch
, which tells the compiler the Target output type is a library (*.dll) file. There are four target types, {exe, library, module, winexe}.
I hesitate to include this link, because it seems like Microsoft moves things around every six to twelve months... but here are the Compiler Options for the Visual Basic.NET compiler in the MSDN collection. Your mileage may vary.
The GenerateInMemory Property
Finally, we set the GenerateInMemory
property of the CompilerParameters
object to true
, so that on a successful compilation, the generated assembly will be returned in the CompilerResults.CompiledAssembly
property.
Note: As an alternative, an output filename could be specified using the /out:filename command line argument, and the assembly could be loaded/instanced from filename instead of the CompilerResults.CompiledAssembly
property.
Building the Code Sandwich
Code sandwich? OK, it's a cheesy attempt to get your attention. Plus I feel like I have overused the word "wrapper" in this article. One of the design decisions that I made was to insulate the end-user from stuff like Import
s, Namespace
s, Class
es and Function
declarations. To accomplish this, I insert the dynamic user supplied code into a "Code Sandwich" of goodness.
Dim sb As StringBuilder = New StringBuilder("")
sb.Append("Imports System" & vbCrLf)
sb.Append("Imports System.Xml" & vbCrLf)
sb.Append("Imports System.Data" & vbCrLf)
sb.Append("Namespace dValuate" & vbCrLf)
sb.Append("Class EvalRunTime " & vbCrLf)
sb.Append("Public Function EvaluateIt() As Object " & vbCrLf)
sb.Append(vbCode & vbCrLf)
sb.Append("End Function " & vbCrLf)
sb.Append("End Class " & vbCrLf)
sb.Append("End Namespace" & vbCrLf)
CompileAssemblyFromSource()
The call to CompileAssemblyFromSource()
passes our CompilerParameters
object, and our code to be compiled. If errors are encountered, the CompilerResults.Errors
property will have a count greater than 0
, and can be iterated through.
oCResults = oCodeProvider.CompileAssemblyFromSource(oCParams, sb.ToString)
If oCResults.Errors.Count <> 0 Then
Me.CompilerErrors = oCResults.Errors
Throw New Exception("Compile Errors")
Else
oAssy = oCResults.CompiledAssembly
oExecInstance = oAssy.CreateInstance("dValuate.EvalRunTime")
oType = oExecInstance.GetType
oMethodInfo = oType.GetMethod("EvaluateIt")
oRetObj = oMethodInfo.Invoke(oExecInstance, Nothing)
Return oRetObj
End If
If no errors are encountered, then the assembly produced from our code can be found in the CompiledAssembly
property, and manipulated like any other assembly. I create an instance of my wrapper class (Sandwich) by using the CreateInstance("dValuate.EvalRunTime")
method.
It's a two-step dance to get the actual method. We have to GetType()
the assembly instance, then GetMethod("EvaluateIt")
to get the MethodInfo
object. Once we have the MethodInfo
object, we can call the Invoke()
method to actually "run" the code. Return values are of type Object
, which is then bubbled up to the calling process.
Final Thoughts
Comparing this solution to the one that I wrote using VBScript raises the obvious points.
Compile time error handling is one of the strengths of this implementation. In addition, I believe that the runtime error reporting would also be greatly improved.
In general, the error handling and reporting of the .NET languages is far superior to earlier efforts from Microsoft. Anyone who has had a user point to a COM "-214xxxx" error dialog box knows how weak error reporting can make diagnosing sporadic run-time errors next to impossible. Since I live in the Dallas area, which uses the 214 area code, I have had users (more than once) tell me that there was an error "with a phone number on it." and ask me if they were supposed to call the number.
Revision History
Date |
Author |
Comment |
1-27-2006 |
rwd |
Original |
1-30-2006 |
rwd |
Updated per reader comment. Added a RichTextBox control to the main form to enter the dynamic code. Also added a design position statement after looking at similar articles. |
|
|
|