Wednesday, August 08, 2007

How would you like to be able to write VB.NET or C# code in your Visual FoxPro applications and have it compiled a single time (JIT) at runtime into an assembly in memory that would even return a .NET object back to you? Here are a couple of simple examples to show what the code would look like in a VFP prg file:

// Initializes the variables to pass to the MessageBox.Show method.
string message = "You did not enter a server name. Cancel this operation?";
string caption = "Error Detected in Input";
MessageBoxButtons buttons = MessageBoxButtons.YesNo;
DialogResult result;
// Displays the MessageBox.
result = MessageBox.Show(message, caption, buttons);
return result;

OR

' Initializes variables to pass to the MessageBox.Show method.
Dim Message As String = "You did not enter a server name. Cancel this operation?"
Dim Caption As String = "Error Detected in Input"
Dim Buttons As MessageBoxButtons = MessageBoxButtons.YesNo
Dim Result As DialogResult
'Displays the MessageBox
Result = MessageBox.Show(Message, Caption, Buttons)
Return Result

How It's Done

Now that we've gotten rid of all the people that replied, 'no thanks', to that first question we can set about the task of showing how the aforementioned is easily accomplished. I'm not going to get into this entire thing in excrutiating detail. I want to present this as simply as possible, but never fear, I will touch on the finer points and point out some of the pitfalls a developer can run into when coding this sort of thing in .NET.

Compile an Assembly in Memory from a String of Source Code

The first thing to know is that .NET provides a class known as the 'CodeDomProvider' and it has a CompileAssemblyFromSource method that is capable of generating compiled assemblies to disk or even in memory. Yep, basically you just hand an instance of a CodeDomProvider subclass, such as the CSharpCodeProvider or VBCodeProvider some CompilerParameters and some source code and... voila, you have a compiled .NET assembly. The CompileAssemblyFromSource method returns a CompilerResults object, and the CompilerResults object just so happens to have a handy-dandy CompiledAssembly property.

CompilerResults compilerResults =
codeDomProvider.CompileAssemblyFromSource(compilerParameters, new string[] {codeString});

The source code basics are provided in the assembly I'll be giving you here. The namespace, class, and even a method template have all been laid out. All you have to do is supply the source code (codestring variable in code below) that the method is going to execute and return back some kind of .NET object from the method.

StringBuilder stringBuilder = new StringBuilder("");
stringBuilder.Append("using System;\n");
stringBuilder.Append("using System.Xml;\n");
stringBuilder.Append("using System.Data;\n");
stringBuilder.Append("using System.Windows.Forms;\n");
stringBuilder.Append("namespace InMemoryAssembly{\n");
stringBuilder.Append("public class InMemoryClass{\n");
stringBuilder.Append("public static object ExecuteDotNetCode(){\n");
stringBuilder.Append(codeString + "\n");
stringBuilder.Append("}\n}\n}\n");
CSharpCodeProvider cSharpCodeProvider = new CSharpCodeProvider();
return GetDotNetAssembly(stringBuilder, cSharpCodeProvider);

Create an Instance of a Class Contained in the Assembly

Once the assembly has been compiled in memory it is time to somehow retrieve an instance of a class contained in the assembly. This is accomplished by making a call to the CreateInstance method of the CompiledAssembly. We simply send a string that represents the fully qualified name of the class and we are pleasantly surprised to find an instance of a System.Object has been instantiated for us.

object oMemoryInstance = assembly.CompiledAssembly.CreateInstance("InMemoryAssembly.InMemoryClass");

Invoke a Method of the Class

Once we have that object we need to find a way to invoke one of its methods. We can accomplish th lot of ways, such as:

Calling the InvokeMember method of the Type class...

oMemoryInstance.GetType().InvokeMember("ExecuteDotNetCode",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, new object[0]);

...but that's like watching paint dry it is so slow.

Calling the Invoke method of the MethodInfo class...

oMemoryInstance.GetType().GetMethod("ExecuteDotNetCode").Invoke(obj2, null);

...but that's like watching grass grow it is so slow.

So, while we could make a call on an Interface (which is fast), I prefer using the Invoke method of the DynamicMethodDelegate class...

MethodInfo mInfo = oMemoryInstance.GetType().GetMethod("ExecuteDotNetCode");
DynamicMethodDelegate dynamicMethodDelegate = Create(mInfo);
dynamicMethodDelegate.Invoke();

Now, while this solution takes more code and there is some rather advanced techniques contained in the Create method that use IL instructions, the call is fast (unlike Type.InvokeMethod and MethodInfo.Invoke). Invoke is even kind enough to send back the return of the method it invoked. This is how we can get a .NET object back...

return dynamicMethodDelegate.Invoke();

For those of you wanting to understand the preceding more in-depth, head on over to the Code Project and read "Fast late-bound invocation through DynamicMethod delegates'. I personally felt my I.Q. steadily increasing as I read that article. And if after reading it you are still not satiated, please feel free to read the PDF entitled, 'Runtime Code Generation with JVM and CLR'. It's nearly guaranteed that your brain will ache after that read.

What are the Hurdles to Be Surmounted

The single biggest hurdle (an awwww that sucks kind of thing) when running .NET code dynamically is that every single time an assembly gets compiled dynamically it gets stuck in memory until the process ends and the AppDomain is unloaded. That would be the end of it - can you imagine running some .NET code dynamically thousands of times and having that many assemblies up in memory on your user's computer? - that's what I call a showstopper. Luckily there are some techniques that can be employed that can mitigate and/or altogether remove this hurdle.

If you want to see the super, advanced, what-the-heck-is-going-on, you're-making-my-head-hurt, must-be-a-cyborg-programmer way of doing it, then head on over to this article that was done by Rick Strahl back in 2002. While you would think that Microsoft would have deprecated most of what Rick used in that article by now (they've had 5 years), Rick's article is still pointed to as one of the best solutions out there and is quite valid to this very day.

On the upside of Rick's solution I find:

  • Dynamic assemblies only stay in memory as long as needed. They can be removed from memory (or at least put up for garbage collection) by simply unloading the appdomain they were loaded in.
  • Your programmer buddies will think you are way cool when you show them the solution.

On the downside I find:

  • Complex solution that's not easy to understand - it took me about 3 readings and quite a few Google searches before I felt like I had my head around it.
  • Assemblies must be compiled to disk and later deleted. Compiling a dynamic assembly to disk and then having to delete it after it is used is not preferrable in my mind for a number of obvious reasons and Disk I/O is expensive.
  • Requires more than one Assembly be deployed for this to work. While not a huge deal it does make deployment and versioning just that much harder.
  • Because the assemblies are unloaded each time any return calls with the exact same source code must be compiled all over again.

Now, if you want the somewhat simple, just-keep-thousands-of-assemblies-from-being-stuck-in-memory, no-frills, get-er-done way of doing this then I think you'll like the solution to this problem that I hit upon. The solution is based on some ideas presented in 'ASP.NET 2.0 - Safely Compile And Execute Source Code Dynamically' by Robbe D. Morris. In it Robbe proposes allowing the assemblies to get stuck in memory, but not allowing a particular piece of dynamic source code to be compiled more than once. In essence, Robbe proposes caching the compiled assemblies for later use rather than discarding them.

What I did was take Robbe's idea and proceed to simplify it slightly and expand on it a bit. I took his techniques and boiled them down to the bare bones (I don't track near the stuff he does in his codecontainer class) and then instead of using a GUID to uniquely identify the assembly I used the source code that made up the assembly itself. This was accomplished by using an MD5 hash on the string of source code to be compiled and saving that MD5 hash along with the cached assemblies. So, basically when the source code is sent in from the VFP application for compilation and a MD5 message digest is created to uniquely identify that source code. That MD5 is searched for in the currently cached assemblies, which are held in a generic List object, and if a match is found the cached assembly is simply returned instead of compiling the source code.

On the upside I find:

  • The number of assemblies loaded into memory is limited.
  • Source code is only compiled once and cached assemblies are accessed instantly, so repeated calls are as fast as can be.
  • Assemblies are compiled in memory as there is no need to compile them to disk.
  • Only one COM-visible assembly needs to registered on the user's machine to enable this solution.
  • The solution seems pretty straight-forward (to me anyways - correct me if I'm wrong).

On the downside I find:

  • Assemblies still stay in memory until the VFP process exits, so for one-offs there is no performance gain.
  • The Class and Method signatures are completely static - there is room for improvement here to make it more scalable and configurable.
  • Your programmer buddies will think you're neat instead of cool.

Installing the VfpDotNet Assembly

Simply locate the VfpDotNet.dll that is provided in the download, ..\VfpDotNet\VfpDotNet\bin\Release\VfpDotNet.dll and register it using RegAsm.exe. You'll probably want to use the/tlb /codebase switches when registering it. There are lots of online resources explaining how to use RegAsm.exe which comes with the .NET framework, so I won't bother providing further instructions regarding it here.

Conclusion

The ability to easily and dynamically run .NET code in a COM-aware environment is one of the Holy Grails for those looking to extend their unmanaged application or perhaps find a migration path that actually makes sense (when did incremental become a bad word?). There are lots of developers out there that are exploring and improving solutions that make it easier and reasonable for a VFP developer to start incorporating some .NET framework stuff into their applications. Speaking of which, did you see this recent post by Rick?

A Download and Some Sample VFP Code

Download VfpDotNet COM-Visible Assembly - Approx. 18 KB

LOCAL lcCSharpCode, lcCPlusPlusCode, lcVBCode, lcJSharpCode, loReturn
LOCAL m.loExecDotNet as DynaDotNet.ExecDotNet
m.loExecDotNet = CREATEOBJECT("DynaDotNet.ExecDotNet")
TEXT TO m.lcCSharpCode noshow
// Initializes the variables to pass to the MessageBox.Show method.
string message = "You did not enter a server name. Cancel this operation?";
string caption = "Error Detected in Input";
MessageBoxButtons buttons = MessageBoxButtons.YesNo;
DialogResult result;
// Displays the MessageBox.
result = MessageBox.Show(message, caption, buttons);
return result;
ENDTEXT

TEXT TO m.lcVBCode noshow
' Initializes variables to pass to the MessageBox.Show method.
Dim Message As String = "You did not enter a server name. Cancel this operation?"
Dim Caption As String = "Error Detected in Input"
Dim Buttons As MessageBoxButtons = MessageBoxButtons.YesNo
Dim Result As DialogResult
'Displays the MessageBox
Result = MessageBox.Show(Message, Caption, Buttons)
Return Result
ENDTEXT

CLEAR
?m.loExecDotNet.GetLoadedAssemblies()
loReturn = m.loExecDotNet.RunCode(lcCSharpCode,1) && First time through the assembly will be compiled
loReturn = m.loExecDotNet.RunCode(lcVBCode,2) && First time through the assembly will be compiled
loReturn = m.loExecDotNet.RunCode(lcCSharpCode,1) && Uses cached assembly
loReturn = m.loExecDotNet.RunCode(lcVBCode,2) && Uses cached assembly
?
?m.loExecDotNet.GetLoadedAssemblies()

Wednesday, August 08, 2007 6:36:11 AM (Central Daylight Time, UTC-05:00)  #    Comments [1]

 

Archive

<August 2008>
SunMonTueWedThuFriSat
272829303112
3456789
10111213141516
17181920212223
24252627282930
31123456