/*
* Util.CSharpHelper.cs
* Authors: Adam Nabinger
* Copyright (c) 2007-2008 Cornell University
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
using System;
using System.CodeDom.Compiler;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Security.Permissions;
using System.Text;
using Microsoft.CSharp;
using Microsoft.Xna.Framework;
namespace Util
{
// TODO: Should run scripts in seperate AppDomain?
// TODO: Only reference assemblies from Assembly.GetCallingAssembly().GetReferencedAssemblies()
///
/// A class for compiling and running simple C# scripts at runtime.
///
public static class CSharpHelper
{
#region Constants
// String constant added to the beginning of any script to ensure that it runs
private const string SimpleScriptPrefix = "public class Script : Util.SimpleScript { public override ";
// String constant that's added to the end of the script
private const string SimpleScriptSuffix = "}}";
// Space string constant
private const string Space = " ";
#endregion
#region Fields
// Used for compiling the C# script
private static readonly CSharpCodeProvider csp = new CSharpCodeProvider();
// The associated parameters for when compiling the script
private static CompilerParameters cp;
#endregion
#region Setup
///
/// Sets up all of the compiler parameters
///
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode), DebuggerHidden]
private static void setup()
{
cp = new CompilerParameters();
cp.GenerateExecutable = false;
cp.IncludeDebugInformation = false;
cp.WarningLevel = 3;
cp.TreatWarningsAsErrors = false;
cp.CompilerOptions = "/target:library /optimize";
//cp.GenerateInMemory = false;
//cp.TempFiles = new TempFileCollection(Environment.CurrentDirectory + @"\Temp\");
cp.ReferencedAssemblies.Add("mscorlib.dll");
cp.ReferencedAssemblies.Add("System.dll");
//cp.ReferencedAssemblies.Add("Util.dll");
foreach (AssemblyName assemblyName in Assembly.GetEntryAssembly().GetReferencedAssemblies())
{
switch (assemblyName.Name)
{
case "Microsoft.Xna.Framework.Game":
cp.ReferencedAssemblies.Add(Assembly.GetAssembly(typeof(Game)).Location);
break;
case "Microsoft.Xna.Framework":
cp.ReferencedAssemblies.Add(Assembly.GetAssembly(typeof(BoundingBox)).Location);
break;
default:
if (File.Exists(assemblyName.Name + ".dll"))
{
cp.ReferencedAssemblies.Add(assemblyName.Name + ".dll");
}
break;
}
}
foreach (string fileName in AssemblyLocator.AllKnownAssemblies)
{
bool add = true;
foreach (string a in cp.ReferencedAssemblies)
{
int val = a.LastIndexOf('\\');
if (val != -1)
{
if (fileName.EndsWith(a.Substring(val), StringComparison.OrdinalIgnoreCase))
{
add = false;
break;
}
}
else
{
if (fileName.EndsWith(a, StringComparison.OrdinalIgnoreCase))
{
add = false;
break;
}
}
}
if (add)
{
cp.ReferencedAssemblies.Add(fileName);
}
}
}
#endregion
#region DoSimpleString
///
/// Execute the given string as a simple segment of c# code.
/// If there are compile errors, an ArgumentException will be throw back containing the details.
///
/// The string to process
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode), DebuggerHidden]
public static void DoSimpleString(string source)
{
StringBuilder sb = new StringBuilder();
sb.Append(SimpleScriptPrefix);
sb.Append("void Run(){");
sb.Append(source);
sb.Append(SimpleScriptSuffix);
setup();
CompilerResults cr = csp.CompileAssemblyFromSource(cp, new string[] { sb.ToString() });
if (!cr.Errors.HasErrors)
{
((SimpleScript)cr.CompiledAssembly.CreateInstance("Script")).Run();
}
else
{
throw HandleErrors(cr.Errors);
}
}
///
/// Execute the given string as a simple segment of c# code, passing in the given object as input
/// If there are compile errors, an ArgumentException will be throw back containing the details.
///
/// The string to process
/// The argument to be passed in when processing the script
/// The name that the script refers to for the argument that is passed in
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode), DebuggerHidden]
public static void DoSimpleString(string source, object input, string inputName)
{
StringBuilder sb = new StringBuilder();
sb.Append(SimpleScriptPrefix);
sb.Append("void Run(object _input) {");
sb.Append(input.GetType().FullName);
sb.Append(Space);
sb.Append(inputName);
sb.Append(" = _input as ");
sb.Append(input.GetType().FullName);
sb.Append(";");
sb.Append(source);
sb.Append(SimpleScriptSuffix);
setup();
CompilerResults cr = csp.CompileAssemblyFromSource(cp, new string[] { sb.ToString() });
if (!cr.Errors.HasErrors)
{
((SimpleScript)cr.CompiledAssembly.CreateInstance("Script")).Run(input);
}
else
{
throw HandleErrors(cr.Errors);
}
}
///
/// Execute the given string as a simple segment of c# code, that returns an object of given type.
/// If there are compile errors, an ArgumentException will be throw back containing the details.
/// If the script does not return an object of outputType, an InvalidCastException will be thrown.
///
/// The string to process
/// The expected type of output
/// Output processed from the script that is of the specified type
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode), DebuggerHidden]
public static object DoSimpleString(string source, Type outputType)
{
StringBuilder sb = new StringBuilder();
sb.Append(SimpleScriptPrefix);
sb.Append("object RunAndReturn(){");
sb.Append(source);
sb.Append(SimpleScriptSuffix);
setup();
CompilerResults cr = csp.CompileAssemblyFromSource(cp, new string[] { sb.ToString() });
if (!cr.Errors.HasErrors)
{
object ret = ((SimpleScript)cr.CompiledAssembly.CreateInstance("Script")).RunAndReturn();
if (outputType.IsInstanceOfType(ret))
{
return ret;
}
throw new InvalidCastException(ret.GetType().ToString());
}
throw HandleErrors(cr.Errors);
}
///
/// Execute the given string as a simple segment of c# code, passing in the given object as input, and that returns an object of given type.
/// If there are compile errors, an ArgumentException will be throw back containing the details.
/// If the script does not return an object of outputType, an InvalidCastException will be thrown.
///
/// The string to process
/// The argument to be passed in when processing the script
/// The name that the script refers to for the argument that is passed in
/// The expected type of output
/// Output processed from the script that is of the specified type
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode), DebuggerHidden]
public static object DoSimpleString(string source, object input, string inputName, Type outputType)
{
StringBuilder sb = new StringBuilder();
sb.Append(SimpleScriptPrefix);
sb.Append("object RunAndReturn(object _input) {");
sb.Append(input.GetType().FullName);
sb.Append(Space);
sb.Append(inputName);
sb.Append(" = _input as ");
sb.Append(input.GetType().FullName);
sb.Append(";");
sb.Append(source);
sb.Append(SimpleScriptSuffix);
setup();
CompilerResults cr = csp.CompileAssemblyFromSource(cp, new string[] { sb.ToString() });
if (!cr.Errors.HasErrors)
{
object ret = ((SimpleScript)cr.CompiledAssembly.CreateInstance("Script")).RunAndReturn(input);
if (outputType.IsInstanceOfType(ret))
{
return ret;
}
throw new InvalidCastException(ret.GetType().ToString());
}
throw HandleErrors(cr.Errors);
}
#endregion
#region DoSimpleFile
///
/// Execute the given string as a simple segment of c# code.
/// If there are compile errors, an ArgumentException will be throw back containing the details.
///
/// The name of the script file to process
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode), DebuggerHidden]
public static void DoSimpleFile(string fileName)
{
//Console.WriteLine("Calling Assembly: " + Assembly.GetCallingAssembly());
DoSimpleString(FileToString(fileName));
}
///
/// Execute the given string as a simple segment of c# code, passing in the given object as input.
/// If there are compile errors, an ArgumentException will be throw back containing the details.
///
/// The name of the script file to process
/// The argument to be passed in when processing the script
/// The name that the script refers to for the argument that is passed in
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode), DebuggerHidden]
public static void DoSimpleFile(string fileName, object input, string inputName)
{
//Console.WriteLine("Calling Assembly: " + Assembly.GetCallingAssembly());
DoSimpleString(FileToString(fileName), input, inputName);
}
///
/// Execute the given string as a simple segment of c# code, that returns an object of given type.
/// If there are compile errors, an ArgumentException will be throw back containing the details.
/// If the script does not return an object of outputType, an InvalidCastException will be thrown.
///
/// The name of the script file to process
/// The expected type of output
/// Output processed from the script that is of the specified type
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode), DebuggerHidden]
public static object DoSimpleFile(string fileName, Type outputType)
{
//Console.WriteLine("Calling Assembly: " + Assembly.GetCallingAssembly());
return DoSimpleString(FileToString(fileName), outputType);
}
///
/// Execute the given string as a simple segment of c# code, passing in the given object as input, and that returns an object of given type.
/// If there are compile errors, an ArgumentException will be throw back containing the details.
/// If the script does not return an object of outputType, an InvalidCastException will be thrown.
///
/// The name of the script file to process
/// The argument to be passed in when processing the script
/// The name that the script refers to for the argument that is passed in
/// The expected type of output
/// Output processed from the script that is of the specified type
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode), DebuggerHidden]
public static object DoSimpleFile(string fileName, object input, string inputName, Type outputType)
{
//Console.WriteLine("Calling Assembly: " + Assembly.GetCallingAssembly());
return DoSimpleString(FileToString(fileName), input, inputName, outputType);
}
#endregion
#region File I/O
///
/// Returns a string of all the text in the given file
///
/// The name of the script file to read
/// A string of all the text in the given file
[DebuggerHidden]
private static string FileToString(string fileName)
{
StringBuilder sb = new StringBuilder();
using (FileStream stream = new FileStream(fileName,FileMode.Open,FileAccess.Read, FileShare.Read))
using (StreamReader reader = new StreamReader(stream))
{
string line;
while ((line = reader.ReadLine()) != null)
{
sb.AppendLine(line);
}
}
return sb.ToString();
}
#endregion
#region Exception Handling
///
/// Condenses the collection of compiler errors into a single exception string
///
/// The collection of errors made from compiling
/// A single exception containing all the compiler errors
private static Exception HandleErrors(CompilerErrorCollection cec)
{
StringBuilder sb = new StringBuilder();
foreach (CompilerError ce in cec)
{
sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "({0},{1}) : {2} {3}: {4}",
new object[] { ce.Line, ce.Column, ce.IsWarning ? "warning" : "error", ce.ErrorNumber, ce.ErrorText }));
}
return new ScriptCompileException(sb.ToString());
}
#endregion
#region Currently Unused
// TODO:
// ///
// /// Compiles the given source and executes a static
// /// function of a class taking a list of parameters
// ///
// public static object Execute(string source, string className, string functionName, object[] parameters)
// {
// return Execute(CompileCode(source), className, functionName, parameters);
// }
// ///
// /// Compiles the given source and executes a static
// /// function of a class taking no parameters
// ///
// public static object Execute(string source, string className, string functionName)
// {
// return Execute(CompileCode(source), className, functionName, null);
// }
// ///
// /// Execute a member function of a class object that takes no parameters
// ///
// public static object Execute(Assembly a, string className, string functionName, ref object callingObject)
// {
// return Execute(a, className, functionName, null, ref callingObject);
// }
// ///
// /// Execute a static class function that takes parameters
// ///
// public static object Execute(Assembly assembly, string className, string functionName, object[] parameters)
// {
// object o = null;
// return Execute(assembly, className, functionName, parameters, ref o);
// }
// ///
// ///
// /// Execute a static class function that takes no parameters
// ///
// public static object Execute(Assembly assembly, string className, string functionName)
// {
// object o = null;
// return Execute(assembly, className, functionName, null, ref o);
// }
// ///
// /// Execute a member function of a class object
// ///
// public static object Execute(Assembly a, string className, string functionName, object[] parameters, ref object callingObject)
// {
// object o = null;
// if (functionName.Length > 0)
// {
// Type t = a.GetType(className);
// MethodInfo info = t.GetMethod(functionName);
// if (parameters == null) parameters = new object[0];
// if (info != null)
// {
// if (info.IsStatic)
// {
// o = info.Invoke(null, parameters);
// }
// else
// {
// if (callingObject == null)
// {
// callingObject = t.Assembly.CreateInstance(t.FullName);
// }
// o = info.Invoke(callingObject, parameters);
// }
// }
// }
// else
// {
// Type t = a.GetType(className);
// callingObject = t.Assembly.CreateInstance(t.FullName);
// }
// return o;
// }
// ///
// ///
// /// Compile a C# source string into an Assembly
// ///
// private static Assembly CompileCode(String source)
// {
// CompilerResults cr = csp.CompileAssemblyFromSource(cp, new string[] { source });
// if (!cr.Errors.HasErrors)
// {
// return cr.CompiledAssembly;
// }
// throw HandleErrors(cr.Errors);
// }
#endregion
}
///
/// A basic class which all scripts will extend.
///
public abstract class SimpleScript
{
///
/// Run
///
public virtual void Run() { }
///
/// Run and take a parameter
///
public virtual void Run(object input) { }
///
/// Run and Return
///
public virtual object RunAndReturn()
{
return null;
}
///
/// Run with a parameter and return.
///
public virtual object RunAndReturn(object input)
{
return null;
}
}
///
/// An Exception to be thrown when a script fails to compile.
///
[Serializable]
public class ScriptCompileException : Exception
{
///
/// Initializes a new instance of the ScriptCompileException.
///
public ScriptCompileException() { }
///
/// Initializes a new instance of the ScriptCompileException with a specified error message.
///
/// The message that describes the error.
public ScriptCompileException(string text) : base(text) { }
///
/// Initializes a new instance of the System.Exception class with a specified error message and a reference to the inner exception that is the cause of this exception.
///
/// The error message that explains the reason for the exception.
/// The exception that is the cause of the current exception.
public ScriptCompileException(string text, Exception innerException) : base(text, innerException) { }
///
/// Initializes a new instance of the System.Exception class with serialized data.
///
/// The System.Runtime.Serialization.SerializationInfo that holds the serialized object data about the exception being thrown.
/// The System.Runtime.Serialization.StreamingContext that contains contextual information about the source or destination.
protected ScriptCompileException(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { }
}
}