JavaScript Call Stack Dumper
Draft 1
Introduction
The purpose of this document is to explain how to use the code I've added to XPConnect which allows the native and JavaScript programmer to manually or programatically dump the current JavaScript call stack to the native console.
The system I describe below will allow programmers to see the current state of the JavaScript call stack (with arguments and locals) and do arbitrary JavaScript evaluations while stopped in the native debugger. I've also added support for dumping the JavaScript call stack via the 'debugger' keyword in JavaScript source code.
This support will work regardless of whether or not the JavaScript code uses XPConnect.
An alternative to the method described here is to iterate over Components.stack.
From within the native debugger...
I've exported two global symbols from the xpconnect and jsdom DLLs:
extern "C" void DumpJSStack(void); extern "C" void DumpJSEval(PRUint32 frameno, const char* text);
These functions can be called manually from within the native debugger [I've tested only NT. This should work in Linux debuggers. On Mac???].
In msdev (Microsoft's Win32 debugger) these functions can be
called from the "Quick Watch" dialog or by adding the call to
the 'Watch" window. Note that this debugger can only
resolve and call the functions if the current frame as selected
in the "Call Stack" or "Variables" window is a frame from one of
the DLLs where these symbols are exported; i.e. the xpconnect or
jsdom DLLs. A successful call to one of these functions should
send output to the native console and evaluate to
<void>
in the Watch or Quick Watch window. An
attempt to call from a frame where the debugger cannot find the
symbol will result in something like CXX0017: Error: symbol
"DumpJSStack" not found
.
DumpJSStack()
will attempt to dump the JS call
stack for the JSContext on the current thread. It uses the JSContext
which is pushed onto the ThreadJSContextStack
by the
DOM and other code in mozilla before calling into JavaScript. The
'top' JSContext will be used regardless of which native frame is
selected in the debugger.
The format for the dump is explained below.
DumpJSEval()
takes two params: 1) the zero based
frame number in which to do the eval, 2) the source text to eval.
The text evaled and the result are echo'd to the
native console:
Calling DumpJSEval(0, "1+1")
might print:
js[0]>1+1 2
Keep in mind that although calling DumpJSStack()
should have no effect on the running JS code, calls to
DumpJSEval()
may have side effects. This
can be powerful if used creatively.
Dumping the JavaScript stack from native code...
You can easily write C++ code that will dump the JavaScript call stack using XPConnect service. Look here for the declarations and examples. Here is a simple sample call...
nsresult rv; NS_WITH_SERVICE(nsIXPConnect, xpc, nsIXPConnect::GetCID(), &rv); if(NS_SUCCEEDED(rv)) xpc->DebugDumpJSStack(PR_TRUE, PR_TRUE, PR_FALSE);
We might want to add such a call to the JavaScript error reporter.
The JavaScript debugger
statement
JavaScript has a debugger
keyword which can appear
in JavaScript source. Older JavaScript engines will flag its use
as a syntax error (use of a reserved word). But newer engines will
allow the keyword. Engine embedders can install a callback hook to
be called if and when the keyword is reached during script
execution. At some future point there may be a JavaScript Debugger
which will install such a hook and pop up a fully featured debugging
window when this event fires. For now I have created such a hook
in xpconnect which is set in DEBUG builds only. If the
debugger
keyword is reached then the JavaScript stack
will be dumped to the native console (same as if
DumpJSStack()
had been called.
After hitting the debugger
keword and dumping
the JS call stack, JS execution will continue as if nothing unusual
had happened. If you would like this to stop in your native
debugger then set a breakpoint in xpc_DebuggerKeywordHandler
in your native debugger.
The debugger
statement can be very useful as a
kind of super dump
as you develop your JavaScript
code. You can also use it as a kind of assert:
if(some_unusual_condition) debugger;
Note 1) in the example above the test will always happen
even though debugger
might have no effect in non-DEBUG
builds 2) at some future point someone is bound to ship a
JavaScript Debugger that will catch these cases (even in non-DEBUG
builds). When that happens, users trying to debug their own JS
code might trip over your debugger
statements. So,
while these debugger
statements might be very
useful as you write code, you may not want to leave them in
permanently.
The Dump Format
The format of the dump output is like:
frame_number function_name(argname = argvalue) ["filename":line_number] local_variable1 = value local_variable2 = value this = value
So an example frame might look like:
0 f(arg1 = [function], arg2 = "bar") ["debug.js":6] local1 = 1 local2 = "second local" this = [object global]
Some things to note...
- In the browser the 'filename' will often be a URL.
- Some places in mozilla that evaluate JavaScript will not
supply a filename, so the dump may report something like
[<unknown>:0]
. - If a function is called with more arguments than its declaration names, then the additional trailing argument values will be displayed without the "argname = " part.
- Functions passed as arguments or stored as variables are displayed as simply "[function]".
- If the type of an argument or variable is 'string' then it will be displayed quoted.
- Functions declared without a name are displayed with the name "anonymous".
- Frames that are not functions are displayed as "<TOP LEVEL>".
- Native (non-JavaScript) frames in the call stack are displayed as "[native frame]".
So, the following (contrived) code stored in a file called "debug.js"
/* an example... */ function f(arg1, arg2) { var local1 = 1; var local2 = "second local"; debugger; // <-- stack is dumped here } var o = {foo : "fu", bar : function(a,b){f(a,b)}}; function g(a1, a2, a3, a4) { var alocal = "something"; var l4 = a4; var localFromArg = a1; o.bar(a1,a2); } g(f, "bar", {foo : "f", bar : "b"}, [1,2,3], "extra", 123);
...might cause output like...
------------------------------------------------------------------------ Hit JavaScript "debugger" keyword. JS call stack... 0 f(arg1 = [function], arg2 = "bar") ["debug.js":5] local1 = 1 local2 = "second local" this = [object global] 1 anonymous(a = [function], b = "bar") ["debug.js":8] this = [object Object] 2 g(a1 = [function], a2 = "bar", a3 = [object Object], a4 = 1,2,3, "extra", 123) ["debug.js":14] alocal = "something" l4 = 1,2,3 localFromArg = [function] this = [object global] 3 <TOP LEVEL> ["debug.js":17] this = [object global] ------------------------------------------------------------------------
Conclusions
Please people, use this to help yourselves understand exactly what your JavaScript code is doing.
And send feedback.