作者
Björn Gustavsson <bjorn(at)erlang(dot)org>
状态
最终/21.0 已在 OTP 21 版本中实现
类型
标准跟踪
创建时间
2017-11-23
Erlang 版本
OTP-21.0
发布历史
2017-11-24, 2017-11-30

EEP 47: 在 try/catch 中添加语法以直接检索堆栈跟踪 #

摘要 #

此 EEP 提议扩展 try/catch 语句,允许检索调用堆栈回溯 (堆栈跟踪),而无需调用 erlang:get_stacktrace/0

规范 #

我们将引入新的语法来检索调用堆栈回溯(以下称为堆栈跟踪)。目前,可以随时调用 erlang:get_stacktrace/0 来检索当前进程中发生的最后一个异常的堆栈跟踪。

try/catch 的当前语法是

try
    Exprs
catch
    [Class1:]ExceptionPattern1 [when ExceptionGuardSeq1] ->
        ExceptionBody1;
    [ClassN:]ExceptionPatternN [when ExceptionGuardSeqN] ->
        ExceptionBodyN
end

我们建议以下异常子句头语法的扩展

Class:ExceptionPattern:Stacktrace [when ExceptionGuardSeq] ->

Stacktrace 必须是一个变量名,而不是一个模式。此外,Stacktrace 必须是未绑定的,并且不能在 ExceptionGuardSeq 中被引用。

这是一个例子

try
  Exprs
catch
  something_was_thrown ->
    %% The default class is 'throw'.
    .
    .
    .
  throw:something_else_was_thrown ->
    .
    .
    .
  throw:thrown_with_interesting_stacktrace:Stk ->
    %% The class 'throw' must be explicitly given when
    %% the stacktrace is to be retrieved.
    .
    .
    .
  error:undef ->
    %% Handle an undefined function specially.
    .
    .
    .
  C:E:Stk ->
    %% Log any other exception and rethrow it.
    log_exception(C, E, Stk),
    raise(C, E, Stk)
end.

动机 #

此功能的主要动机是能够弃用(并最终删除)erlang:get_stacktrace/0

erlang:get_stacktrace/0 的问题在于,它强制保留进程中最新异常的堆栈跟踪,直到发生另一个异常或进程终止。堆栈跟踪通常包括最后一个函数调用、BIF 调用或(在 OTP 21 中)失败的操作符的参数。参数的大小可以是任意的。

这是一个例子

1> catch abs(lists:seq(1, 1000)).
{'EXIT',{badarg,
      [{erlang,abs,
                 [[1,2,3,4,5,6,7,8,9,10,11,12,13,
                   14,15,16,17,18,19,20|...]],
                 []},
          {erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,674}]},
          {erl_eval,expr,5,[{file,"erl_eval.erl"},{line,431}]},
          {shell,exprs,7,[{file,"shell.erl"},{line,687}]},
          {shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
          {shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]}}
2>

包含从 1 到 1000 的整数的列表将保留在导致异常的进程中,直到同一进程中发生另一个异常。

在未来的版本中,如果 erlang:get_stacktrace/0 已被更改为始终返回 [] 或已删除,则不再需要将堆栈跟踪无限期地保留在进程中。

另一个动机是,可以避免以下示例中的陷阱

try
  Expr
catch
  C:E ->
    do_something(),
    log_exception(C, E, erlang:get_stacktrace())
end

如果 do_something() 生成并捕获异常,则对 erlang:get_stacktrace/0 的调用将检索错误的堆栈跟踪。

原理 #

语法 #

关于语法,我们考虑过在堆栈跟踪变量之前使用另一个标记而不是冒号。这将继续允许在检索堆栈跟踪时使类 throw 隐式化。也就是说,您可以编写一个异常模式,后跟一些特殊标记,后跟堆栈跟踪变量的名称,并且将隐式理解类 throw

我们拒绝了这一点,原因有两个

  • 我们找不到合适的分割标记。我们最好的建议是 @,但这行不通,因为 @ 在原子中是允许的。像 / 这样的标记可能会混淆至少解析器(也可能是人类读者),因为模式允许包含常量表达式。双冒号 (::) 不会引起任何歧义问题,但每个人都立即将其与类型声明联系起来。

  • 实际上,当捕获类 throw 的异常时,几乎从不对堆栈跟踪感兴趣。

为什么不允许匹配堆栈跟踪? #

Stacktrace 必须是一个变量,而不是一个模式。原因有两个

  • 一般来说,不鼓励对堆栈跟踪进行模式匹配。其目的是让人类检查它,以帮助调试。

  • 允许对堆栈跟踪进行模式匹配会很昂贵。当发生异常时,会保存原始堆栈跟踪。原始堆栈跟踪包含从堆栈收集的有限数量的延续指针(默认情况下为 8)以及可能失败的函数调用或 BIF 调用的参数。将原始堆栈跟踪转换为可以匹配或显示的符号形式非常昂贵;通过仅允许一个变量,只有在匹配了子句并且即将执行其主体时才会进行转换。

限制 erlang:get_stacktrace/0 的作用域? #

在 OTP 20 中,我们在 erlang:get_stacktrace/0 的文档中引入了一个新的警告

只有在(直接或间接)从 try 表达式的作用域内调用 erlang:get_stacktrace/0 时,才能保证它返回堆栈跟踪。

我们的意图是通过限制作用域,可以在退出作用域时清除堆栈跟踪。例如,以下代码将继续工作,但堆栈跟踪将在离开 try/catch 时清除

try Expr
catch
  C:R ->
   {C,R,helper()}
end

helper() ->
  erlang:get_stacktrace().

不幸的是,以下略有不同的示例会迫使我们做出艰难的选择

try Expr
catch
  C:R ->
   helper(C, R)
end

helper(C, R) ->
  {C,R,erlang:get_stacktrace()}.

helper/2 的调用是尾递归的。如果要保持调用尾递归,则无法清除堆栈跟踪。相反,如果要清除堆栈跟踪,则该调用不能再是尾递归的。

另一个问题是,编译器无法警告所有不返回堆栈跟踪的 erlang:get_stacktrace/0 调用实例。它所能做的只是警告明显不起作用的调用,例如以下示例

try Expr
catch
  C:R ->
    .
    .
    .
end,
Stk = erlang:get_stacktrace(),
.
.
.

也就是说,只有在同一个函数中使用了 try/catchcatch,并且紧接着调用了 erlang:get_stacktrace/0,编译器才会发出警告。

我们可以将 erlang:get_stacktrace/0 的有用作用域限制为 try/catch 中子句的语法作用域。例如

try
  Expr
catch
  C:E ->
    Stk = erlang:get_stacktrace(),
    log_exception(C, E, Stk)
end

与引入新语法相比,这种解决方案似乎没有任何优势。开发人员仍然必须更新其程序(从辅助函数中删除对 erlang:get_stacktrace/0 的调用,并将其移动到 try/catch 的语法作用域中)。

向后兼容性 #

由于新语法会在 OTP 20 及之前的版本中导致编译错误,因此不会影响任何现有的源代码。

抽象格式已经包含一个用于异常子句中堆栈跟踪的变量(名称为 _)。这意味着许多操作抽象 Erlang 代码的工具将继续工作而无需任何更改。

可以分几个阶段弃用 erlang:get_stacktrace/0 BIF,以尽量减少更改的影响。

例如

  • 在 OTP 21 中,编译器将发出警告,提示 erlang:get_stacktrace/0 已弃用。

  • 在 OTP 23(或可能是 OTP 22)中,erlang:get_stacktrace/0 将开始返回 []。许多尚未更新的程序将继续工作,只是如果引发异常,将无法使用堆栈跟踪来帮助调试。

  • 在未来的某个版本(OTP 24?),可以删除 erlang:get_stacktrace/0

实现 #

可以在 PR #1634 中找到实现。

版权 #

本文档已置于公共领域。