Introduction
For an application I'm working on, I need the ability to allow the user to enter some text and have that text be compiled into a class just as if he had typed the class himself and passed it through a compiler. The application will wrap the text in some template code to form a (hopefully valid) program. But then it needs to be compiled so that the application can use the resultant class. The compiled Assembly doesn't need to be stored to disk for future reference, it can get garbage collected once the application is done with it. This method implements that functionality.
Background
Among the various articles here on the Code Project that deal with this topic are:
Dynamic Creation Of Assemblies/Apps[^]
Compiling with CodeDom[^]
Although I had no need to use CodeDom to create the program code, these articles (and MSDN) provided a starting point for my eventual solution.
My Compile method
There really isn't much to the code:
- Check the input parameters
- Ask the system if the requested language exists
- Get a Provider for the requested language
- Get default parameters for the Provider's compiler
- Set additional parameters as required
- Compile the provided code
- Check for errors
- Return the compiled Assembly
Any Exceptions thrown by called methods will simply be passed along; there's no point catching them here.
public static System.Reflection.Assembly
Compile
(
string Code
,
string Language
,
params string[] ReferencedAssemblies
)
{
if ( System.String.IsNullOrEmpty ( Code ) )
{
throw ( new System.ArgumentException
( "You must supply some code" , "Code" ) ) ;
}
if ( System.String.IsNullOrEmpty ( Language ) )
{
throw ( new System.ArgumentException
( "You must supply the name of a known language" , "Language" ) ) ;
}
if ( !System.CodeDom.Compiler.CodeDomProvider.IsDefinedLanguage ( Language ) )
{
throw ( new System.ArgumentException
( "That language is not known on this system" , "Language" ) ) ;
}
using
(
System.CodeDom.Compiler.CodeDomProvider cdp
=
System.CodeDom.Compiler.CodeDomProvider.CreateProvider ( Language )
)
{
System.CodeDom.Compiler.CompilerParameters cp =
System.CodeDom.Compiler.CodeDomProvider.GetCompilerInfo
( Language ).CreateDefaultCompilerParameters() ;
cp.GenerateInMemory = true ;
cp.TreatWarningsAsErrors = true ;
cp.WarningLevel = 4 ;
cp.ReferencedAssemblies.Add ( "System.dll" ) ;
if
(
( ReferencedAssemblies != null )
&&
( ReferencedAssemblies.Length > 0 )
)
{
cp.ReferencedAssemblies.AddRange ( ReferencedAssemblies ) ;
}
System.CodeDom.Compiler.CompilerResults cr =
cdp.CompileAssemblyFromSource
(
cp
,
Code
) ;
if ( cr.Errors.HasErrors )
{
System.Exception err = new System.Exception ( "Compilation failure" ) ;
err.Data [ "Errors" ] = cr.Errors ;
err.Data [ "Output" ] = cr.Output ;
throw ( err ) ;
}
return ( cr.CompiledAssembly ) ;
}
}
Using the Code
In the application I'm working on, the user has the option to enter text into a TextBox just as he would write a string literal when writing a program. For instance, in a (C#) program I may write:
string text = "Hello, world!" ;
Ordinarily, in a TextBox, the Hello, World!
would not be entered with the quotes, but in a program it's required. Things get even more complex if the text requires escapes:
string text = "Hello, \"Bob\"!" ;
or
string text = @"Hello, ""Bob""!" ;
Because mistakes can happen when you transcribe data, for this application I want to allow the user (a developer) to be able to copy-and-paste a string literal, exactly as he wrote it in a program, to the TextBox and have it passed through the compiler, just as it will be when his own application gets compiled.
This method:
- Accepts the Text from the TextBox
- Wraps it in a very simple C# program (notice how)
- Uses the CSharp (C#) compiler to compile the code into an Assembly
- Retrieves the Type defined by the program
- Gets the field that contains the compiled string literal
- Retrieves the string literal and returns it
private static string
WrapText
(
string Text
)
{
string code = System.String.Format
(
@"
namespace TextWrapper
{{
public static class TextWrapper
{{
public const string Text = {0} ;
}}
}}
"
,
Text
) ;
System.Reflection.Assembly assm = PIEBALD.Lib.LibSys.Compile
(
code
,
"CSharp"
) ;
System.Type type = assm.GetType ( "TextWrapper.TextWrapper" ) ;
System.Reflection.FieldInfo field = type.GetField
(
"Text"
,
System.Reflection.BindingFlags.Public
|
System.Reflection.BindingFlags.Static
) ;
return ( (System.String) field.GetValue ( null ) ) ;
}
Note: I intend to support VB.net as well.
CodeDom is in the System.Runtime.Remoting dll, so add a reference.
Conclusion
Overkill? I don't think so. For the samples above, sure, but consider more complex string literals, especially Regular Expressions, which often contain a lot of escapes and may be long enough to warrant being broken onto several lines. For example here's one that's not too big:
private static readonly System.Text.RegularExpressions.Regex HrefReg =
new System.Text.RegularExpressions.Regex
(
"href\\s*=\\s*(('(?'uri'[^'#]*)?(#(?'id'[^']*))?')" +
"|(\"(?'uri'[^\"#]*)?(#(?'id'[^\"]*))?\"))"
) ;
To copy-and-paste this the usual way, I'd have to copy the two sections separately, and then remove the escapes. If I edit the string in the TextBox and then want to copy-and-paste it back, I have to reverse the steps, ensuring that I get all the escapes back where they belong.
Using a verbatim string literal would make it easier:
private static readonly System.Text.RegularExpressions.Regex HrefReg =
new System.Text.RegularExpressions.Regex
(
@"
href\s*=\s*(('(?'uri'[^'#]*)?(#(?'id'[^']*))?')
|(""(?'uri'[^""#]*)?(#(?'id'[^""]*))?""))
"
) ;
Using the technique described in this article, either of these string literals may be copied-and-pasted directly to the TextBox (or wherever) and parsed by the actual compiler that will be used to compile the strings later. If the text is edited away from the source code, it may be copied-and-pasted back without change.
Points of Interest
I wrestled for a long time with trying to load the compiled Assembly into a different AppDomain. I had some success and a lot of headache. Eventually I came across the following comment in the MSDN documentation of AppDomain.RelativeSearchPath[^]:
// Because Load returns an Assembly object, the assemblies must be
// loaded into the current domain as well. This will fail unless the
// current domain also has these directories in its search path.
So that's what was causing the headache. Plus, if loading an Assembly into a different AppDomain doesn't keep the Assembly from being loaded into the current one, then I don't see the point of trying.
History
2009-08-15 First submitted