Friday, May 27, 2005

Adding JScript.NET Macro Support to Your Application

I always have ideas for making FlexWikiPad better kicking around in the back of my head. Of course, I'm still cranking on getting basic functionality in place, so it'll be a long time before some of these ideas get realized, but it's still fun. One of the ideas that I thought about this morning was how to add macro support to the app, so people could add a little automation if they wanted to. Having been exposed a bit to Javascript lately, I've decided that it is an excellent language, with a great balance between power and ease of use...so it seems like a good choice for a scripting language to use from within FlexWikiPad. On thinking about it a little more, I decided that an even better choice is JScript.NET, which combines the niceties of the Javascript language with the power of the .NET libraries. Sweet.


I set out this morning to see if I could figure out what it would take to weld JScript.NET into a Windows Forms application. As it turned out, it was so easy to do, I didn't even need to look on the Internet to find one of the dozens of other implementations of this that must already exist. And it's too cool not to share. So here's the code:


using System.CodeDom.Compiler;
using System.Reflection;
using Microsoft.JScript;


// Create the compiler object
JScriptCodeProvider provider = new JScriptCodeProvider();
ICodeCompiler compiler = provider.CreateCompiler();

CompilerParameters options =
new CompilerParameters();
options.GenerateInMemory =
true;
// Generating an "executable" just means that the code will have an entry point, rather than simply
// being a collection of classes.
options.GenerateExecutable = true;
// Adding references is optional - mscorlib and Microsoft.JScript are automatically referenced
options.ReferencedAssemblies.Add("System.Windows.Forms");
CompilerResults results = compiler.CompileAssemblyFromSource(options, textBox1.Text);


// Run the newly generated assembly's Main method
Assembly assembly = results.CompiledAssembly;
MethodInfo entryPoint = assembly.EntryPoint;
entryPoint.Invoke(
null, new object[] { null } );


And that's it. The code above is the handler for a button I put on a form. The only other thing on the form is a multiline text box that holds the code I want to compile. In my case, I typed in


import System.Windows.Forms;
MessageBox.Show(”Hello world”);


as a simple test, but I could have used any valid JScript.NET program. Note that one of the benefits of JScript.NET is that it doesn't require me to explicitly create a Main method - anything at global scope will automatically be stuffed into one for me automatically. This sort of brevity is a real feature in a macro language, in my opinion.


As far as how to use this to achieve automation, that's the easy part: the way I've written this, the JScript.NET code will run in the same AppDomain as the application code that's calling it. So they can both easily access static members of any types they can both access. One way to handle this is to add this line of code


options.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().Location);


just before you call CompileAssemblyFromSource. That will make the generated assembly depend on the EXE that's calling it, allowing it to use any types that are defined within the exe. So for instance, a class like this in the application


public class Context {
 
private static Context _context;
 
private string _data;

 
public static Context Current {
   
get {
     
if (_context == null) {
        _context =
new Context();
      
}
     
return _context;
    }
  }
 
 
public string Data {
   
get { return _data; }
   
set { _data = value; }
  }
}


Would let me write macro code that looks like this


Context.Current.Name = "Craig";


and after the macro ran, I could retrieve the value “Craig” from within the EXE by simply reading Context.Current.Name. Or I could do the reverse, and set values in Context.Current before invoking the macro, and it would be able to read them. The possibilities are endless.

16 comments:

  1. Just have to say:



    This is COOL :)



    Im sure i'll be looking to this next time I have to do macro implementations :)



    Thanks for the tip :)



    Is there any limitations to what you can do in the code? Or does it have access to the complete .net assemblys?

    ReplyDelete
  2. Cool. But don't you need to worry about the fact that assemblies (including dynamically generated ones) are never unloaded? In other words, if you repeatedly run that code, you'll start filling up your memory with dynamically generated assemblies that never get cleaned up.

    ReplyDelete
  3. Klok: No limit - it is a fully compiled .net program. Pary on, Garth!



    Kevin: You are absolutely correct. Caching the generated assembly would take care of this problem, obviously, and would be pretty easy to do (as you no doubt know).

    ReplyDelete
  4. if you're willing to restrict yourself to VB.NET or JScript.NET, .NET comes with built-in scripting support: http://msdn.microsoft.com/msdnmag/issues/02/08/VisualStudioforApplications/

    ReplyDelete
  5. Note the DynamicMethod support in .net 2.0. Instead of generating a whole assembly, you can simply generate a method. *The method is subject to GC*. Now I only have to figure out how to generate DynamicMethods through a compiler instead of IL.

    ReplyDelete
  6. Putting this code into an ASPX web page is fun. You can rewire Console output from the program to a textbox on the same page by adding the following code to Craig's extract given here:



    Assembly assembly = results.CompiledAssembly;

    //put the next 3 lines in

    StringBuilder sb = new StringBuilder();

    StringWriter sw = new StringWriter(sb);

    Console.SetOut (sw);



    MethodInfo entryPoint = assembly.EntryPoint;

    entryPoint.Invoke(null, new object[] { null } );



    //then set a textbox on the webpage to the text held in the StringBuilder

    this.SomeTextBoxOnThePage.Text += sb.ToString();



    ReplyDelete
  7. And by "fun", you apparently mean "giant security hole". :)

    ReplyDelete
  8. ...so far, my compiler.aspx seems to have a single purpose in life and that is to allow arbitrary code to be executed on the web server...not exactly something Keith would gaze lovingly at...but fun none the less ;)

    ReplyDelete
  9. Your code is great. I'm having a lot of fun with it. However I'm having two problems:



    1. Each time I enter code into the textbox and run it the main executable's memory usage goes up. I think each click of my "Run Script" button is calling into memory a new assembly that stays there. Do you know how to unload an assembly after it has executed?



    2. How do I access the main form's properties and methods in a way better than:

    Application.OpenForms[0].Text = "(something)";



    Thank you for your hard work!

    Drew Barfield

    ReplyDelete
  10. 1. You're almost certainly correct. And in .NET 1.1, there is no way to unload an assembly. In .NET 2.0, there might be, but I don't know what it is. If I get a free minute, I'll try to look it up. Otherwise, you can always cause the load to happen in a different AppDomain, and shut down the AppDomain when you're done.



    2. What's wrong with the Context trick I showed? Just add a property that points to the main form.

    ReplyDelete
  11. I'm a Knuckle Head...



    I originally tried using the Context Class method and it failed... because I didn't "import" the EXE namespace in the macro. It works now!



    import System

    import System.Windows.Forms

    import JScriptTestBed

    MessageBox.Show( Context.Current.MainForm.Text );



    Displays "Form1" in a MessageBox



    Thanks!

    ReplyDelete
  12. Ah yes, that would do it. :)

    ReplyDelete
  13. Scripting Applications: Not Just Smalltalk

    ReplyDelete
  14. The code seems to work great except for parsing JavaScript objects. I need to parse something like the following:

    { id: 101, name: "Mark" }

    But I'm not sure how to get the object back. Invoke returns a object but it's always null. Any idea how to get this to work?

    thanks!

    ReplyDelete
  15. Great !

    I had some trouble running your sample, which I copy-pasted, just because of a stupid detail : double-quotes around “Hello World” are nice english typographic double quotes (“, ”), while JScript expects normal ugly straight double quotes (").

    Anyway, it gave me an opportunity to iterate through the error collection, which was a great experience.

    Thank you for the tips.

    ReplyDelete
  16. Sorry about that!

    ReplyDelete