Duktape 错误对象深度解析:创建、增强、回溯数据与 file/line 责任定位

原创2026-10-09 00:27:541,609 阅读
文章标签:语言运行时嵌入式解释器

Duktape 错误对象深度解析:创建、增强、回溯数据与 file/line 责任定位

Duktape 在标准 ECMAScript 的 Error 基础上提供了一整套增强机制:错误对象不仅拥有 name 与 message,还带有文件名、行号与可打印的堆栈回溯(traceback)。本文以 doc/error-objects.rst 为骨架,结合源码实现,系统讲解 Duktape 错误对象的创建与抛出流程、Duktape.errCreate / Duktape.errThrow 错误处理器、_Tracedata 内部回溯数据格式,以及 fileName / lineNumber 的"责任定位"(blaming)规则。读完本文,你将掌握如何配置错误消息详细级别、如何自定义错误增强逻辑、如何理解并处理 Duktape 的堆栈回溯,以及在低内存与安全敏感场景下的取舍方案。

错误消息详细级别(Error message verbosity levels)

标准 ECMAScript 的 Error 实例非常简陋,只包含 name 和 message。Duktape(以及大多数主流引擎)在此基础上额外提供了文件名、行号和 traceback。Duktape 通过两个编译期宏控制错误消息的详细程度:DUK_USE_VERBOSE_ERRORS 与 DUK_USE_PARANOID_ERRORS,共三种组合:

DUK_USE_VERBOSE_ERRORS DUK_USE_PARANOID_ERRORS 行为描述
set(默认) 未设置 消息包含出错的键/值内容,例如 number required, found 'xyzzy' (index -3)。这是默认行为。
set set 消息详细但不包含出错的键/值,例如 number required, found string (index -3)。适用于把错误消息中的键/值视为潜在安全泄露的场景。
未设置 忽略 错误对象不再携带真实错误消息,.message 被设置为错误码转换成的字符串。适用于极低内存目标平台。

对应的配置项定义位于 DUK_USE_VERBOSE_ERRORS.yaml(自 1.0.0 引入,默认开启,关闭后可减小 footprint 但错误信息大幅减少)与 DUK_USE_PARANOID_ERRORS.yaml(自 1.4.0 引入,默认关闭,同时被打上 ecmascript 与 sandbox 标签)。后者在源码注释中说明:默认情况下 Duktape 会对 "null.foo = 123;" 这类属性访问错误、"undefined()" 这类非法调用错误中涉及的基值(base value)与键(key)做摘要(通过 duk_push_readable_tval()),而启用 DUK_USE_PARANOID_ERRORS 后不再输出这些摘要。

原文档还记录了未来工作设想:提供一种"低内存错误消息"模式——错误消息字符串仍然存在,但把消息种类压缩到最少,例如所有 duk_require_xxx() 的类型不匹配统一为 "unexpected type",所有栈索引错误统一为 "invalid argument"。

错误增强总览(Error augmentation overview)

Duktape 允许错误对象在两个时点被增强:

  1. 创建时(creation):仅在 Error 实例上生效。Duktape 先为其添加 traceback(或 file/line 信息,取决于配置);然后若设置了 Duktape.errCreate,则调用它进一步增强或整体替换该错误对象。注意:只有原型链上包含 Error.prototype 的值才会被创建时增强,其他类型(即使是普通对象)一律不动;用户错误处理器也只收到 Error 实例。
  2. 抛出时(throw / re-throw):当任意值被抛出时,若设置了 Duktape.errThrow,则调用它以增强或替换被抛出的值。这里不限制类型——任意值都可以被抛出,因此该处理器必须谨慎处理所有值类型,并且要妥善应对重复抛出的情况。

通常情况下,创建时增强优于抛出时增强:对象只被创建一次,却可能被多次抛出/重抛;若在抛出点增强,重抛会导致错误被多次处理(覆盖之前的字段值),而且有些错误对象可能从未被抛出(例如 new Error('...') 后仅读取 .stack),却同样需要回溯信息。Duktape 的内建增强(本质是附加 traceback)发生在创建时;可选的错误处理器则允许用户在创建时和抛出前两个时点进一步处理。

错误对象的创建路径

Duktape 中错误对象有三种来源:

1. ECMAScript 代码创建

通常与 throw 语句绑定,例如:

throw new Error('my error');

此时 Error 对象应捕获创建它的源码文件与行号(即 new Error(...) 所在位置)。

2. C 代码通过 Duktape API 创建

duk_error(ctx, DUK_ERR_RANGE_ERROR, "invalid argument: %d", argvalue);

此类场景中抛错点的 __FILE__ / __LINE__ 非常有价值。Duktape API 中创建错误对象的调用被实现为宏,以便便捷地捕获 __FILE__ / __LINE__——这正是生成有用 traceback 的关键。在 duk_error.h 中可以看到,DUK_ERROR / DUK_ERROR_RAW 宏会把 DUK_FILE_MACRO 与 DUK_LINE_MACRO 连同错误码一起打包(错误码占高 8 位、行号占低 24 位)传给 duk_err_handle_error();_RAW 变体则允许调用方显式指定 file/line,便于"受检查调用"使用被检查函数自身的调用点而非宏内部的调用点。

3. Duktape 实现内部抛出

通常使用 DUK_ERROR() 宏,例如:

DUK_ERROR(thr, DUK_ERR_TYPE_ERROR, "invalid argument: %d", argvalue);

此时 __FILE__ / __LINE__ 会进入堆栈回溯,但**不会被归咎(blame)**为错误的 fileName / lineNumber 来源——因为该 file/line 是 Duktape 内部代码,对用户代码不是最有用的信息。DUK_ERROR() 之外还派生了一系列针对特定错误的辅助宏(如 DUK_ERROR_INTERNAL、DUK_ERROR_ALLOC_FAILED、DUK_ERROR_UNSUPPORTED、DUK_ERROR_RANGE_INDEX 等,见 duk_error.h)。

通过 Duktape API 或实现内部抛出的错误永远是 Error 实例,因此必然被增强;且这两类场景中"创建"与"抛出"发生在同一时刻。

用户代码可以分离创建与抛出

ECMAScript 代码没有限制错误创建与抛出必须紧挨着,例如:

var err = new Error('value too large');
if (arg >= 100) {
    throw err;
}

甚至可以不抛出、只读取 traceback:

var err = new Error('currently here');
print('debug: reached this point\n' + err.stack);

这正是"创建时增强优于抛出时增强"的原因:重抛可能导致错误被多次增强(覆盖旧值),而从未抛出的错误依然可以借助回溯信息。

创建时的内建增强与 errCreate 处理

当执行构造函数调用(new Foo())时,Duktape 会检查即将返回给调用方的最终结果。这是对标准构造函数调用处理的改动,统一作用于任意对象创建(因此带来一定开销)。若最终值是一个 Error 实例(内部原型链包含 Error.prototype):

  • 若对象可扩展(extensible),则被 Duktape 内建增强逻辑附加错误信息(如 tracedata);
  • 若设置了 Duktape.errCreate,则再交给用户回调处理;注意对象不必可扩展也会触发用户回调,但它仍必须是 Error 实例。

Duktape 拒绝覆盖同名已有字段:例如若创建出的对象已经有 _Tracedata 字段,增强过程不会覆盖它。(用户错误处理器没有此限制,且可以整体替换错误值。)

虽然某个对象本身不会被构造两次,但在特殊场景下可能出现"创建期间被增强两次"。例如:

function Constructor() {
    return new Error('my error');
}

var e = new Constructor();

这里增强会执行两次:new Error('my error') 执行时增强一次(若存在 errCreate 处理器则被调用一次);new Constructor() 返回时,返回的 Error 值替换了构造函数的默认对象,替换值又被增强一次。为规避该问题,Duktape 的增强代码拒绝添加已存在的字段,从而保证第二步不会覆盖 traceback 数据。用户 errCreate 处理器也必须正确应对同一错误对象被多次调用的情况,最简单的做法是"只处理一次":

Duktape.errCreate = function (e) {
    if ('timestamp' in e) {
        return e;  // only touch once
    }
    e.timestamp = new Date();
    return e;
}

创建时增强的缺点是:错误信息未必能准确反映真正执行 throw 语句的位置——用户代码完全可以在另一个地方、另一个时间创建错误值,甚至把同一个错误值抛出多次。

普通函数调用创建的 Error 也会被增强

Error 构造函数(或其子类构造函数)也可以作为普通函数被调用,标准语义下这与构造函数调用等价。Duktape 同样会增强内建错误构造函数经普通函数调用创建的错误;但用户自定义的 Error 子类不会获得此行为。例如:

MyError = function(msg) { this.message = msg; this.name = 'MyError'; return this; }
MyError.prototype = Error.prototype;

var e1 = new Error('test 1');    // augmented, constructor call
var e2 = Error('test 2');        // augmented, special handling
var e3 = new MyError('test 3');  // augmented, constructor call
var e4 = MyError('test 4');      // not augmented

print(e1.stack);
print(e2.stack);
print(e3.stack);
print(e4.stack);

输出:

Error: test 1
        global test.js:4 preventsyield
Error: test 2
        Error (null) native strict preventsyield
        global test.js:5 preventsyield
MyError: test 3
        global test.js:6 preventsyield
undefined

注意 Error 构造函数以普通函数方式调用时,traceback 因内部细节而与构造函数调用略有不同。该行为的源码依据在 duk_bi_error.c 的 duk_bi_error_constructor_shared():当 DUK_USE_AUGMENT_ERROR_CREATE 启用且非构造函数调用时,会调用 duk_err_augment_error_create(thr, thr, NULL, 0, DUK_AUGMENT_FLAG_NOBLAME_FILELINE) 进行增强,并显式传入 NOBLAME_FILELINE 标志——因为普通函数调用场景中 __FILE__ / __LINE__ 并不理想。让用户错误在非构造函数调用下也获得增强在实现上很困难:既难以判定何时增强合适,又会给每次普通函数调用增加开销。

错误抛出与 errThrow 处理

任意错误值被抛出时,可选的 Duktape.errThrow 处理器都可以处理或替换该值(对所有类型生效)。用户处理器必须注意两点:

  • 只修改相关类型的值,例如仅针对 Error 实例;
  • 正确应对重抛。

例如,以下代码在错误对象首次被抛出时附加时间戳:

Duktape.errThrow = function (e) {
    if (!(e instanceof Error)) {
        return e;  // only touch errors
    }
    if ('timestamp' in e) {
        return e;  // only touch once
    }
    e.timestamp = new Date();
    return e;
}

错误处理器的存储方式与设计取舍

当前方案

创建/抛出错误处理器存放在 Duktape.errCreate 与 Duktape.errThrow 两个属性中。其优势:

  • Duktape 对象在 C 与 ECMAScript 两侧都易于访问,无需额外 API 绑定;
  • 对沙箱化(sandboxing)相对友好:初始化沙箱时可把 Duktape 对象移入 stash(用户代码不可访问),错误处理器即可从 C 侧通过 stash 受控;
  • 处理器的生效范围是共享同一 Duktape 内建对象(即共享同一全局环境)的所有线程,因此自动对恢复(resumed)的协程生效——这通常是一个不错的默认行为。

设计备选方案

原文档还评估了若干替代存放位置:

  • 内部数据结构(如 thr->errcreate / thr->errthrow):沙箱视角更强,但需要自定义绑定来读写处理器,且内存管理必须感知这些字段;
  • 调用线程值栈(caller frame),仅在特定受保护调用期间生效:Lua 采用此模型,Duktape 在 0.9.0 之前也曾如此;缺点是需要为极少使用的错误处理器在受保护调用中额外管理;
  • 全局对象:整体比 Duktape 对象更差,沙箱化更糟且无显著优点;
  • 线程对象:需要额外代码把处理器"继承"给恢复的线程(而这又似乎是合理的默认行为);
  • 全局 stash:对沙箱友好,但默认只能从 C 代码访问,被认为是当前方案的最佳替代之一;
  • 线程 stash:对沙箱友好,但存在处理器"继承"问题。

实现侧,duk_error_augment.c 中 duk__err_augment_user()(src-input/duk_error_augment.c)负责调用用户处理器:它以受保护模式递归调用 duk_pcall_method(),通过 thr->heap->augmenting_error 标志防止递归重入(该标志会跟随协程恢复传递),处理器不可调用或自身抛错时,产生的错误会替换原始错误。

Error 对象的属性

在默认配置(启用 traceback)下,由实现控制创建的 Error 对象具有如下属性:

属性 标准 继承 描述
name 是 是 例如 TypeError(通常为继承属性)
message 是 否 构造时给定的消息(或为空),为自有属性
fileName 否 是 构造该错误的文件(继承访问器)
lineNumber 否 是 构造该错误的行号(继承访问器)
stack 否 是 可打印的堆栈回溯字符串(继承访问器)
_Tracedata 否 否 堆栈回溯数据,内部原始格式(自有、内部属性)

Error.prototype 上的非标准属性:

属性 标准 描述
stack 否 基于 _Tracedata 生成可打印 traceback 的访问器
fileName 否 基于 _Tracedata 取得文件名的访问器
lineNumber 否 基于 _Tracedata 取得行号的访问器

所有访问器都位于原型上,以便在实例没有同名自有属性时兜底:这既最小化了错误实例的属性数量,又允许在需要时提供实例级特定值。这些访问器带 setter——用户代码写入实例特定值时,setter 会捕获写入并创建同名自有属性,从而在后续读取中遮蔽(shadow)原型访问器。

要点补充:

  • stack 属性名源自 V8,行为也与 V8 接近:V8 允许用户写 stack 但不创建同名自有属性,写入的值后续仍可见;
  • fileName / lineNumber 属性名源自 Rhino;
  • Duktape 1.3.0 及更早版本中用户无法直接写 .fileName、.lineNumber 或 .stack(继承的 setter 会捕获并忽略写入),只能借助 Object.defineProperty() 或 duk_def_prop() 创建覆盖属性;从 Duktape 1.4.0 起 setter 被改为透明写入——写入仍被 setter 捕获,但 setter 会自动创建自有属性;
  • _Tracedata 是内部格式,可能随版本(甚至构建)变化,绝不应序列化或在 Duktape heap 生命周期之外使用;
  • 在尺寸优化构建中,traceback 信息可能被省略:此时 fileName 与 lineNumber 是具体的自有属性,.stack 是返回 ToString() 强制转换后的错误字符串的继承属性(如 TypeError: my error message);
  • 尺寸优化构建中,由 Duktape 实现创建的错误没有有用的 message 字段,message 被设置为错误 code 的字符串表示;用户代码抛出的异常则正常携带 message;
  • _Tracedata 含有当前调用栈中的函数引用,这类引用存在潜在沙箱风险,因此 tracedata 被存放在内部属性中。

.fileName 与 .lineNumber 的责任定位(blaming)

问题概览

错误创建/抛出时,并不总是一眼能确定该把哪个 file/line 作为错误来源:.fileName / .lineNumber 应当帮助应用开发者最快定位最可能的出错原因。相关的 file/line 候选有三类:

  1. C 调用点的 __FILE__ / __LINE__:通常指向 Duktape/C 函数内的某一行,但也可能因为 Duktape/C 函数调用了其他文件中的辅助函数而指向辅助函数;C 调用点也可能位于 Duktape 内部(如用户代码调用 duk_require_xxx(),内部经 DUK_ERROR() 宏抛出);此外在调用栈完全为空时仍可抛出错误,此时 C 调用点信息依然可用。
  2. 被编译源码文本的 file/line:只与编译期间抛出的错误相关(典型是 SyntaxError,也可能是其他错误)。
  3. 通向错误的实际调用栈条目(activations):可能是 Duktape/C 或 ECMAScript 函数。ECMAScript 函数默认同时有 .name 与 .fileName,而 Duktape/C 函数没有;函数创建后属性可增可删。

以下 SyntaxError 同时展示了三类 file/line 来源:

duk> try { eval('\n\nfoo='); } catch (e) { print(e.stack); print(e.fileName, e.lineNumber); }
SyntaxError: parse error (line 3)
        input:3                                        <-- file/line of source text (SyntaxError)
        duk_js_compiler.c:3612                         <-- __FILE__ / __LINE__ of DUK_ERROR() call site
        eval  native strict directeval preventsyield   <-- innermost activation, eval() function
        global input:1 preventsyield                   <-- second innermost activation, caller of eval()
input 3   <-- .fileName and .lineNumber blames source text for SyntaxError

从应用视角看,最有用的 file/line 通常是调用栈中最接近的"用户函数"(而非"基础设施函数")。以下几类往往不适宜作为归咎目标:

  • 被视为基础设施的 Duktape/C 或 ECMAScript 函数(如错误检查器、一对一的系统调用包装等);
  • Duktape 内部的 C 调用点(本质上几乎总是基础设施);
  • 缺少 .fileName 属性的 Duktape/C 或 ECMAScript 函数——即使它是用户函数也应忽略,因为得到的 file/line 没有意义。

理想情况下,Duktape 需要能判断某函数是否应被忽略,但这目前尚不可行;下面给出的是现状。

需要说明的是:file/line 信息对良好的错误报告很重要,但所有相关信息始终完整存在于堆栈回溯中;即使归咎的 file/line 不理想,通常也只是恼人而非致命问题。

Duktape 1.3 行为

Duktape 1.3 的归咎规则相对简单:

  • 编译期间抛出的错误总是归咎源码文本的 file/line(典型为 SyntaxError,也可能是如内存不足等内部错误);
  • Duktape 内部(含 duk_require_xxx() 等 API 函数)抛出的错误忽略 C 调用点,采用最内层激活的 file/line 信息——即使最内层激活的函数没有 .fileName 属性、导致错误的 .fileName 变为 undefined;
  • 通过 Duktape API(duk_push_error_object()、duk_error() 等)创建/抛出的错误总是归咎 C 调用点,.fileName / .lineNumber 与 C 调用点的 __FILE__ / __LINE__ 一致;该行为硬编码,用户可在错误对象上自行定义 .fileName / .lineNumber 覆盖。

这些规则存在两个明显短板。

短板一:所有用户抛出的错误都归咎 C 调用点,而这往往不是最佳选择。例如基础设施校验函数:

/* foo/bar/quux.c */

static duk_ret_t my_argument_validator(duk_context *ctx) {
        /* ... */

        /* The duk_error() call site's __FILE__ and __LINE__ will be
         * recorded into _Tracedata and will be provided when reading
         * .fileName and .lineNumber of the error, e.g.:
         *
         *     err.fileName   --> "foo/bar/quux.c"
         *     err.lineNumber --> 1234
         *
         * If this an "infrastructure function", e.g. a validator for
         * an argument value, the file/line blamed is not very useful.
         */

        duk_error(ctx, DUK_ERR_RANGE_ERROR, "argument out of range");

        /* ... */
}

短板二:当 C 调用点不被归咎、且最内层激活没有 .fileName 属性时(Duktape/C 函数的默认情况),错误的 .fileName 会变成 undefined。例如:

((o) Duktape 1.3.0 (v1.3.0)
duk> try { [1,2,3].forEach(123); } catch (e) { err = e; }
= TypeError: type error (rc -105)
duk> err.fileName
= undefined
duk> err.lineNumber
= 0
duk> err.stack
= TypeError: type error (rc -105)
        forEach  native strict preventsyield
        global input:1 preventsyield
duk> Array.prototype.forEach.name
= forEach
duk> Array.prototype.forEach.fileName
= undefined

forEach() 有 .name 却无 .fileName,导致 err.fileName 为 undefined——显然不如把错误归咎于最近的、带文件名的 input 调用点有用。

Duktape 1.4.0 行为

Duktape 1.4.0 在不归咎 C 调用点时改进了归咎行为:不再取最内层激活的 file/line,而是取最靠近的、具有 .fileName 属性的激活。上述 forEach() 示例因此得到改善:

((o) Duktape 1.3.99 (v1.3.0-294-g386260d-dirty)
duk> try { [1,2,3].forEach(123); } catch (e) { err = e; }
= TypeError: function required, found 123 (stack index 0)
duk> err.fileName
= input
duk> err.lineNumber
= 1

若给 forEach() 赋予文件名,它便会成为归咎对象:

((o) Duktape 1.3.99 (v1.3.0-294-g386260d-dirty)
duk> Array.prototype.forEach.fileName = 'dummyFilename.c';
= dummyFilename.c
duk> try { [1,2,3].forEach(123); } catch (e) { err = e; }
= TypeError: function required, found 123 (stack index 0)
duk> err.fileName
= dummyFilename.c
duk> err.lineNumber
= 0

编译期间抛出的错误(典型 SyntaxError)行为不变;显式使用 duk_error() 抛出时归咎 C 调用点的行为也不变——因为这类抛出既可能来自基础设施代码也可能来自应用代码,目前尚没有足够信息选出理想的 file/line。

替换 fileName / lineNumber 访问器

若应用需要对 file/line 归咎做更多控制,可以替换继承的 Error.prototype.fileName 与 Error.prototype.lineNumber 访问器,实现适合自身的逻辑——例如基于文件名白名单/黑名单或文件名模式过滤函数。代价是应用需要自行解码版本相关的 _Tracedata。

未来改进方向

  • 控制 C 调用点的归咎:允许 C 代码按错误指示其 C 调用点是否应参与归咎。Duktape 内部已通过 DUK_ERRCODE_FLAG_NOBLAME_FILELINE(定义于 duk_api_internal.h,值为 1L << 24,与错误码 OR 在一起传递意图)实现该机制,在 duk_api_stack.c 中该标志会被剥离并转换为 DUK_AUGMENT_FLAG_NOBLAME_FILELINE;只需在 API 中暴露即可,但也可有其他 API 设计。
  • 控制编译错误的归咎:目前源码文本的 file/line 总是被归咎;技术上编译错误也可能发生在"基础设施代码"内,不一定总是归咎正确,可在编译 API 调用中加入标志解决。
  • 控制函数的归咎:允许 Duktape/C 与 ECMAScript 函数提供标志,指示其是否应参与 file/line 归咎。1.4.0 中 .fileName 属性在某种程度上承担了此职责(缺失 .fileName 即视为基础设施函数被忽略),但存在"有 .fileName 的基础设施函数"与"无 .fileName 的非基础设施函数"两种情况,显式控制仍有用。该标志可实现为 duk_hobject 标志或(内部/外部)属性。
  • lightfunc 的处理:目前 lightfunc 从不参与 file/line 归咎,是否需要归咎尚无定论。

错误原因链(Cause chains)

当前 Duktape 不支持原因链:ECMAScript 本身没有原因链概念,也不存在公认的非官方标准。自定义原因链很容易支持——允许给错误设置 cause 属性,并让 traceback 格式化器遵循它。原文档给出了几种非侵入式做法:

try {
    f();
} catch (e) {
    var e2 = new Error("something went wrong");  // line N
    e2.cause = e;                                // line N+1
    throw e2;                                    // line N+2
}

这种方式比较笨拙,且错误行信息容易被扭曲;把创建放在同一行可缓解行号问题,但牺牲可读性:

try {
    f();
} catch (e) {
    var e2 = new Error("something went wrong"); e2.cause = e; throw e2;
}

另一个思路是扩展错误构造函数,在构造调用中直接指定 cause(模仿 Java),使用更顺手,但更可能干扰标准语义:

try {
    f();
} catch (e) {
    throw new Error("something went wrong", e);
}

而绝不应采用从 Error.prototype 继承的 setter 方法(如 e2.setCause(e))——这类调用不可移植,在别的 ECMAScript 引擎中会因 setCause 未定义而抛错。由于错误也会从 C 代码(Duktape API)和 Duktape 实现内部创建/抛出,原因链处理必须一并考虑这些来源。另外 cause 属性可以被设为任意值,实现需容忍非 Error 原因(如 e.cause = 1,在 traceback 中合理打印)与原因环(如 e1.cause = e2; e2.cause = e1;,需要检测或做深度限制)。

_Tracedata 格式详解

设计目标

_Tracedata 的价值在于:在错误处理展开调用栈之前,以极快的速度捕获相关调用栈信息。多数情况下 traceback 根本不会被用到,因此它必须紧凑且廉价。为满足这两点,当前格式相当"晦涩":格式随版本变化,不打算被用户代码直接访问。

_Tracedata 是一个扁平数组,依次填充:①可能的编译错误站点,②可能的 C 调用点,③调用栈内容(从栈顶向下,直到栈底或最大 traceback 深度)。该数据只由 Duktape 内部函数处理:Error.prototype.stack 访问器将其转为可打印的 traceback;fileName / lineNumber 访问器基于它做 file/line 归咎。截至 Duktape 1.4 没有公开的解码辅助函数,但用户代码可在 errCreate / errThrow 钩子中用 Duktape.act() 检查当前调用栈。

具体示例

Duktape 1.4.0 中的实际 tracedata 示例(用 Duktape.enc('jx', ...) 转储内部属性):

((o) Duktape 1.3.99 (v1.3.0-294-g72447fe)
duk> try { eval('\n\nfoo='); } catch (e) { err = e; }
= SyntaxError: parse error (line 3)
duk> err.stack
= SyntaxError: parse error (line 3)
        input:3
        duk_js_compiler.c:3655
        eval  native strict directeval preventsyield
        global input:1 preventsyield
duk> Duktape.enc('jx', err[Duktape.dec('hex', 'ff') + 'Tracedata'], null, 4)
= [
    "input",                \  compilation error site
    3,                      /
    "duk_js_compiler.c",    \  C call site
    4294970951,             /
    {_func:true},           \
    107374182400,           |  callstack entries
    {_func:true},           |
    34359738375             /
]

各部分构成

编译错误站点:若错误在编译期间抛出(典型 SyntaxError),首先向 _Tracedata 压入两项——源码文件名(字符串)与出错行号(double)。在 duk_error_augment.c 中,compile_ctx->h_filename 与 compile_ctx->curr_token.start_line 被写入,且 (flags<<32) + (line) 中的 flags 为 0,即默认被归咎。

C 调用点:若调用带有相关 C 调用点,则压入两项——__FILE__ 字符串,以及一个 double,其值为 (flags << 32) + (__LINE__)。唯一的标志位指示该 __FILE__ / __LINE__ 对在用户查询错误相关 fileName / lineNumber 时是否应被"归咎"。源码中对应 DUK_AUGMENT_FLAG_NOBLAME_FILELINE(duk_error.h);DUK_AUGMENT_FLAG_SKIP_ONE(duk_error.h)则用于跳过调用栈顶的激活。实现见 duk_error_augment.c。

调用栈条目:随后每个调用栈元素追加一对数组条目——激活的函数对象(内含函数类型与名称、文件名(或等价的 "global"、"eval")以及可能的 PC-to-line 调试信息),以及一个 double,其值为 (activation_flags << 32) + (activation_pc)。C 函数的程序计数器为 0。激活标志值定义于 duk_hthread.h(源码注释中列出 DUK_ACT_FLAG_STRICT、DUK_ACT_FLAG_TAILCALLED、DUK_ACT_FLAG_CONSTRUCT、DUK_ACT_FLAG_DIRECT_EVAL、DUK_ACT_FLAG_PREVENT_YIELD 等;在 duk_bi_error.c 的 duk__error_getter_helper() 中,这些标志被解码为 strict、tailcall、construct、directeval、preventsyield 等可读后缀,PC 值则借助 duk_hobject_pc2line_query()(DUK_USE_PC2LINE)转为行号)。标志允许在 traceback 中标注尾调用等情况。

补充说明

  • IEEE double 可精确容纳 53 位整数,因此当前表示法有充足空间存放标志,但标志必须位于标志字段低端(第 20 位及以下);
  • 每个激活追加的元素数不必恒定,只要能从数组头开始解码即可(当前不要求随机访问);
  • this 绑定(如有)当前不记录;
  • 激活记录的变量值不记录(调用栈可被检查、寄存器映射可将标识符名映射到寄存器,因此技术上可得,属于未来调试支持的工作方向);
  • _Tracedata 目前是数组,未来可能改成独立的内部类型(本质上是 GC 知道如何访问的 typed buffer),以优化内存与性能。

与源码实现的对应

duk__add_traceback()(src-input/duk_error_augment.c)展示了 _Tracedata 的完整组装过程:先按 DUK_USE_TRACEBACK_DEPTH 与当前调用栈深度取较小值,预分配精确大小的裸数组(duk_push_harray_with_size_outptr 并清除原型),再依次写入编译错误站点、C 调用点(含 NOBLAME 标志编码)与每个激活的(函数对象, pc+flags)对,最后通过 duk_xdef_prop_stridx_short_wec(thr, -2, DUK_STRIDX_INT_TRACEDATA) 定义为内部自有属性。而当 traceback 被禁用(DUK_USE_AUGMENT_ERROR_CREATE 且无 DUK_USE_TRACEBACKS)时,duk__add_fileline()(src-input/duk_error_augment.c)直接以自有属性方式写入 fileName / lineNumber:归咎顺序为编译错误站点 → 未被 NOBLAME 标志排除的 C 调用点 → 最内层带 .fileName 属性的调用栈条目。

应用实战建议

  • 安全敏感环境:启用 DUK_USE_PARANOID_ERRORS(默认关闭),避免错误消息泄露属性键与值;结合把 Duktape 对象移入 stash 的沙箱初始化方式,从 C 侧控制 errCreate / errThrow。
  • 极低内存目标:同时关闭 DUK_USE_VERBOSE_ERRORS 并考虑关闭 traceback,换取 footprint 减小,代价是错误信息退化为错误码字符串。
  • 调试辅助:依赖创建时增强的 stack 属性即可获得内建 traceback;如需自定义归属逻辑,可替换 Error.prototype.fileName / lineNumber 访问器(需自行解码版本相关的 _Tracedata)。
  • 注意内部契约:_Tracedata 是内部格式,禁止序列化或跨 heap 生命周期使用;错误处理器需防御同一错误对象被多次调用(用 'timestamp' in e 之类的幂等判断),并妥善处理重抛与所有值类型。

延伸阅读

登录后查看全文
duktape