JScript 中的函数调用跟踪






4.58/5 (17投票s)
无需修改代码即可进行全面的 JScript 函数调用跟踪。
引言
JScript“应用程序”开发中最繁琐的方面之一是缺乏用于编写调试和跟踪信息的地方。通常,您必须在代码中充斥着 `alert` 语句或将内容写入文件才能监视正在发生的事情。虽然 Dev Studio 2005 提供了 TracePoint 功能,但这需要在开发环境中手动设置每个 TracePoint。
Java Script Debug (JSD) 组件旨在解决这些问题,提供以下功能:
- 自动跟踪所有 JScript 函数调用 - 无需修改您的代码。
- 一个客户端 API,用于从您的 JScript 代码中输出跟踪信息。
可以使用任何拦截并显示 `OutputDebugString` 输出的实用程序来查看输出,例如 SysInternals(实际上现在是 Microsoft 的)DebugView 实用程序。
背景
为了说明 JSD 的工作原理,本节将使用一段在 Internet Explorer 中运行的 JScript 代码的示例。但是,请记住,JSD 可与任何活动脚本宿主配合使用。
考虑 IE 加载的 HTML 页面中的以下 `SCRIPT
` 元素:
<SCRIPT language= "jscript">
window.title="JSD Example"
</SCRIPT>
IE 本身不处理脚本代码;Microsoft 使用的脚本模型被划分为不同的组件,用于处理宿主、脚本解析和执行以及调试。这些独立的组件通过一组定义明确的接口进行通信。这种方法的优点是,这些组件中的任何一个都可以被替换,而无需更改其他组件。
当 IE 处理上面示例中的 `language` 属性时,它会在注册表中查找一个具有“jscript
”ProgID 的 COM 组件。这将是 Microsoft JScript 引擎。要使用不同的脚本引擎,只需在 `language` 属性中使用不同的名称即可。只要它映射到一个支持必需的 ActiveScript 接口的 COM 组件,IE 就会使用该引擎来处理脚本。另一个最常用的名称当然是“vbscript
”。
在建立与脚本引擎的连接时,IE 会调用 `SetScriptSite` 方法并传递一个 `IActiveScriptSite` 指针。脚本引擎使用此指针在多种不同情况下回调 IE。
创建脚本引擎后,IE 将使用引擎的 `ParseScriptText` 方法传递 `SCRIPT` 元素内的代码进行解析。
最终,这段代码将在 IE 中响应用户交互或事件(例如 `onload`)而执行。此时,脚本引擎将遇到“window
”对象。引擎对此对象一无所知,因此会调用 `IActiveScriptSite` 指针来获取该对象的派发接口。然后,它使用此接口设置对象的“title
”属性。
有了这些信息,就可以(相对)轻松地制定一个策略来实现跟踪 JScript 函数调用的目标:
- 创建一个支持必需接口的脚本引擎。
- 让 IE 使用此引擎而不是实际的 JScript 引擎。
- 在脚本被解析之前,用跟踪代码对脚本代码进行插桩。
- 在脚本执行时输出跟踪语句。
这些步骤将在以下各节中进行描述。
创建脚本引擎
谢天谢地,无需从头开始创建脚本引擎,因为我们只对拦截 `IActiveScript` 接口上的几个方法感兴趣。相反,我们只需创建一个包装 Microsoft JScript 引擎实例的 COM 组件。
当 IE 调用 `SetScriptSite` 时,我们的引擎会创建一个实现 `IActiveScriptSite` 接口的组件实例,并缓存来自 IE 的接口指针。它还会创建一个负责输出跟踪信息的 COM 对象实例(见下文)。脚本中使用此对象作为 `__JSD`。
当 IE 调用我们接口上的 `ParseScriptText` 方法时,脚本代码会被传递给解析器,并且会(稍后讨论)的兴趣点会被插桩为对 `__JSD` 对象的调用。这里的“插桩”仅仅是插入适当的脚本文本。
除了上面描述的方法之外,所有其他调用都只是委托给引擎的“真实”实例。此外,对其他接口的任何查询接口调用都只是使用 ATL 的 `COM_INTERFACE_ENTRY_FUNC_BLIND` 机制进行委托。
引擎使用
有两种方法可以让 IE 使用 JScriptDebug 引擎:
显式使用
这要求您在代码中告诉 IE 您想使用 JSD。例如,以下 `<SCRIPT>` 元素可以做到这一点:
<SCRIPT language="jscriptdebug">
// Script contents here
</SCRIPT>
当 IE 看到 `language` 属性时,它会查找在“jscriptdebug
”ProgID 下注册的脚本引擎,并创建一个实例。虽然这很简单,但它不符合不修改代码的设计目标。
隐式使用
要跟踪函数而无需告诉 IE 使用 JSD 而不是正确的 JScript 引擎,我们需要隐式使用。为此,必须修改正确 JScript 引擎的 `InProcServer32` 设置,使其指向 `jscriptdebug.dll` 而不是 `jscript.dll`。
JScript 脚本引擎的位置存储在以下注册表项下:
HKEY_CLASSES_ROOT/CLSID/{f414c260-6ac0-11cf-b6d1-00aa00bbbb58}/InProcServer32
默认值是 DLL 的位置;它会像这样:
c:\winnt\system32\jscript.dll
要改用 JSD,只需将条目替换为 JSD DLL 的路径;例如:
c:\utils\jscriptdebug.dll
现在,所有尝试使用 JScript 引擎的操作都将导致使用 JSD。JSD 的隐式使用称为替换模式。
解析和插桩
当 IE 将一块脚本代码传递给 JSD 时,会对其进行解析以查找任何函数入口和出口点。找到的每个实例都会被插桩额外的代码,该代码会发出调试字符串来描述函数及其参数。然后,插桩后的代码将被传递给执行实际脚本工作的真实 JScript 引擎的托管实例。
注入的代码包括对上面描述的全局 `__JSD` 对象的的方法调用。
函数入口的代码如下所示:
__JSD._TraceFn(arguments,false);
以及函数出口的代码:
__JSD._TraceFnExit(arguments,false);
请注意(与下面列出的许多其他 JSD 函数类似),这些函数都带有一个“arguments
”参数。此对象是每个函数对象的隐式成员,JSD 使用它来提取传递给函数的参数的值。JSD 还可以使用此对象来查找函数的名称。在大多数情况下,您也可以按名称传递函数对象,例如:
function foo() {
__JSD._TraceFn(foo);
}
JSD 可以从函数对象中获取“arguments
”对象。但是,对于匿名函数,例如:
var fn = function() {
__JSD._TraceFn(???);
}
没有名称可以传递给 JSD。传递 `arguments` 对象在这两种情况下都有效。
代码中的任何 `return
` 语句也会被替换,以便覆盖所有可能的出口点,例如:
if (bFinished)
return true;
将被替换为
if (bFinished)
{var __x=true;__JSD._TraceFnExit(arguments,__x); return __x;}
同样,“arguments
”用于访问函数名(见上文)。
如果解析器因任何原因失败,未插桩的脚本将被传递给脚本引擎,并在输出流中写入错误消息。
请注意,JSD 插桩的任何代码都会添加条件编译变量 `@JSD`。这提供了一种控制客户端 API 使用的简单方法(请参见下文)。
生成跟踪输出
对 `__JSD` 对象进行的所有跟踪输出调用最终都会导致对 Win32 API 函数 `OutputDebugString` 的调用。可以通过运行合适的查看器来查看这些发出的字符串 - 例如 SysInternals 的这个。
为了帮助从其他 `OutputDebugString` 调用中过滤这些消息,所有跟踪语句都以“JSD:”前缀开头。字符串也会缩进以显示每个函数的嵌套级别。
`__JSD` 对象从作为插桩函数调用的一部分传递的“arguments
”对象中获取所需信息(函数名和传递的参数)。只需调用此对象派发接口上的相应方法即可读取所需的值。
使用代码
如上所述,JSD 提供跟踪信息以帮助调试,而无需修改代码。只需确保按上述方式注册组件并运行您的应用程序,即可生成输出。
JSD 还公开了一个客户端 API,提供:
- 隐式使用的控制。
- 一组额外的方法,用于在代码中插桩以输出调试信息。
控制方法
以下方法会影响隐式的 JSD 调用:
Reset
重置内部值,并根据当前调用堆栈计算当前缩进级别。
这在您从嵌套到try
/catch
处理程序深处抛出异常时最有用。如果您不调用 `Reset`,JSD 将从异常发生前达到的缩进级别开始跟踪下一批函数。很快,跟踪语句就会在输出中消失!
用法
catch(e)
{
__JSD.Reset(arguments);
reportError("Exception caught in global catch handler");
}
Trace
从此代码点开始,直到再次通过调用打开/关闭跟踪。接受布尔参数。
var bPrevious = __JSD.Trace; // get previous setting
__JSD.Trace = true;
// Do something
__JSD.Trace = bPrevious; // reset to previous setting
附加 API 方法
所有 API 方法都是全局 JSD 对象的成员函数——该对象在您的代码中以 `__JSD` 的名称可用。
如果您在替换模式下运行,则 `__JSD` 对象将可用,但在使用普通 JScript 引擎时则不可用。为了使您的代码在两种情况下都能正常运行,您可以使用 JSD 解析代码时添加的 `@JSD` 条件编译标志,例如:
@if (@JSD)
__JSD.ClearTrace();
@end
注意 - `TraceText` 等 API 方法始终发出跟踪语句,无论全局跟踪标志的状态如何(见上文)。
API 方法如下:
TraceText
将文本字符串插入 JSD 跟踪消息流。用法:
__JSD.TraceText("Halfway through function foo");
TraceStack
这会输出当前调用堆栈中函数的跟踪,并显示每个函数的参数。堆栈跟踪也作为字符串返回。用法:
alert("Error occurred here : " + __JSD.TraceStack(arguments));
此方法还接受一个(可选的)类型为 `bool` 的第二个参数——如果存在且设置为 `false`,则当前函数不包含在调用堆栈中,只有它上面的函数才包含。
TraceFn
记录函数入口并列出任何参数。用法:
__JSD.TraceFn(arguments);
此函数还接受一个可选名称,该名称会覆盖函数的实际名称。此参数的真正原因是为了能够为匿名函数命名(见下文)。
function
foo(){
__JSD.TraceFn(arguments,"callMeBar");
}
TraceFnExit
记录函数退出,以及可选的函数返回值。用法:
__JSD.TraceFnExit(arguments,"bye");
ClearTrace
输出特殊的调试字符串 `DBGVIEWCLEAR`。任何调试消息的查看器都可以拦截此字符串,并将其用作清除显示消息的指令。DebugView 就是这样工作的(真是巧合!)。用法:
__JSD.ClearTrace()
JSD 指令
在某些情况下,需要对代码进行插桩以控制 JSD。这以嵌入在注释中的指令形式进行。
_JSD_NO_TRACE_
JSD 在函数的第一条语句之前立即插桩每个调用。这意味着以下代码:
function foo() { __JSD.Trace(false); }
仍然会跟踪 `foo` 的所有调用,因为在 `Trace` 方法关闭跟踪输出之前会调用注入的代码。要关闭特定函数调用的跟踪,您需要像这样插桩函数声明:
function foo() /* _JSD_NO_TRACE_ */ {
// Body of function
}
或者像这样:
function foo() // _JSD_NO_TRACE_ {
// Body of function
}
当您需要避免跟踪像 `onmousemove` 这样不断触发并产生过多输出的函数时,请执行此操作。
此外,如果您的脚本的前 128 字节包含此指令:
/* _JSD_NO_TRACE_ */
那么整个文件将被排除在插桩过程之外。
_JSD_TRACE_
这与 `_JSD_NO_TRACE_` 相反,并确保函数被跟踪(即使全局跟踪标志设置为关闭)。实际上,此指令不是必需的,因为在函数开头调用 `__JSD.TraceFn` 会起到相同的效果。它的好处是,如果直接使用真实的 JScript 引擎,它会被忽略,而显式调用 `TraceFn` 则需要被移除或以某种方式进行保护(条件编译等)。
_JSD_NAME_
允许您为匿名函数命名。例如:
foo.doIt = function() /* foo::doIt */
{
// Body of function
}
为 `foo` 类的成员函数命名。
如果匿名函数没有这样命名,JSD 将为每个函数生成一个名称,如下所示:
JScript_Anonymous_Function_XXX
其中,XXX 只是一个整数,每次 JSD 解析匿名函数时都会递增。
设置
可以使用 JSD Configurator 实用程序来管理所有 JSD 设置。这些设置在此应用程序随附的帮助文本中得到了充分描述,并在此处进行了总结。
有许多参数可以让您在一定程度上控制 JSD 的输出:
Trace
- 设置被插桩函数的输出跟踪的默认值;1 表示默认开启,0 表示默认关闭。即使此设置设为零,对 API 函数(见下文)的调用仍然会被跟踪。Indent
- 嵌套函数调用的每个缩进级别添加的空格数。默认值为 3。使用 0 关闭缩进。Instrument
- 确定是否插桩代码以跟踪函数入口和出口。如果存在此值且设置为零,则跟踪关闭,否则启用插桩。当您想显式使用 JSD API 的功能并且不希望每个调用都被记录时,可以关闭此选项。
选择加入
由于隐式使用意味着 JSD在任何 JScript 被使用的地方都会被使用(例如,Windows Explorer 搜索窗口、Windows 登录脚本、MSDN 文档浏览器等),您必须显式选择加入才能获得这种隐式使用(所以它不是真正隐式的!)。
这种额外的配置级别主要是为了避免在开发过程中引入的任何错误导致严重问题。虽然(希望)这对于发布版本来说不是问题,但谨慎起见,仍应限制 JSD 的使用方式——以防万一!
要选择加入 JSD 使用,需要您的应用程序列在 JScriptDebug 注册表项下:
默认情况下,只有列在此处的应用程序才能获得跟踪功能,并且只有当 `DWORD` 值设置为 1 时。所有其他应用程序将使用 Microsoft JScript 引擎。
问题 / 疑难解答 / 待完成的功能
- 该解析器是手工制作的、临时性的解决方案。它不会成功解析 Microsoft 脚本引擎将要解析的所有 JScript 代码。但是,它在大多数情况下都能正常工作,如果您编写“标准”JScript 代码,您不会遇到任何问题。如果您喜欢在代码中推断可读性的极限,那么此组件不适合您!
- 您必须在 `return` 语句的末尾使用分号,即使您要返回一个函数定义。JSD 中的脚本解析器在找不到此标记时会失败。可以改进解析器以避免这种需要,但这涉及到对否则相对简单的解析器进行一些相当棘手的修改。分号很便宜,所以目前这并不是一个真正的问题。
- 如果解析器遇到这样的正则表达式,它可能会崩溃:
var re = new /}/;
解析器不会注意到“}”字符在正则表达式中,因此应该被忽略。这应该很容易修复,但一个简单的变通方法是为正则表达式使用带引号的字符串。
摘要
仅跟踪函数调用只是故事的一半。它没有告诉你任何关于函数入口和出口之间发生的事情。很久以前,我玩过一个名为The Script Adapter 的优秀实用组件。该组件包装 COM 对象,以解决许多与脚本相关的问题——同样,无需修改源 COM 组件。
在开发 JSD 的同时,我还修改了这个组件,使其提供与此处描述的类似的跟踪功能。这意味着,包装组件的任何方法调用都将使用 `OutputDebugString` 输出,并附带任何参数和返回值。如果时间允许(并且有足够的兴趣),我将提供此组件。
总而言之,这两个组件在查找问题和监视 JScript 代码中的操作方面都发挥了不可估量的作用——也许我应该写更好的代码!可能还有其他实用程序可以做同样的事情——我需要它时找不到,所以我就写了这个。乐趣总是在追逐中。
历史
- 07/05/07 - 添加了 JSD Configurator 的链接。
- 07/04/20 - 首次发布到 CodeProject。