Click here to Skip to main content
15,881,803 members
Articles / Programming Languages / JScript .NET

A JavaScript Shell

Rate me:
Please Sign up or sign in to vote.
4.25/5 (4 votes)
18 Nov 2010CPOL4 min read 30.6K   738   29  
Bases to create your own JavaScript shell for your application
using Microsoft.JScript.Vsa;
using System.Reflection;
using System.Linq;
using System;
using System.Collections.Generic;
using Microsoft.JScript;
using System.CodeDom.Compiler;
using System.Text.RegularExpressions;

// Following pragma is to remove the warnings over the use of obsolete Microsoft.Vsa
#pragma warning disable 612
namespace Scaredfinger.JSShell
{
  /// <summary>
  /// Defines a base for shells
  /// </summary>
  public interface IShell : IDisposable
  {
    /// <summary>
    /// Gets whether the shell is running
    /// </summary>
    bool Running { get; }

    /// <summary>
    /// Adds a variable to the global scope
    /// </summary>
    /// <param name="name">Variable name</param>
    /// <param name="value">Variable's value</param>
    void AddGlobalObject(string name, object value);

    /// <summary>
    /// Evaluates a java script line and return the result
    /// </summary>
    /// <param name="textline">Javascript block to execute. block : js expression (; js expression)*</param>
    /// <returns>block output, if any</returns>
    object Eval(string textline);

    /// <summary>
    /// Creates an Event Handler from a specified function body. This handler would work as an instance method of the shell. This means
    /// withing its body, "this" would refer to the shell's scope. Sender and event args are available as "sender" and "e" respectively.
    /// </summary>
    /// <typeparam name="TEventArgs">Event args type, to create a strongly typed handler</typeparam>
    /// <param name="functionBody">Java script function body, just the body, no declaration nor curlies</param>
    /// <returns>Delegate instance</returns>
    EventHandler<TEventArgs> CreateEventHandler<TEventArgs>(string functionBody)
      where TEventArgs : EventArgs;

    /// <summary>
    /// Opens this shell. Starts running it, readies it for work
    /// </summary>
    void OpenShell();

    /// <summary>
    /// Closes this shell. No more operations are posible.
    /// </summary>
    void CloseShell();
  }

  /// <summary>
  /// Provides a basic implementation using existing VSA elements.
  /// </summary>
  /// <remarks>
  /// Many features of <see cref="Microsoft.JScript"/> relay on the use of a <see cref="Microsoft.Vsa.IVsaEngine"/>, which is
  /// obsolete, but not yet replaced; it is also obscure (very), poorly documented, dificult to use and the worst: inextensible, too many 
  /// internals, some of them abstract.
  /// 
  /// For all these reassons my shells are not suposed to be created directly but instead using a <see cref="IShellFactory{TShell}"/>.
  /// 
  /// To Create a new Shell:
  /// <list type="numbered">
  ///   <item>Create a class that extends shell</item>
  ///   <item>Mark the methods you'd like to make available from the shell with <see cref="ShellOperationAttribute"/></item>
  ///   <item>Create a <see cref="ShellFactory{TShell}"/></item>
  ///   <item>Create a shell instance, <see cref="ShellFactory{TShell}.CreateShell()"/></item>
  /// </list>
  /// 
  /// Javascript naming convention is the same as Java (Camelcase with lower start letter). I prefer Pascal (Upper Camelcase) case,
  /// so my examples would be Pascal case.
  /// 
  /// Sample implementation:
  /// <code>
  /// 
  ///namespace SampleShell
  ///{
  ///  public class SampleShell : Shell
  ///  {
  ///    public TextWriter Output
  ///    {
  ///      get;
  ///      set;
  ///    }
  ///    
  ///    public SampleShell()
  ///      : this(Console.Out)
  ///    {
  ///    }
  ///    
  ///    public SampleShell(TextWriter output)
  ///    {
  ///      Output = output;
  ///    }
  ///    
  ///    [ShellOperation]
  ///    [Description("Shows this help screen")]
  ///    public void Help()
  ///    {
  ///      foreach (var method in GetType().GetMethods().Where(x => x.GetCustomAttributes(typeof(ShellOperationAttribute), false).Any()))
  ///      {
  ///        var attr = method.GetCustomAttributes(typeof(DescriptionAttribute), false).FirstOrDefault() as DescriptionAttribute;
  ///        
  ///        var parameters = string.Join(",",
  ///          (
  ///            from p in method.GetParameters()
  ///            select string.Format("{0} {1}", p.ParameterType.Name, p.Name)
  ///          ).ToArray()
  ///        );
  ///        
  ///        var text = string.Format("\t{0} {1}({2}):\t{3}", method.ReturnType.Name, method.Name, parameters,
  ///          attr == null ? "" : attr.Description);
  ///          
  ///        Print(text);
  ///      }
  ///    }
  ///    
  ///    [ShellOperation]
  ///    [Description("Prints a text")]
  ///    public void Print(string text)
  ///    {
  ///      Output.WriteLine(text);
  ///    }
  ///    
  ///    [ShellOperation]
  ///    [Description("Closes this shell")]
  ///    public void Exit()
  ///    {
  ///      CloseShell();
  ///    }
  ///  }
  ///}
  /// </code>
  /// </remarks>
  public class Shell : IShell
  {
    #region Constants

    /// <summary>
    /// String pattern for handler functions
    /// </summary>
    const string StrEventHandlerBody = @"
  var __anonymous_Handler = function(sender, e)
  {{
    {0};
  }}
";

    #endregion

    #region Fields

    /// <summary>
    /// Scripting engine. Behind this reference is the power of the dark side...
    /// </summary>
    VsaEngine Engine
    {
      get;
      set;
    }

    /// <summary>
    /// Represents a private scope which known this instance
    /// </summary>
    private ShellScope Scope
    {
      get;
      set;
    }

    /// <summary>
    /// Gets or sets whether the shell is closed
    /// </summary>
    private bool Closed
    {
      get;
      set;
    }

    #endregion

    #region IShell Members

    /// <inheritdoc/>
    public bool Running
    {
      get;
      private set;
    }

    /// <inheritdoc/>
    public void AddGlobalObject(string name, object value)
    {
      if (value == null)
        throw new ArgumentNullException("value");

      if (string.IsNullOrEmpty(name))
        throw new ArgumentException("'name' cannot be null nor empty");

      if (Scope.HasField(name))
        throw new DuplicatedGlobalObjectException(name);

      // Adding global object to scope
      Scope.CreateField(name, value);
    }

    /// <inheritdoc/>
    public object Eval(string source)
    {
      if (!Running)
        throw new ShellNotRunningException();

      // Overriding default scope with a ShellScope
      // This ways shell functions and user defined global objects
      // are available from the shell
      Engine.PushScriptObject(Scope);
      try
      {
        // Executing the evaluate
        return Microsoft.JScript.Eval.JScriptEvaluate(source, Engine);
      }
      finally
      {
        // Restoring default scope
        Engine.PopScriptObject();
      }
    }

    /// <inheritdoc/>
    public virtual EventHandler<TEventArgs> CreateEventHandler<TEventArgs>(string functionBody)
      where TEventArgs : EventArgs
    {
      // Compile the function
      var function = Eval(string.Format(StrEventHandlerBody, functionBody)) as ScriptFunction;

      // Check if everything went ok
      if (function == null)
        throw new BadHandlerException();

      // Create the delegate
      return (sender, e) => function.Invoke(Scope, new[] {sender, e});
    }

    /// <inheritdoc/>
    public void OpenShell()
    {
      if (Running)
        throw new ShellAlreadyRunningException();

      // Gets the Vsa Engine. Remember, this is the power of the dark side....
      var getEngine = GetType().GetMethod("GetEngine", BindingFlags.NonPublic | BindingFlags.Instance);
      Engine = (VsaEngine)getEngine.Invoke(this, new object[0]);

      // Creates a scope to override the global scope
      Scope = new ShellScope(this, Engine.GetMainScope());

      Running = true;
    }

    /// <inheritdoc/>
    public void CloseShell()
    {
      Running = false;

      if (Closed)
        return;

      Closed = true;
    }

    #endregion

    #region IDisposable Members

    /// <inheritdoc/>
    public void Dispose()
    {
      CloseShell();

      // Closing Darth Engine
      Engine.Close();
    }

    #endregion
  }

  /// <summary>
  /// A Block scope with reference to the shell object.
  /// </summary>
  /// <remarks>
  /// This scope would override global scope and make available all shell functions and defined global objects. For each function in 
  /// <see cref="Target"/> shell, there will be a variable with the same name in the scope. Global variables would be instance or static 
  /// fields in the Scope object. <see cref="CreateField"/>
  /// 
  /// Let's see following example:
  /// 
  /// Shell definition:
  /// <code>
  /// class MyShell : Shell
  /// {
  ///   public void Foo() {...}
  ///   public int Goo(int x) {...}
  /// }
  /// </code>
  /// 
  /// From the shell it would be like if have done:
  /// <code>
  /// var __this = new MyShell();
  /// var Foo = __this.Foo ;
  /// var Goo = __this.Goo ;
  /// </code>
  /// 
  /// And using it would be completely transparent:
  /// <code>
  /// > Foo()
  /// // What ever MyShell::Foo() might output
  /// > Goo(1); Goo(2); // And so
  /// // What ever ...you know
  /// </code>
  /// </remarks>
  internal class ShellScope : BlockScope
  {
    #region Static Tools

    static readonly Regex RexGuidCleaner = new Regex("[{}-]", RegexOptions.Compiled);
    static readonly Random RndIdGenerator = new Random();

    static string CreateScopeName()
    {
      return RexGuidCleaner.Replace(Guid.NewGuid().ToString(), "_");
    }

    static int CreateScopeId()
    {
      return RndIdGenerator.Next(100, int.MaxValue);
    }

    const string StrFunctionCallFormat = "{0}.{1};";

    #endregion

    #region Fields

    /// <summary>
    /// Keeps a reference to the shell
    /// </summary>
    private IShell Target
    {
      get;
      set;
    }

    /// <summary>
    /// Holds the name of current scope
    /// </summary>
    private string ScopeName
    {
      get;
      set;
    }

    /// <summary>
    /// Numeric id for current scope
    /// </summary>
    public int ScopeId
    {
      get;
      set;
    }

    /// <summary>
    /// To avoid any posible thinkable name clash
    /// </summary>
    private string ShellReference
    {
      get { return "__this" + ScopeName; }
    }

    /// <summary>
    /// Keeps track of scope variables
    /// </summary>
    private readonly Dictionary<string, FieldInfo> _dicMembers = new Dictionary<string, FieldInfo>();

    #endregion

    #region Creation && Initialization

    /// <summary>
    /// A completely flexible constructor
    /// </summary>
    /// <param name="shell">The shell execuing code</param>
    /// <param name="parent">The parent Vsa scope, currently the global scope</param>
    /// <param name="scopeName">A name for he scope</param>
    /// <param name="scopeId">An int id for the scope</param>
    ShellScope(IShell shell, ScriptObject parent, string scopeName, int scopeId)
      : base(parent, scopeName, scopeId)
    {
      ScopeName = scopeName;
      ScopeId = scopeId;
      Target = shell;
      CreateFields();
    }

    /// <summary>
    /// Creates a new scope
    /// </summary>
    /// <param name="shell">The shell execuing code</param>
    /// <param name="parent">The parent Vsa scope, currently the global scope</param>
    public ShellScope(IShell shell, ScriptObject parent)
      : this(shell, parent, CreateScopeName(), CreateScopeId())
    {
    }

    /// <summary>
    /// Creates a field for each <see cref="ShellOperationAttribute">shell operation</see>.
    /// </summary>
    private void CreateFields()
    {
      // Creating shell reference var
      CreateField(ShellReference, FieldAttributes.Public, Target);

      // Creating a field for each function
      foreach (var member in Target.GetType().GetMethods().Where(x => x.GetCustomAttributes(typeof(ShellOperationAttribute), false).Any()))
        CreateField(member.Name, FieldAttributes.Public, CreateFunction(member));
    }

    /// <summary>
    /// Creates a scriptfunction reference to assign to variables named after functions
    /// </summary>
    /// <param name="member">Function</param>
    /// <returns>ScriptFunction</returns>
    private ScriptFunction CreateFunction(MethodInfo member)
    {
      try
      {
        // Pushing current scope in the top
        // So shell reference is available
        engine.PushScriptObject(this);

        // Creating the ScriptFunction Object 
        return (ScriptFunction)Eval.JScriptEvaluate(
          string.Format(StrFunctionCallFormat, ShellReference, member.Name), engine);
      }
      finally
      {
        // Restoring current scope
        engine.PopScriptObject();
      }
    }

    #endregion

    #region Public Members

    /// <summary>
    /// Queries the existance of specified field
    /// </summary>
    /// <param name="name">Field name</param>
    /// <returns>true if it does, false if it doesn't</returns>
    public bool HasField(string name)
    {
      return _dicMembers.ContainsKey(name);
    }

    /// <summary>
    /// Creates a new field, a global var for instance
    /// </summary>
    /// <param name="name">Field name</param>
    /// <param name="value">Field value</param>
    public void CreateField(string name, object value)
    {
      CreateField(name, FieldAttributes.Public, value);
    }

    #endregion

    #region Overrides

    /// <summary>
    /// Creates a new "global" variable
    /// </summary>
    /// <param name="name">Variable name</param>
    /// <param name="attributeFlags">Field attributes for variable creation</param>
    /// <param name="value">Initial value</param>
    /// <returns>The reflection object for this variable</returns>
    protected override JSVariableField CreateField(string name, FieldAttributes attributeFlags, object value)
    {
      if (_dicMembers.ContainsKey(name))
        throw new DuplicatedGlobalObjectException(name);

      var result = base.CreateField(name, attributeFlags, value);
      _dicMembers.Add(name, result);

      return result;
    }

    /// <summary>
    /// Finds matching scope member. Overrides default behaviour returning members from internal dicionary
    /// </summary>
    /// <param name="name">Member name</param>
    /// <param name="bindingAttr">Attributes to refine search</param>
    /// <returns>Found member.</returns>
    public override MemberInfo[] GetMember(string name, BindingFlags bindingAttr)
    {
      return _dicMembers.ContainsKey(name) ? new MemberInfo[] { _dicMembers[name] } : base.GetMember(name, bindingAttr);
    }

    #endregion
  }

  /// <summary>
  /// Base interface for generating Shells
  /// </summary>
  /// <typeparam name="TShell"></typeparam>
  public interface IShellFactory<TShell>
    where TShell : class, IShell, new()
  {
    void AddReference(Assembly assembly);
    void AddImport(string import);

    TShell CreateShell();
  }

  /// <summary>
  /// 
  /// </summary>
  /// <typeparam name="TShell"></typeparam>
  /// <remarks>
  /// This implementation would create a JScript.NET class derived from <see cref="TShell"/>, then it would compile it using
  /// <see cref="JScriptCodeProvider"/>. Resulting assembly would be able to many <see cref="Microsoft.JScript"/> features, including
  /// the obscure ones.
  /// <code>
  /// var factory = new ShellFactory&lt;MyShell&gt;() ;
  /// 
  /// factory.AddReference(system) ;            // Creates a reference to assembly System
  /// factory.AddImport("System");              // Allows using System types from the shell
  /// factory.AddImport("System.Reflection") ;  // Allows using System.Reflection types from the shell
  /// </code>
  /// </remarks>
  public class ShellFactory<TShell> : IShellFactory<TShell>
    where TShell : class, IShell, new()
  {
    #region Constants

    /// <summary>
    /// Javascript import format string
    /// </summary>
    const string StrImport = "import {0};";

    /// <summary>
    /// Shell class format string
    /// </summary>
    const string StrShell = @"
{2}
import {1};
import System;

class __ShellImpl extends {1}.{0}
{{

}}
";

    static readonly Regex RexImportValidator = new Regex("^[a-zA-Z_]\\w*(\\.[a-zA-Z_]\\w*)*$", RegexOptions.Compiled);

    /// <summary>
    /// Gets a compiler info to create default compiler parameters later
    /// </summary>
    /// <seealso cref="System.CodeDom"/>
    /// <seealso cref="CodeDomProvider"/>
    /// <seealso cref="CompilerParameters"/>
    static readonly CompilerInfo JsCompilerInfo = CodeDomProvider.GetCompilerInfo("JScript");

    /// <summary>
    /// Creates a JS
    /// </summary>
    static readonly JScriptCodeProvider JsCompiler = new JScriptCodeProvider();

    /// <summary>
    /// Holds a Type instance for shell type
    /// </summary>
    static readonly Type ShellType = typeof(TShell);

    /// <summary>
    /// Holds a default (shared) factory instance. Might be useull when creating Shell instances from diferent scopes, this way
    /// you don't need to "save" a shared instance on your own. But be aware, changes on a factory affect al Shells created with 
    /// it.
    /// </summary>
    public static ShellFactory<TShell> Default = new ShellFactory<TShell>();

    #endregion

    readonly List<Assembly> _lstReferences = new List<Assembly>();

    public void AddReference(Assembly reference)
    {
      if (reference == null)
        throw new ArgumentNullException("reference");

      _lstReferences.Add(reference);
    }

    readonly List<string> _lstImports = new List<string>();

    public void AddImport(string import)
    {
      if (! RexImportValidator.Match(import).Success)
        throw new ImportFormatException(import);

      _lstImports.Add(import);
    }

    public TShell CreateShell()
    {
      // Creating default parameters for the JS crompiler
      var options = JsCompilerInfo.CreateDefaultCompilerParameters();

      // Adding referenced assemblies to compiler parameters
      options.ReferencedAssemblies.AddRange(
          (from x in _lstReferences
           select x.Location).ToArray()
        );

      // These 2 will always be referenced
      options.ReferencedAssemblies.Add(ShellType.Assembly.Location);
      options.ReferencedAssemblies.Add("mscorlib");

      // Creating imports text
      // import System ;
      // import System.Reflection;
      // stuff like that
      var imports = string.Join("\r\n", (from import in _lstImports
                                         select string.Format(StrImport, import)).ToArray());

      // Formats the actual shell class
      var source = string.Format(StrShell, ShellType.Name, ShellType.Namespace, imports);

      // Compiles the shell class
      var result = JsCompiler.CompileAssemblyFromSource(options, source);

      // There should be no compilation errors
      if (result.Errors.Count > 0)
        throw new CriticalException("Something has gone real wrong!!");

      return (TShell)Activator.CreateInstance(result.CompiledAssembly.GetType("__ShellImpl"));
    }
  }

  /// <summary>
  /// To mark the methods available from the shell
  /// </summary>
  [AttributeUsage(AttributeTargets.Method)]
  public class ShellOperationAttribute : Attribute
  {

  }
}
#pragma warning restore 612

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect SunHotels
Spain Spain
I Received a Bachelor's Degree in Computer Science at the Mathematics and Computer Science Faculty, University of Havana, Cuba.

I mainly work in web applications using C# and some Javascript. Some very few times do some Java.

Comments and Discussions