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.

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:

' Instance our CodeDom wrapper
Dim ep asNew cVBEvalProvider

We then call the Eval() method, passing a stringcontaining our VB.NET statements:

' Compile and run, getting the results back as an object
Dim objResult asObject = 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.

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.

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 stringcontaining 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 elsecase 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.

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 Imports, Namespaces, Classes and Function declarations. To accomplish this, I insert the dynamic user supplied code into a "Code Sandwich" of goodness.

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.

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.

Comments and Discussions

THANK YOU for the wonderful example!
I've been researching how to do this and the one thing I can't find out is how I can expose objects from my host to the script being compiled/executed. I see how you passed BACK something from the script back to the host - but I want to send something IN to the host, perhaps exposing a textbox on the form?

I sure hope you are still reading this - I know your post is quite old!

Is that Possible that I can create new EXE file?
I mean this is a exe file with VBScript Runner...
I type all the code in this and now On the new code I want to Generate Exe file on desktop.
(I mean Own Language compiler)

I have a large amount of data coming into a database from different sources. As I process through the data, I check the source of the data. Depending on the source, I have different VB code that needs to be processed. So I used this example to dynamically compile and run the code, to transform the data and return a result. However, each time it executes CompileAssemblyFromSource, it creates a dll in my C:\Windows\Temp directory. So for every record of data, I choose the vbcode that needs to be dynamically run, and it creates another dll in c:\windows\temp. The problem is it doesn't seem to release that dll when it's done. So if I try to delete it, I get access denied. When I stop my windows service, then I am allowed to delete it. The other problem is, since it never releases its hold on the dll, all of those pointers to the dlls seem to fill up the memory, and I eventually get a 'System.OutOfMemoryException'. Is there any way to force it to release the dll to prevent the out of memory error, and so I can have something delete them periodically? Or some way to force it to reuse the same filename for its dll.

I get an error when trying to compile and run this. The only thing I can figure out is that the compiled assembly is not running with administrative privileges. Any ideas on how to make this execute successfully?

Your post seems very interesting to me so that I want to ask a question about a problem I can't come around: how can I embed correctly an assembly file in the dll I'm compiling in the same way you show here?

What I try to do is to add the reference to the assembly in the CompilerParameters.ReferencedAssemblies but when I try to invoke by reflection a method that needs that assembly I get an exception which tells me that the assembly cannot be resolved because it is loaded only by reflection.

I have made a few tweaks to allow the inclusion of parameters to the eval'd code, but I can't get it to load a namespace from the current project (this is in ASP.NET and the namespace I'm trying to incude is a dll from VB.NET, btw).

I've added two parameters to the ep.Eval call, a function parameter string and array of parameters, which should match. These are then used by the string builder to include the parameter string in the generated function def, and by the Invoke call to include the parameter array. So you end up with something like (trivial example):

This works fine - however, if I try to include custom namespaces by adding something like sb.Append("Imports MyCustomNS" & vbCrLf) to the generated code, it complains that 'MyCustomNS' does not contain any public members, or it can't find it.

Namespace or type specified in the Imports 'whatevertheprojectis.module1' doesnt contain any public member or cannot be found. Make sure the namespace or the type is defined and contains at least one public member. Make sure the imported element name doesnt use any aliases.

Anyone got any ideas? I'm ultimately wanting to access subs/functions and variables within the main application, I can send parameters in and return them but not for the life of me "interact" with anything thats outside the on demand compiled code.

Well the first item that concerns me is your mention of "vbscript". This sample does not deal with "vbscript", but rather actual VB.NET (or C#) code. The reason I point this out is because VBScript has not been supported on Pocket IE (PIE) in the past. So "vbscript" to me seems like a bad road to follow from the start.

As to the remainder of your question: Each object / method / property in the framework is documented on the MSDN site, and at the bottom of each page there is a section indicating which versions of the framework support the function, as well as each platform. So you only need to look up the bits you are interested in, then scroll to the bottom of the documentation page, and see if it's supported on Pocket PC.

As far as executing a seperate assembly, executable, or batch, this functionallity is exposed via the framework Process object. For example, here is some sample code for running a command in a "DOS" window, (the command processor) and getting the text that would normally be sent to the standard output...

Public Class cShell

Public Shared Function GetProcessText(ByVal process As String, ByVal param As String, ByVal workingDir As String) As String
Dim p As Process = New Process
' this is the name of the process we want to execute
p.StartInfo.FileName = process
If Not (workingDir = "") Then
p.StartInfo.WorkingDirectory = workingDir
End If
p.StartInfo.Arguments = param
' need to set this to false to redirect output
p.StartInfo.UseShellExecute = False
p.StartInfo.RedirectStandardOutput = True
' i dislike cmd windows popping up...
p.StartInfo.CreateNoWindow = True
' start the process
p.Start()
' read all the output
' here we could just read line by line and display it
' in an output window
Dim output As String = p.StandardOutput.ReadToEnd
' wait for the process to terminate
p.WaitForExit()
Return output
End Function

Is it just me or doesn't VBCodeProvider ever return the CompilerError.Column? Sad becasue CSharpCodeProvider does. I see this in studio as well when looking at the build output - just the source path followed by (line number) where in cs I see source path(line, column)

Great article! I was wondering if you (or anyone else) could possibly answer a burning question that I have: Like with VBScript, is there something like an AddObject method so could I add an object (such as an instance of a class) so the properties and methods were exposed?

I implemented it both ways for a project a while back, allowing users to write scripts in c# or VB.NET. One of the tricky parts is building the "code sandwich". I used CodeDom to do the whole thing so I didn't have to write it once for each type of script I was using, but its pretty hard to do anything more complex than a namespace / class / method declaration.

Now, I'm trying to figure out a way to allow the user to debug their scripts. I've been looking at Mdbg but it looks like it requires the debugee to be in a seperate process. Anybody tried to do this, or am I insane?

That's funny that you mention that. The old VBScript thing I did actually had a "Debugger" that executed each line of script seperately, highlighting the line as it went. I used F8 (VB6 Debug Step) to execute the next line.

I really want something like that also for this little project. As soon as you write the Article, I will borrow it...

Years ago MS had several VB objects that you could license to build VB6 macros into your own application. I got an eval, but the "license" part scared me off. I believe that they exposed the "Thunder" editor as a control that you could place on a form.

How cool would that be? TextBox, RichTextBox, VisualStudioEditorBox.

As for the cheap debug I have been thinking that version 0.0.1 would work something like this:

1) parse out the compile output errors. Maybe into an object that would interface into my UI, so that:

2) Using that line numbers referenced in the error output, highlight the lines of code in the editor as referenced by the error message.

A mouse click, hover, or cursor move to a highlighted line would throw the related error message into a textbox at the bottom of the form.

If the reader wants a detailed explination regarding the CodeDom one should expect a published title like {CodeDom : A detailed explination} or {CodeDom : How To}.

I found the article VERY usefull and have implimented the code within my own application (Thanks wduros1!). The principle of posting articles on CodeProject is not to drill down to the Father of all classes. Heck then maybe while he's at it he can include a detailed explination on {System} while he's at it.

No, this articles deserves a hell of allot more than a 1.2 rating. I find it pretty discusting that you cannot objectively look at the post and give it it's worth! If you want to know how the Codedom is put together how about trying {F1}.

This could be a much better article if you'd go into a lot more detail about what your code does and how CodeDOM works. For example, the function "CompileAndRunCode" looks like it's very interesting, yet you don't have any explanation of it, or even show what the code does!

¡El diablo está en mis pantalones! ¡Mire, mire!

Real Mentats use only 100% pure, unfooled around with Sapho Juice(tm)!