Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / ATL

Function Call Tracing in JScript

4.58/5 (18 votes)
3 Jul 2007CPOL15 min read 1   6.5K  
Comprehensive JScript function call tracing without code modification.

Screenshot - FlopTrace.gif

Introduction

One of the most tedious aspects of developing JScript 'applications' is the lack of somewhere to write debugging and trace information. Most often, you have to litter your code with alert statements or write stuff to file in order to monitor what's going on. While Dev Studio 2005 does provide the TracePoint facility, this requires manually setting each TracePoint under the development environment.

The Java Script Debug (JSD) component is designed to overcome these problems, by providing the following:

  • automatic tracing of all JScript function calls - without any modifications to your code.
  • a client API for outputting trace information from within your JScript code.

Output can be viewed using any utility that intercepts and displays the OutputDebugString output, e.g., SysInternals' (actually now Microsoft's) DebugView utility.

Background

To illustrate how JSD works, this section will use the example of some JScript code running within Internet Explorer. Remember, however, that JSD works with any active script host.

Consider the following SCRIPT element within an HTML page loaded by IE:

HTML
<SCRIPT language= "jscript">
    window.title="JSD Example"
</SCRIPT>

IE itself does not handle script code; the scripting model used by Microsoft is partitioned so that different components are used to handle hosting, script parsing and execution, and debugging. These disparate components communicate via a set of well defined interfaces. The advantage of this approach is that any one of these components can be replaced without the need to change the others.

When IE processes the language attribute in the above example, it looks in the registry for a COM component that is registered with the "jscript" ProgID. This will be the Microsoft JScript engine. To use a different scripting engine, simply use a different name for the language attribute. As long as this maps to a COM component that supports the required ActiveScript interfaces, IE will use that engine to process the script. The other name most commonly used is, of course, "vbscript".

As part of establishing the connection to the scripting engine, IE will call the SetScriptSite method and pass an IActiveScriptSite pointer. This pointer is used by the scripting engine to call back to IE in a number of different situations.

After creating the scripting engine, IE will pass the code within the SCRIPT element for parsing using the engine's ParseScriptText method.

Eventually, this code will be executed in response to user-interaction or events (e.g., onload) within IE. At this point, the scripting engine will encounter the "window" object. The engine has no knowledge of this object, and so will call on the IActiveScriptSite pointer to get a dispatch interface to the object. It, then, uses this interface to set the "title" property of the object.

Armed with this information, it is (relatively) easy to devise a strategy to achieve the goal of tracing JScript function calls:

  • Create a scripting engine that supports the necessary interfaces.
  • Get IE to use this engine rather than the real JScript engine.
  • Instrument the script code with trace code before the script is parsed.
  • Output trace statements when the script is executed.

The steps are described in the following sections.

Creating a Scripting Engine

There is no need to create a scripting engine from scratch (thankfully) as we are only interested in intercepting a few methods on the IActiveScript interface. Instead, we simply create a COM component that wraps an instance of the Microsoft JScript engine.

When IE calls SetScriptSite, our engine creates an instance of a component that implements the IActiveScriptSite interface and caches the interface pointer from IE. It also creates an instance of the COM object which is responsible for outputting the trace information (see below). This object is referred to in script using the name __JSD.

When IE calls the ParseScriptText method on our interface, the script code is passed to the parser, and points of interest (see below) are instrumented with method calls to the __JSD object. Here, 'instrumentation' simply means inserting the appropriate script text.

Apart from the methods described above, all other calls are simply delegated to the 'real' instance of the engine. Additionally, any query interface calls for additional interfaces are simply delegated using the ATL COM_INTERFACE_ENTRY_FUNC_BLIND mechanism.

Engine Usage

There are two ways to get IE to use the JScriptDebug engine:

Explicit Usage

This requires that you tell IE in your code that you want to use JSD. For example, the following <SCRIPT> element does this:

HTML
<SCRIPT language="jscriptdebug"> 
// Script contents here
</SCRIPT>

When IE sees the language attribute, it looks for a script engine registered under the "jscriptdebug" ProgId, and creates an instance of it. While this is simple to do, it does not meet the design goal of not having to modify code.

Implicit Usage

To trace functions without having to tell IE that it should use JSD instead of the proper JScript engine, we need implicit usage. To achieve this, it is necessary to modify the InProcServer32 setting of the proper JScript engine so that it points to jscriptdebug.dll and not jscript.dll.

The location of the JScript scripting engine is stored under the following registry key:

HKEY_CLASSES_ROOT/CLSID/{f414c260-6ac0-11cf-b6d1-00aa00bbbb58}/InProcServer32

The default value is the location of the DLL; it will be something like this:

c:\winnt\system32\jscript.dll

To use JSD instead, the entry is simply replaced with the path to the JSD DLL; for example:

c:\utils\jscriptdebug.dll

Now, all attempts to use the JScript engine will result in the use of JSD instead. Implicit usage of JSD is referred to as Replace Mode.

Parsing and Instrumentation

When IE passes a block of script code to JSD, it is parsed to look for any function entry and exit points. Each instance found is instrumented with additional code which emits debug strings to describe the function and its arguments. The instrumented code is then passed to the managed instance of the real JScript engine which does the actual scripting work.

The injected code consists of method calls on the global __JSD object described above.

The code for a function entry looks like this:

JavaScript
__JSD._TraceFn(arguments,false);

and for the function exit:

JavaScript
__JSD._TraceFnExit(arguments,false);

Note that (in common with many of the other JSD functions listed below), these functions are called with an argument of "arguments". This object is an implicit member of every function object, and is used by JSD to extract the values of the arguments passed to the function. JSD can also use this object to find out the name of the function. In most situations, you could also pass the function object by name, e.g.:

JavaScript
function foo() {
   __JSD._TraceFn(foo); 
}

and JSD could get the "arguments" object from the function object. However, for an anonymous function, e.g.:

JavaScript
var fn = function() {
   __JSD._TraceFn(???);
}

there is no function name to pass to JSD. Passing the arguments object works in both situations.

Any return statements in the code are also replaced so that all possible exit points are covered, for example:

JavaScript
if (bFinished)
    return true;

is replaced by:

JavaScript
if (bFinished)
    {var __x=true;__JSD._TraceFnExit(arguments,__x); return __x;}

Again, "arguments" is used to access the function name (see above).

If the parser fails for any reason, the un-instrumented script is passed to the scripting engine instead, and an error message is written to the output stream.

Note that any code which is instrumented by JSD has the conditional compilation variable @JSD added. This provides an easy way to control the use of the client API - see below.

Generating Trace Output

All calls to trace output which are made on the __JSD object ultimately result in a call to the Win32 API function, OutputDebugString. These emitted strings can be viewed by running a suitable viewer - like this one from SysInternals.

To help filter these messages from other calls to OutputDebugString, all trace statements are preceded by the "JSD:" prefix. Strings are also indented to show levels of nesting for each function.

The __JSD object gets the information it requires (function name and passed arguments) from the "arguments" object, which is passed in as part of the instrumented function call. It is a simple case of calling the appropriate methods on the dispatch interface of this object to read the required values.

Using the code

As described above, JSD gives you trace information to assist debugging without the need to modify your code. Just make sure the component is registered as described above and run your application, and the output will be produced.

JSD also exposes a client API which provides:

  • Control over implicit usage.
  • An additional set of methods for incrementing your code to output debugging information.

Control Methods

The following methods affect the implicit JSD calls:

  • Reset
  • Resets internal values, and also works out the current indent level based on the current call stack.

    This is most useful when you have thrown an exception from deep within a nested call to a try/catch handler high up the call stack. If you don't call Reset, JSD will start tracing the next lot of functions at whatever level of indentation was reached before the exception was raised. Fairly soon, the trace statements will disappear off the output!

    Usage:

    JavaScript
    catch(e)
    {
       __JSD.Reset(arguments); 
      reportError("Exception caught in global catch handler");
    }
  • Trace
  • Turns tracing on/off from this point in the code until it is turned on/off again with another call. Takes a boolean argument.

    JavaScript
    var bPrevious = __JSD.Trace;   // get previous setting
    
    __JSD.Trace = true;
    
    //  Do something
    
    __JSD.Trace = bPrevious;    // reset to previous setting

Additional API Methods

All API methods are member functions of the global JSD object - which is available to your code under the name __JSD.

The __JSD object will be available if you are running in Replace Mode but not when you are using the normal JScript engine. To enable your code to run without problems in both situations, you can use the @JSD conditional compilation flag which is added when JSD parses your code, e.g.:

JavaScript
@if (@JSD)
       __JSD.ClearTrace();
@end

Note - API methods such as TraceText always emit trace statements regardless of the state of the global trace flag (see above).

The API methods are as follows:

  • TraceText
  • Inserts the text string into the stream of JSD trace messages. Usage:

    JavaScript
    __JSD.TraceText("Halfway through function foo");
  • TraceStack
  • This writes out a trace of the functions in the current call stack and displays the arguments to each one. The stack trace is also returned as a string. Usage:

    JavaScript
    alert("Error occurred here : " + __JSD.TraceStack(arguments));

    This method also takes an (optional) second parameter of type bool - if this is present and set to false, the current function is not included in the call stack, only those above it.

  • TraceFn
  • Records entry to the function and lists any arguments. Usage:

    JavaScript
    __JSD.TraceFn(arguments);

    This function also takes an optional name which overrides the actual name of the function. The real reason for this parameter is to allow for naming of anonymous functions (see below).

    JavaScript
    function 
        foo(){
       __JSD.TraceFn(arguments,"callMeBar");
    }
  • TraceFnExit
  • Records exit from a function, and optionally the value returned from the function. Usage:

    JavaScript
    __JSD.TraceFnExit(arguments,"bye");
  • ClearTrace
  • Outputs the special debug string DBGVIEWCLEAR. This can be intercepted by any viewer of debug messages, and used as an instruction to clear the messages on display. DebugView works in this way (what a lucky coincidence!). Usage:

    JavaScript
    __JSD.ClearTrace()

JSD Directives

In some situations, it is necessary to instrument the code to control JSD. This is done in the form of directives embedded within comments.

  • _JSD_NO_TRACE_
  • JSD instruments each call immediately before the first statement in the function. This means that the following:

    JavaScript
    function foo() { __JSD.Trace(false); }

    will still trace all calls to foo as the injected code is called before the Trace method turns the trace output off. To turn off tracing of a specific function call, you need to instrument the function declaration like this:

    JavaScript
    function foo() /* _JSD_NO_TRACE_ */  {
         // Body of function
    }

    or like this:

    JavaScript
    function foo() // _JSD_NO_TRACE_ {
    
        // Body of function
    }

    Do this when you need to avoid tracing a function like onmousemove that is firing constantly and generating too much output.

    Additionally, if the first 128 bytes of your script contains this directive:

    JavaScript
    /* _JSD_NO_TRACE_ */

    then the whole file is excluded from the instrumentation process.

  • _JSD_TRACE_
  • This is the opposite of _JSD_NO_TRACE_ and ensures that the function is traced (even if the global trace flag is set to off). In reality, this directive is not required as putting __JSD.TraceFn at the start of the function would do the same job. It has the benefit of being ignored by the real JScript engine if this is being used directly, whereas an explicit call to TraceFn would need to be removed or guarded in some way (conditional compilation etc.).

  • _JSD_NAME_
  • Allows you to give a name to an otherwise anonymous function. E.g.:

    JavaScript
    foo.doIt = function() /* foo::doIt */ 
    {
       // Body of function
    }

    gives a name to the member function of the foo class.

    If anonymous functions are not named in this way, JSD will generate a name for each function, as follows:

    JScript_Anonymous_Function_XXX

    where XXX is simply an integer, incremented each time an anonymous function is parsed by JSD.

Settings

The JSD Configurator utility can be used to manage all the JSD settings. The settings are described fully in the help text supplied with this application as well as summarised here.

There are a number of parameters which allow you to have some control over the output of JSD:

  • Trace - Sets the default value for tracing the output of instrumented functions; 1 is on by default, 0 is off by default. Even when this setting is set to zero, calls to the API functions (see below) will still be traced.
  • Indent - Number of spaces that are added for each level of indentation for nested function calls. Default is 3. Use 0 to turn off indentation.
  • Instrument - Determines whether to instrument code to trace function entry and exit. If this value is present and set to zero, tracing is turned off, otherwise instrumentation is enabled. You would turn this off when you want to use the features of the JSD API explicitly and don't want each and every call logged.

Opt-In

Because implicit usage means that JSD is used anywhere that JScript would have been used (for instance, the Windows Explorer search window, Windows logon scripts, MSDN document explorer, etc.), you must explicitly opt-in to get this implicit usage (so it's not really implicit!).

This additional level of configuration is mostly to avoid any bugs that are introduced during development from causing nasty problems. While (hopefully) this is not an issue for the release version, it is still prudent to limit the usage of JSD in this way - just in case!

To opt-in to JSD usage requires that your application is listed under the JScriptDebug registry key:

Screenshot - TraceApps.gif

Only applications listed here get the trace functionality, by default, and only if the DWORD value is set to 1. All other applications will use the Microsoft JScript engine.

Problems / Issues / Outstanding Features

  • The parser is a hand-crafted, ad-hoc solution. It will not successfully parse all the JScript code that the Microsoft script engine will parse. However, it works most of the time, and if you write 'standard' JScript code, you won't have any problems. If you like to push the boundaries of readability in your code, then this component is not for you!
  • You must use a semi-colon at the end of a return statement, even if you are returning a function definition. The script parser in JSD will fail if it doesn't find this token. The parser could be improved to avoid this need, but this would involve some reasonably tricky modifications to what is otherwise a relatively simple parser. Semi-colons are cheap, so for the moment, it's not really an issue.
  • The parser will probably break if it finds a regular expression like this:
  • JavaScript
    var re = new /}/;

    The parser will not notice that the "}" character is in a regular expression and therefore should be ignored. This should be fairly easy to fix, but a simple workaround is to use quoted strings for the regular expression.

  • When JSD is loaded but the application is not listed in the TraceApp key, an instance of the real JScript engine is returned and the JSD instance deletes itself. This should mean that the JSD DLL can be freed by the application and unloaded. Despite DllCanUnloadNow returning S_OK and the reference count being correctly returned to zero, JSD is not unloaded by some applications.
  • Although the parser passes the un-instrumented to the JScript engine when it recognizes that it has failed, there may be situations where it thinks it has succeeded but actually emits code that doesn't compile. Caveat emptor!
  • The same principles could be used to instrument VBScript. I don't tend to use VBScript, so I haven't had the motivation to do this.
  • Because the source has been changed by the instrumentation process, the flag SCRIPTTEXT_HOSTMANAGESSOURCE has to be removed when passing the modified source to the engine. Otherwise, the source code is not correctly aligned when single-stepping in the debugger. One consequence of removing this flag is that Visual Studio 2005 (for instance) no longer applies the syntax highlighting to the source when debugging. There may be a solution to this problem, but I haven't found it yet!
  • The project is VC6, but would benefit from an update (ATL7.0 has a C++ regular expression parser - the current parser uses the VB RegExp object by importing the DLL).
  • The code for the JSD Configurator is not provided. This application demonstrates nothing of interest, and so only the binary is available.
  • Probably plenty more!

Summary

On its own, tracing function calls is only half the story. It doesn't tell you anything that goes on between the function entry and exit. Quite a while ago, I played around with an excellent utility component called The Script Adapter. This component wraps COM objects in order to solve a number of scripting related issues - again, without the need to modify the source COM component.

While developing JSD, I also modified this component so that it provides similar trace functionality to that described here. This means, any method call for a wrapped component is written out using OutputDebugString along with any arguments and return values. If time permits (and there is enough interest), I will make this component available.

Together, these two components have proved invaluable in tracking down problems and monitoring what's happening in my JScript code - maybe, I should just write better code! There may well be other utilities out there that do the same thing - I couldn't find one when I needed it, so I wrote this instead. The fun is always in the chase.

History

  • 05/07/07 - Links to JSD Configurator added.
  • 20/04/07 - First posted to CodeProject.

License

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