/* * 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) { } } }