Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Code Test Bed

0.00/5 (No votes)
25 Oct 2010 1  
Algorithm experimentation via dynamic code generation and compile
UserSource.jpg

Introduction

Have you ever been coding merrily away and come up against a problem that you *think* can be readily solved by using [insert favorite algorithm and/or snippet here] with just a *few* tweaks?

No point in denying it, we all do it. I present to you a tool for testing those tweaks by completely side-stepping your on-going development.

Pardon Me? Why Would I Side-step?

Although it may sound heretical, as developers, there are times during the process of coding that we don't know precisely what we'll implement until we begin to code it. Most of the time, we code what we know and already works even pulling in already written and tested code within a project (no one wants to re-invent the wheel, especially if they're the ones who invented it in the first place). But every so often, we hit a point in the code where we would really like to step aside and try out a little something that we just thought of to see if it will give us the results that we're pretty sure it will give us.

When I want to try something out, I'm really not interested in firing up another instance of my development environment (Visual Studio, in case you were wondering). I want something that’s lightweight that will let me code just the stuff that I need to try and nothing else. Think of it as sort-of-scripting.

How Am I Going To Do That?

Great question! I've developed this little utility to provide exactly that service for us developers. With it, you can code in C# or VB.NET and the utility will wrap it all together into an assembly that it will execute for you, allowing you to show off your genius.

Well, not entirely. This utility lets you enter the code that you want to try, but you also have to code the lines to get output.

Okay, I'll Bite. Show Me How to Use It

That’s the easy part. Fire it up and you'll see the screen above (but without the user entered source). You can just start typing in your source. There’s no intellisense, no code colorization, no auto-formatting, no pretty printing, no nothing. Bare bones, enter your code here.

You can create instances of any .NET class that you can reference and instantiate, even constructing Windows Forms on the fly. All of you coders out there who cut your teeth on the original beta versions of C# with Notepad know what I'm talking about. This utility isn't all that different, except that you don't need to go to the command line to compile your code.

Once you've selected the language, you're using in the language combobox, you can compile your code by clicking the Build button or pressing the F5 key. Once the build completes, the UI will switch to the Errors tab to show you your plethora of errors. If you compiled error free, you can click the Run button or press Shift+F5 to run your code.

When your code runs, you will see that it runs as a console application. This is on purpose. Your user code gets wrapped into a console application to execute.

Wait a Minute, I Can Only Do Console Applications?

No, you're not limited to console applications … … … well … … … yes, yes you are, but no you're not. Okay, let’s clear this one up.

Yes, the application will only write console applications, but all that means is that to try something out in a Windows Form, you have to write the code to create the Form. It’s not hard and I've provided a sample. The reason for the console applications is that the code you write is wrapped into a pre-defined container for execution. Here’s the C# version of the wrapper:

namespace OpGenGo {
	using System;
	using System.Collections.Generic;
	using System.Text;	
	
	// <summary>
	// Driver to execute the user entered code
	// </summary>
	public class Program {
		
		#region > Module Entry Point <
		// <summary>
		// Program Entry Point
		// </summary>
		public static void Main(string[] args) {
			AppDomain.CurrentDomain.UnhandledException += 
			new UnhandledExceptionEventHandler
			(OpGenGo.Program.OnUnhandledException);
			try {
				OpGenCompGo uc = new OpGenCompGo();
				uc.UserCode();
			}
			catch (Exception ex) {
				Console.WriteLine();
				Console.WriteLine(">> Exception during 
					processing of user code:");
				Console.WriteLine((ex.Message));
				Console.WriteLine();
			}
			Console.WriteLine();
			Console.WriteLine(">> Press any key to continue <<");
			Console.WriteLine();
			Console.ReadKey(true);
		}
		#endregion
		
		#region > Application Domain Unhandled Exception Handler <
		// <summary>
		// Application Domain exception handler for *any* unhandled exceptions.
		// </summary>
		public static void OnUnhandledException(object sender, 
				UnhandledExceptionEventArgs e) {
			System.Exception uex = ((System.Exception)(e.ExceptionObject));
			Console.WriteLine();
			Console.WriteLine(">> Unhandled Exception during processing:");
			Console.WriteLine((uex.Message));
			Console.WriteLine();
		}
		#endregion
	}
	
	// <summary>
	// Class to encapsulate the execution of the user entered code.
	// </summary>
	public class OpGenCompGo {
		
		#region > Public Methods <
		// <summary>
		// User entered code execution.
		// </summary>
		public void UserCode() {
			// *********************************
			// * B E G I N   U S E R   C O D E *
			// *********************************
		    Console.WriteLine("Hello, World!");
			// *********************************
			// *   E N D   U S E R   C O D E   *
			// *********************************
		}
		#endregion
	}
}

If you look through the code, you'll see a section clearly labelled as the user entered code. Everything else is part of the wrapper, but the user entered code can be whatever it is that you're trying to do. The idea here is discreet coding. You only need to code the part that you want to try. In the above code sample, all it does is write the traditional “Hello, World!” message.

So How Does this Goofy Thing Work, Anyway?

The source is broken up into two components; the user interface and the code generator.

The user interface handles the user entered code, presentation of the generated code, presentation of errors, ability to load user code and save generated code and assemblies (yeah, I went ahead and let you save it out to the file system). So much for the easy stuff.

The code generator is all in the DynComp component. When I first started to build this utility, I was thinking about having a small library of wrapper code to pick from so that I could wrap my code into various containers. It would be easy to do, quick to write, simple to understand, and even easier to extend by creating more wrappers in the library. Great idea, right? Not really. That idea gets away from the need to just code what I want to try, so I dumbed it down a bit. Besides, I really wanted to play around with the CodeDOM.

The CodeDOM?? Are You Crazy??

Why? Who have you been talking to?

Seriously, I wanted to use the CodeDOM to create the code so that I didn't need a library of wrappers. In the end, it kept the utility nice and tidy. That is, if you consider about 275 lines of code to generate the wrapper tidy.

I know. Dumbfounded silence. Yup. Somewhere around 275 lines of CodeDOM code to generate the wrapper. Okay, maybe I am a little on the nutso side, but by using the CodeDOM I can modify the code to accept any .NET CodeDOM provider with the only limitation being that the CodeDOM provider must be able to actually compile from a CodeCompileUnit object (surprise, not all of them can, some will throw an unsupported function exception).

Part of the trick here is that, for the most part, the user entered code is *not* interpreted into CodeDOM calls, it’s inserted using the CodeDOM as snippet lines. Here, I'll show you:

// Separate our source into lines
string[] SrcLines = _Source.Split(new char[] { '\n' }, 
	StringSplitOptions.RemoveEmptyEntries);

The above line simply breaks the user entered code into individual lines. One assumption that I make in the following code is that you, as the user, have entered all of your using/Imports statements at the top of the User Source, which allows the following to work properly:

// Set the user added imports
while (SrcIdx < SrcIdxMax)
{
	if (SrcLines[SrcIdx].StartsWith("using ") ||
		SrcLines[SrcIdx].StartsWith
			("Imports ", StringComparison.OrdinalIgnoreCase))
	{
		int IdxStrt = SrcLines[SrcIdx].IndexOf(' ') + 1;
		int IdxStop = SrcLines[SrcIdx].IndexOf(';');
		string ImpName = string.Empty;
		if (IdxStop < 0)
			ImpName = SrcLines[SrcIdx].Substring(IdxStrt);
		else
			ImpName = SrcLines[SrcIdx].Substring
					(IdxStrt, IdxStop - IdxStrt);
		if (!(ImpName.Equals("system", StringComparison.OrdinalIgnoreCase) ||
			  ImpName.Equals("system.collections.generic", 
				StringComparison.OrdinalIgnoreCase)))
			CNS.Imports.Add(new CodeNamespaceImport(ImpName));
		SrcIdx++;
	}
	else
	{
		// No more "using" or "imports" lines
		break;
	}
}

The above is used to separate out the import code lines. These are the only user entered lines of code that are not entered as snippets. You'll note that the “System” and “System.Collections.Generic” classes are always included, so they are ignored if the user entered them. Anything else gets put in as a import.

To handle the rest of the user entered code:

while (SrcIdx <= SrcIdxMax)
{
	CodeSnippetStatement Stmt = new CodeSnippetStatement
	(string.Format("{0}{1}", new String('\t', 4) ,SrcLines[SrcIdx]));
	UserTry.TryStatements.Add(Stmt);
	SrcIdx++;
}

The above code adds the remaining lines of user entered code as snippet statements inside a try block. By putting the user code into the try block, we can capture a system exception and report it out.

By way of example, the user entered code at the top of this article generates the following code:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.3615
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace OpGenGo {
	using System;
	using System.Collections.Generic;
	using System.Text;
	
	
	// <summary>
	// Driver to execute the user entered code
	// </summary>
	public class Program {
		
		#region > Module Entry Point <
		// <summary>
		// Program Entry Point
		// </summary>
		public static void Main(string[] args) {
			AppDomain.CurrentDomain.UnhandledException += 
				new UnhandledExceptionEventHandler
				(OpGenGo.Program.OnUnhandledException);
			try {
				OpGenCompGo uc = new OpGenCompGo();
				uc.UserCode();
			}
			catch (Exception ex) {
				Console.WriteLine();
				Console.WriteLine(">> Exception during 
					processing of user code:");
				Console.WriteLine((ex.Message));
				Console.WriteLine();
			}
			Console.WriteLine();
			Console.WriteLine(">> Press any key to continue <<");
			Console.WriteLine();
			Console.ReadKey(true);
		}
		#endregion
		
		#region > Application Domain Unhandled Exception Handler <
		// <summary>
		// Application Domain exception handler for *any* unhandled exceptions.
		// </summary>
		public static void OnUnhandledException(object sender, 
				UnhandledExceptionEventArgs e) {
			System.Exception uex = ((System.Exception)(e.ExceptionObject));
			Console.WriteLine();
			Console.WriteLine(">> Unhandled Exception during processing:");
			Console.WriteLine((uex.Message));
			Console.WriteLine();
		}
		#endregion
	}
	
	// <summary>
	// Class to encapsulate the execution of the user entered code.
	// </summary>
	public class OpGenCompGo {
		
		#region > Public Methods <
		// <summary>
		// User entered code execution.
		// </summary>
		public void UserCode() {
			// *********************************
			// * B E G I N   U S E R   C O D E *
			// *********************************
		    for(int i = 0; i < 21; i++)
			{
				a = i * i * i * i;
				Console.WriteLine(string.Format("{0} : {1}", i, a));
			}
			// *********************************
			// *   E N D   U S E R   C O D E   *
			// *********************************
		}
		#endregion
	}
}

Coding Change-over

Every developer that I know who works with both C# and VB.NET, has the same problem. We all, from time to time, code C# like VB and VB like C#. Personally, my biggest issue is when I'm coding VB and add a semi-colon to the end of a line. Well, rather than being annoyed by this, I put in the following code:

// If the user selected VB and the lines end with a semi-colon (;), 
// strip out the semi-colon
if (_ProviderLanguage == "VB")
{
	for (int sidx = 0; sidx < SrcLines.Length; sidx++)
	{
		if (SrcLines[sidx].EndsWith(";"))
			SrcLines[sidx] = SrcLines[sidx].Substring
				(0, SrcLines[sidx].Length - 1);
	}
}

This way, if I select VB as my source type and I put in a semi-colon, it just gets stripped off. Other than this small piece of code and the separate handling of the imports/using statements, there is no interpretation or translation of the user entered code.

Interesting to Note, But Not Really Valuable

Take a look at the code shown below:

/// <summary>
/// Generate the source from the namespace.
/// </summary>
/// <param name="CDP">CodeDomProvider object.</param>
/// <returns>Generated code on success, string.Empty otherwise.</returns>
/// <remarks>10) Call one of the generator's GenerateCodeFrom... 
/// methods to emit the code</remarks>
private string GetCode(CodeDomProvider CDP)
{
    // Clear the old
    CleanTemps();

    // Our compiler parameters object
    CompilerParameters parms = new CompilerParameters();

    // Create a code generator options object
    CodeGeneratorOptions opt = new CodeGeneratorOptions();

    // Set the indentation level
    opt.IndentString = new String('\t', 1);

    // Use a StringWriter object to capture the code
    StringBuilder rc = new StringBuilder();
    StringWriter sw = new StringWriter(rc);

    // Generate the code
    CDP.GenerateCodeFromCompileUnit(_CCU, sw, opt);
    //CDP.GenerateCodeFromNamespace(_CCU.Namespaces[0], sw, opt);

    // Return the result
    return rc.ToString();
}

Look for the comment // Generate the code. You will see two generation statements, one commented and one not.

There is one major and one minor difference between these two statements. The major difference is that when generating from a CodeCompileUnit, the generator will produce code for all namespaces that are in the CodeCompileUnit. The minor difference, and bigger surprise, is that when you generate code from a namespace, you don't get the following header block:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.3615
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

I told you it was minor and not really of value.

Warning: Unsafe Code in Use

Not a big deal, really. I use the unsafe technique to load the font Anonymous Pro (available from www.ms-studio.com) into a private font collection from an embedded source using the following code:

_PFC = new PrivateFontCollection();
Stream FontStream = this.GetType().Assembly.GetManifestResourceStream
			("Testbed.Fonts.Anonymous Pro.ttf");
byte[] FontData = new byte[FontStream.Length];
FontStream.Read(FontData, 0, (int)FontStream.Length);
FontStream.Close();
unsafe
{
    fixed (byte* pFontData = FontData)
    {
        _PFC.AddMemoryFont((System.IntPtr)pFontData, FontData.Length);
    }
}

The only unsafe portion here is the code using the pointer to reference the embedded font in order to load it into the private font collection. This is that strange yet unambiguous font that you see when you enter your source code.

In Summation

Like all of the utilities that I write, I wrote this one to make my life a little easier and to learn a bit more along the way. If it helps you as well, so much the better. If you like it and want to share it, better yet. If you want to improve it, great but remember to check the licensing requirements if you make changes (in particular the part where you have to post your code).

Updates

Version 1.1

  • Changed tabstops (the first 20) in the user code window to 4-character tabs
  • Added Cut/Copy/Paste editing to the user code window
  • Added drag & drop to the user code window
  • Added Copy to the generated code and error windows
  • Some code refactoring and cleanup
  • Changed exception handling for cleaner fails

History

  • v1 - Initial release
  • v1.1 - Some code refactoring, more effective exception handling, added edit niceties

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here