RSS
 

Global `eval` with a result in Internet Explorer

24 Aug

A while ago, I was charged with the task of writing a JavaScript console window for certain environments that didn’t have access to the Internet Explorer developer tools. Such environments include Windows Desktop Gadgets and applications that make use of the .NET or COM WebBrowser controls, that run on the Internet Explorer engine (MSHTML).

The first non-trivial task was figuring out how to make Internet Explorer execute code in the global scope and then get the result. Indirect eval doesn’t help me because I need to support document modes where IE’s eval implementation is broken. It was clear that I would have to use Internet Explorer’s proprietary window.execScript function, but I wasn’t sure how I would get the result to display in the console window. Looking around the web didn’t help. As usual, kangax had an article and even a solution for providing a somewhat cross-browser global eval. However for Internet Explorer, the solution didn’t return a result so I was back to square one and it was time to get my hands dirty.

To begin with, I tried what was a rather naive attempt at a quick solution:

function executeConsoleInput(code) {
    var result;
    window.execScript("console.___lastInputResult___ = " + code);
    result = console.___lastInputResult___;
    delete console.___lastInputResult___;
    return result;
}

Before I tested it I already knew what the big problems would be:

  • All input becomes an expression, no statements or declarations allowed. This means any of var, try, catch... finally, if...else, do... while, etc will throw a syntax error. Also, function foo() { } is treated as a function expression and not a declaration.

  • Aside from that major spanner in the works, the first semi-colon in our code finalizes the assignment to executeConsoleInput.result, so anything after that is evaluated but not part of the result.


So this really wasn’t a viable option unless I wanted to limit the console user to single expressions. However, I felt I was on the right track and it didn’t take me long to realize I could use both execScript and eval together:

function executeConsoleInput(code) {
    var result;
    window.execScript("console.___lastInputResult___ = eval('" + code + "')");
    result = console.___lastInputResult___;
    delete console.___lastInputResult___;
    return result;
}

Perfect! …well, almost. Although this worked perfectly on a few short statements it quickly became clear I’d still been a little naive. Certain characters in the input would, quite obviously, break eval. Line-breaks were the most troublesome. Eventually, I came up with this and added it to the solution:

code = code.replace(/["'\\]/g, "\\$&").replace(/\r\n/g, "\\n");

After re-reading kangax’s article, I decided to post a comment with my solution for a global eval that returns a result, but since I’ve been working a little more on the console recently I decided to upgrade the comment to a fully fledged article of my own.

What’s even more intriguing about this is that it could be a viable polyfill for global eval in IE 8 and lower and it definitely is viable for IE 9/10 running in compatibility modes. Why can I make this assertion? I set up a jsperf test that compares the two functions and in IE 9 compatibility modes the withEval function is faster! I could only test IE 7 in a virtual machine, where withEval was between 15-25% slower. Whether or not this is an acceptable loss compared to the result and compatibility mode gains is up for debate. Personally, I prefer the same behaviour across browsers and, generally, thousands of execScript(eval()) operations per second is enough for anyone. I would be very interested in an explanation for the IE 9 eval optimization, either way.

Update: It turns out I didn’t need to escape the input — as John-David Dalton points out in the comments, you can assign the expression to a global variable and give that to eval instead of a string value. D’oh, I’m not sure why that didn’t occur to me! This also works around a problem with unicode escape sequences in the string that I hadn’t gotten around to addressing yet.

If we add this to kangax’s cross-browser globalEval code, we can make it behave to consistently return a value across Internet Explorer versions less than 9 and other browsers:

var globalEval = (function() {
  var isIndirectEvalGlobal = (function(original, Object) {
    try {
      // Does `Object` resolve to a local variable, or to a global, built-in `Object`,
      // reference to which we passed as a first argument?
      return window.eval('Object') === original;
    }
    catch(err) {
      // if indirect eval errors out (as allowed per ES3), then just bail out with `false`
      return false;
    }
  })(Object, 123);

  if (isIndirectEvalGlobal) {
    // if indirect eval executes code globally, use it
    return function(expression) {
      return window.eval(expression);
    };
  }
  else if ("execScript" in window) {
    // if `window.execScript` exists, use it along with `eval` to obtain a result
    return function(expression) {
      globalEval.___inputExpression___ = expression;
      window.execScript("globalEval.___lastInputResult___ = eval(globalEval.___inputExpression___)");
      return globalEval.___lastInputResult___;    
    };
  }

  // otherwise, globalEval is `undefined` since nothing is returned
})();

// and a simple test, inspired by kangax's detection method:
alert((function (Object) {
    return globalEval("Object === 123"); // result is false
})(123));

There is a slight difference in implementation requirements here, though – this requires globalEval to be defined as a global variable (or a property of the window object).

 

Tags: , , , ,

Leave a Reply

 

 
  1. Zev Spitz

    August 27th, 2011 at 8:54 pm

    Will this console be publicly available? I do a lot of development in HTA.
    Until now I have been using an old version of Firebug Lite, but the newer versions do not work locally.

     
    • Andy

      August 28th, 2011 at 9:24 am

      Hi Zev,

      It’s always been my intention to release it publicly, but it’s a long way from being ready and I have to work on it in my spare time. Keep your eye on my blog, as this is where it will be released.

       
  2. John-David Dalton

    August 27th, 2011 at 10:07 pm

    Hi Andy,

    I’m the guy behind @kangax’s Fuse.js reference in his post over global eval. I also gave a small mention of capturing the global eval result in my JSConf EU 2010 talk (~4:26 mark) and later, in May 2011, produced a standalone globalEval that attempted to address calling globalEval recursively.

    I wanted to comment on a few things from your post:

    1) Some environments where (1,eval)(...) errors, like Opera 9.25, will still allow the indirect call `window.eval(…)`, so you might change to that.

    2) Firefox < 4 will still allow access to variables from the execution context of where eval() was called. For example in Firefox 3.6 this alert(globalEval('typeof isIndirectEvalGlobal')); will alert Object.

    3) You can avoid escaping the expression in IE by assigning it to a property of globalEval like globalEval.code = expression; executing globalEval.result = eval(globalEval.code).

    4) In the source of my example I cover some more IE gotchas, solutions to the Firefox < 4 issue, and how to use script injection as an alternate globalEval technique.

     
    • Andy

      August 29th, 2011 at 10:05 am

      Hi John-David,

      I saw the discussion you started on es-discuss regarding window.eval before I saw your comment and thought it was an interesting coincidence :-) When I read kangax’s article, I thought the technique behind Fuse.js sounded similar to what I was doing. The interesting part for me is, in testing IE 9 and its compatibility modes, using eval and execScript together was faster than just using execScript alone. I remembered reading kangax’s article before but forgot to follow up on my comment about taking a look at fuse.js.

      It seems you’ve definitely covered more ground browser-wise, though. To be honest, most of my work is in IE environments so I didn’t put as much research into an x-browser solution as I could have. Thanks for point number 3 – something that should have occurred to me but it didn’t even cross my mind, oops ;-) I was having problems replace-escaping unicode escape sequences and naturally those problems have gone now. I see there’s more work to be done on my side too, though – for example the recursion issue.

      Again, thanks for the heads up(s).

       
  3. Zev Spitz

    August 29th, 2011 at 6:55 am

    1) Have you seen this?
    http://code.google.com/p/jscriptshell/
    2) Would using Visual Studio as a debugger, fit your needs? If you don’t have a full Visual Studio, the Integrated Shell also does debugging.