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
| You must Sign In to use this message board. |
|
|
 |
|
 |
Great article, thanks.
One problem I have with a console (formless) application is how to access a method outside the compiled class, eg Module1.FunctionA() or ClassXYZ.FunctionB(), all of which are declared Public.
I'm getting the error BC30451, Name 'FunctionA' is not declared when I try to call it from the compiled function. The same goes if I prefix the call with ConsoleApplication1, Module1 or ClassXYZ. They're all unrecognized.
Is there a way I can get the thing to - compile inside the Module1 namespace - or import, reference, or pass in args() the module, the class or at least a sub/function?
My code looks like:
Public Module Module1 Public Function A End Function ... Public Class XYZ Public Function B End Function Sub Whatever CompileAndRun() End Sub ... End Class End Module
Project options shows: Assembly name is server Root namespace is ConsoleApplication1 Startup object is ConsoleApplication1.Module1
Rather than using args() As String, I'm using As Object, so I should be able to pass anything to the compiled function.
Solved
At Mike's Code Blog, here, he writes: "tell the compiler what extra assemblies we want to reference. The most general method I've found so far is simply to reference all of the assemblies that our parent program references, as well as the parent assembly itself":
Dim executingAssembly As System.Reflection.Assembly executingAssembly = System.Reflection.Assembly.GetExecutingAssembly() params.ReferencedAssemblies.Add(executingAssembly.Location) for each assemblyName As System.Reflection.AssemblyName _ in executingAssembly.GetReferencedAssemblies() params.ReferencedAssemblies.Add(System.Reflection.Assembly.Load(assemblyName).Location) Next
However, the parent program assembly 'server' has *not* been added, so add it here (path will need changing):
params.ReferencedAssemblies.Add(System.Reflection.Assembly.LoadFile _ ("c:\MGServer\server\bin\debug\server.exe").Location)
Many thanks, Eric T
modified on Tuesday, October 6, 2009 5:41 PM
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I am working on project for a company and i will like to compile the program and send it to the company to install. please how to go about it.
usman
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Following your excellent example I created an application which successfully performs functions on a dynamically compiled assembly. The compiled assembly also calls back to the Entry assembly to execute shared functions. However I cannot get it to access a windows forms control on the Entry assembly. So I tried creating an unshared function on the Entry assembly and called it back from the dynamic assembly.
This appeared to work inasmuch as I could single step through the called function in the Entry assembly and it appeared to add an entry to a list box control and to append some text to a textbox. But on return Nothing was visible in either control! I tried adding a me.refresh but that didn't help either
Any ideas?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
It's hard to imagine without seeing the code, but I doubt it has anything to do with the dynamic compilation if you've got to the point where you can step into the entry assembly.
Two ideas off the top of my head: 1.) Could it be a threading issue? Controls must be modified on the UI tread. 2.) Maybe you inadvertantly created a second instance of the form class, and you're modifying the controls on that - in which case you wouldn't see changes on the "real" form.
Negative Ghostrider - the pattern is full.
|
| Sign In·View Thread·PermaLink | 5.00/5 |
|
|
|
 |
|
 |
Thanks Jim.
Yes you're right. I "created a second instance of the form".
My textBox access code was in the Form. My 'invokmemember' of the method updating the textBox was as an instance. i.e. I created an instance of the Form. I did that because I couldn't use a shared method to access a form control.
So I changed my 'invokemember' to static. I moved my textBox access code into a module. I got the module class and the static method and I updated the form control from the module.
I'm sure there's a simpler method.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi Jim,
Great article.
I would like to pass an array to the StaticMethod, so I tried changing the StaticMethod like this:
Public Shared Function StaticMethod(ByVal Arg() As String) As String
Doing this give a "Missing Method Exception"
Any ideas?
Thank you, Tom
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Invoking the method is tricky; if any of your arguments are wrong, it won't work. Check that you're using the right name (my method is called StaticFunction,) and that the public and shared (static) are compatible with your Invoke call.
The argument change from a string to an array of strings shouldn't have any affect on it, if all other things are the same.
----------------------- Negative Ghostrider - the pattern is full.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Jim,
I just migrated your project to VS2005 without any problems and successfully integrate your sample code into an application that generates custom lot numbers for batch production.
Thanks for the wonderfully comprehensive example...it has saved me ooodles of time. 
Scott
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
 |
|
|
 |
|
 |
I am developing a web application in which i am using This "SYSTEM.CODEDOM.COMPILER" namespace which provide with the Csharpprovider to compile the c#program on the fly...it is working fine.....
Problem: Since this is a web application,if we try to access the application through the browser(INTERNET EXPLOER)and compile the program, it gives me an error saying that access is denied for the file c:\windows\system32\*.tmp.
plsssssssss help me out with this........
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
 | hi  nbcbm | 0:01 23 Jan '06 |
|
|
 |
 | Re: hi  Jim Rogers | 5:32 23 Jan '06 |
|
|
 |
|
 |
thank u i've looked at it but the prob that i wana to make a win32 program not a dos program so plz can you help me???
like in the sub7 you can edit the server and you can change some things in it so how can i do that plz look the only thing i wana to do is to change change the value of string in the program so can i edit the compiled program?????? plz help me
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
 | Re: hi  Jim Rogers | 9:57 24 Jan '06 |
|
 |
a.) Stop writing like a script kiddie, it's not clear what you're talking about.
b.) You can't edit an already-compiled program with this technique, if that's what you're trying to do.
c.) There is no DOS here; whether you have a console or windows application depends on what framework classes you use.
d.) If you're looking for help trying to hack something, don't look here.
|
| Sign In·View Thread·PermaLink | 5.00/5 |
|
|
|
 |
|
 |
Jim Rogers wrote: c.) There is no DOS here; whether you have a console or windows application depends on what framework classes you use.
yep here is the prob i wana to compile windows application not console
Jim Rogers wrote: a.) Stop writing like a script kiddie, it's not clear what you're talking about.
am sorry if i said something not clear cuz my english is not very good
Jim Rogers wrote: d.) If you're looking for help trying to hack something, don't look here.
noooo i sayid (sub7) cuz it was the only example i know so am sorry
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
 |
Hi. Nice article dude!
I'm just wondering if you (i) can do something similar for a pocket pc device.
ie Use a desktop component to generate some form code which can be uploaded to the pocket pc and then 'loaded'. The method i am current using is to build a 'form description file' which is interpretted by the pda at runtime and used to generate a form on the fly.
If this question is not up your alley, perhaps one of your neighbours might know?!
Cheers, James.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
How do i write a common procedure in vb.net or asp.net so that i can use it across the project. I dont want to creat a class and instantiate it.
thanks in advance
I have been working in VB6 for quiet some time and latetly ventured into VB.Net and ASP.Net
|
| Sign In·View Thread·PermaLink | 1.00/5 |
|
|
|
 |
|
|
 |
|
 |
That is an interesting thread.
Use AppDomain.CreateInstanceAndUnwrap to create an instance in a new domain; set the permissions for that domain however you want.
This could be very handy if you're implementing in a scenario where questionable users might be able to access your scripts, or if you just want to prevent future developers from misusing your script engine.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi Jim:
Please help!
I read your article here. I have a simliar task. I am building a web application which can do scheduled build for CS or VB projects. CS project is building fine, but my VB projects build alwasy fails. It seems can't find the references of the .net frame work. http://www.codeproject.com/vb/net/DotNetCompilerArticle.asp#xx671845xx
Below is my code: the build error is very clear that the references were not taken into account.
build errors: ================ BUILD FAILED FOR JackTestVB PRIMARY ERROR MESSAGE - D:\BuildManager\JACKVB\JackTestVB\Global.asax.vb(31,0) : error BC30002: Type 'EventArgs' is not defined. D:\BuildManager\JACKVB\JackTestVB\Global.asax.vb(35,0) : error BC30002: Type 'EventArgs' is not defined. D:\BuildManager\JACKVB\JackTestVB\Global.asax.vb(39,0) : error BC30002: Type 'EventArgs' is not defined. D:\BuildManager\JACKVB\JackTestVB\Global.asax.vb(43,0) : error BC30002: Type 'EventArgs' is not defined. D:\BuildManager\JACKVB\JackTestVB\Global.asax.vb(47,0) : error BC30002: Type 'EventArgs' is not defined. D:\BuildManager\JACKVB\JackTestVB\Global.asax.vb(51,0) : error BC30002: Type 'EventArgs' is not defined. D:\BuildManager\JACKVB\JackTestVB\Global.asax.vb(55,0) : error BC30002: Type 'EventArgs' is not defined.
my code to build for both C# or VB projects ================================== public bool CompileCSharpVBDotNetProject( String[] sRefPaths )
{
bool bRet = false;
int i = 0;
int j = 0;
String sReference = "";
String[] sFilArr = new String[FilesList.Count];
FileInfo FilInf = new FileInfo( sFullPath );
CSharpCodeProvider CSProv = null;
VBCodeProvider VBProv = null;
ICodeCompiler iCodeComp = null;
CompilerResults CompRes = null;
CompilerParameters Params = new CompilerParameters();
Params.OutputAssembly = sBinaryBuildTarget + "\\" + sBinaryFileName;
Params.TreatWarningsAsErrors = bTreatWarningsAsErrors;
Params.WarningLevel = iWarningLevel;
// SHACK-UP REFERENCES ...
for( i = 0; i < ReferenceList.Count; i++ )
{
sReference = "";
for( j = 0; j < sRefPaths.GetLength( 0 ); j++ )
{
sReference = sRefPaths[j] + "\\" + ReferenceList[i].ToString();
if( File.Exists( sReference ) )
break;
}
if( sReference.Trim() != "" )
Params.ReferencedAssemblies.Add( sReference );
}
//hard code dll refereencs to test VB compiler (did not work)
// Params.ReferencedAssemblies.Add("System.dll");
// Params.ReferencedAssemblies.Add("System.Drawing.dll");
// Params.ReferencedAssemblies.Add("System.Data.dll");
// Params.ReferencedAssemblies.Add("System.Web.dll");
// Params.ReferencedAssemblies.Add("System.XML.dll");
// SET COMPLETE PATH FOR EACH FILE ...
for( i = 0; i < FilesList.Count; i++ )
{
sFilArr[i] = FilInf.DirectoryName + "\\" + FilesList[i].ToString();
}
if( iBinaryType == BINARY_TYPE_DLL )
Params.GenerateExecutable = false;
else
Params.GenerateExecutable = true;
try
{
switch( iProjectType )
{
case TYPE_CSHARP:
CSProv = new CSharpCodeProvider();
iCodeComp = CSProv.CreateCompiler();
CompRes = iCodeComp.CompileAssemblyFromFileBatch( Params, sFilArr );
break;
case TYPE_VBNET:
VBProv = new VBCodeProvider();
iCodeComp = VBProv.CreateCompiler();
CompRes = iCodeComp.CompileAssemblyFromFileBatch( Params, sFilArr );
break;
}
for( i = 0; i < CompRes.Errors.Count; i++ )
{
ErrorList.Add( CompRes.Errors[i] );
if( CompRes.Errors[i].IsWarning )
bHasWarnings = true;
else
bHasErrors = true;
}
for( i = 0; i < CompRes.Output.Count; i++ )
{
OutputList.Add( CompRes.Output[i] );
}
if( CompRes.NativeCompilerReturnValue == 0 ||( bHasWarnings && !bHasErrors ) )
bRet = true;
else
bRet = false;
}
catch( DirectoryNotFoundException ex )
{
sErrorMessage = ex.Message;
bRet = false;
}
catch( FileLoadException ex )
{
sErrorMessage = ex.Message;
bRet = false;
}
catch( FileNotFoundException ex )
{
sErrorMessage = "FILE INCLUDED IN .NET PROJECT FILE IS NOT UNDER SOURCE CONTROL. " + ex.Message;
bRet = false;
}
if( CSProv != null )
CSProv.Dispose();
return bRet;
}
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I don't know what strings you're passing into the function in each case, but maybe your problem is the dll's you're looking in. According to MSDN, the System.EventArgs class is in Mscorlib.dll. Look at the bottom of the class documentation for the dll location:
Events Args at MSDN
So maybe you need mscorlib.dll and not system.dll, which is in the commented DLL section. A namespace doesn't necessarily correspond the the name of the DLL it lives in, even within the framework itself.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Jim,
Thank you for looking into the error!
Jack and I reproduced the error with your compiler. We first modified Form1.vb to include more Dlls as below - Dim refs() As String = {"System.dll", "System.Drawing.dll", "System.Data.dll", "System.Web.dll", "System.XML.dll", "mscorlib.dll", "Microsoft.VisualBasic.dll"}
Then, we tried to compile the code below with your compiler and got the same error - BC30002 - Type "EventArgs' is not defined.
Imports System.Web Imports System.Web.SessionState
Public Class Global Inherits System.Web.HttpApplication
#Region " Component Designer Generated Code "
Public Sub New() MyBase.New()
'This call is required by the Component Designer. InitializeComponent()
'Add any initialization after the InitializeComponent() call
End Sub
'Required by the Component Designer Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Component Designer 'It can be modified using the Component Designer. 'Do not modify it using the code editor. Private Sub InitializeComponent() components = New System.ComponentModel.Container() End Sub
#End Region
Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs) ' Fires when the application is started End Sub
Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs) ' Fires when the session is started End Sub
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs) ' Fires at the beginning of each request End Sub
Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs) ' Fires upon attempting to authenticate the use End Sub
Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs) ' Fires when an error occurs End Sub
Sub Session_End(ByVal sender As Object, ByVal e As EventArgs) ' Fires when the session ends End Sub
Sub Application_End(ByVal sender As Object, ByVal e As EventArgs) ' Fires when the application ends End Sub
End Class
Would please look at it again? Thanks!
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|