扩展从 erlang:get_stacktrace/0
BIF 和 catch
操作符返回的调用堆栈回溯(以下简称堆栈跟踪)中的每个条目,使其包含文件名和行号信息。
目前,从 erlang:get_stacktrace/0
(以及 catch
操作符)返回的堆栈跟踪是一个三元组列表,其中每个元组看起来像
{Module,Function,Arity}
(在某些情况下,第三个元素可能是参数列表,而不是函数参数的数量。)
我们建议将每个元组更改为
{Module,Function,Arity,LocationInfo}
LocationInfo
是一个属性列表(二元组列表),其中包含文件名和行号信息。 如果有可用的行号信息,则列表看起来像
[{file,FilenameString},{line,LineNumber}]
应该使用 proplists:get_value/3
或 lists:keyfind/3
访问该列表,而不是直接匹配,因为未来版本可能会向该列表添加更多项或更改顺序。
文件名通常与模块名相同,并添加扩展名 “.erl”,但如果函数定义已放在头文件中,则文件名将是头文件的名称。 如果 Erlang 源文件是由诸如 yecc 之类的代码生成器生成的,文件名也会有所不同。
行号永远不会为零;而是将 LocationInfo
设置为空列表。
如果没有可用的位置信息,则列表将为空。 以下是一些位置信息可能缺失的原因
该模块已使用不支持生成行号信息的旧 BEAM 编译器编译。
该模块是通过调用 compile:forms/1,2
创建的,其中表单不包含非零行号和/或文件名。
解析转换创建了行号为零的抽象表单。
该模块是使用未提供文件名和/或(非零)行号的替代编译器创建的。
行号信息可能已从 BEAM 文件中剥离。
异常发生在 BIF 中(在运行时系统中用 C 实现)。
此 EEP 没有明确指定如何实现行号信息,但它对实现提出了一些要求
如果未发生异常,则行号信息的存在对程序的执行时间应(几乎)没有影响。 实际上,这意味着不允许实现添加在未发生异常时将执行的额外指令或 BIF 调用。
行号信息不应依赖于 BEAM 文件中存在的调试信息。
行号信息应默认包含在 BEAM 文件中。(可能存在关闭包含行号信息的选项。)
加载行号信息应该是默认设置。可能有一个选项可以关闭加载行号信息以节省内存。
在示例中,我们将使用以下模块
-module(example).
-export([m/1]).
-include("header.hrl").
m(L) ->
{ok,lists:map(fun f/1, L)}. %Line 6
和头文件 header.hrl
f(X) ->
abs(X) + 1. %Line 2
使用 R14B01 调用我们的示例模块,我们得到以下结果
1> example:m([-1,0,1,2]).
{ok,[2,1,2,3]}
2> example:m([-1,0,1,2,not_a_number]).
** exception error: bad argument
in function abs/1
called as abs(not_a_number)
in call from example:f/1
in call from lists:map/2
in call from lists:map/2
in call from example:m/1
3> catch example:m([-1,0,1,2,not_a_number]).
{'EXIT',{badarg,[{erlang,abs,[not_a_number]},
{example,f,1},
{lists,map,2},
{lists,map,2},
{example,m,1},
{erl_eval,do_apply,5},
{erl_eval,expr,5},
{shell,exprs,7}]}}
在启用了行号信息的系统中,我们得到
1> example:m([-1,0,1,2]).
{ok,[2,1,2,3]}
2> example:m([-1,0,1,2,not_a_number]).
** exception error: bad argument
in function abs/1
called as abs(not_a_number)
in call from example:f/1 (header.hrl, line 2)
in call from lists:map/2 (lists.erl, line 948)
in call from lists:map/2 (lists.erl, line 948)
in call from example:m/1 (example.erl, line 6)
3> catch example:m([-1,0,1,2,not_a_number]).
{'EXIT',{badarg,[{erlang,abs,[not_a_number],[]},
{example,f,1,[{file,"header.hrl"},{line,2}]},
{lists,map,2,[{file,"lists.erl"},{line,948}]},
{lists,map,2,[{file,"lists.erl"},{line,948}]},
{example,m,1,[{file,"example.erl"},{line,6}]},
{erl_eval,do_apply,5,[{file,"erl_eval.erl"},{line,482}]},
{erl_eval,expr,5,[{file,"erl_eval.erl"},{line,276}]},
{shell,exprs,7,[{file,"shell.erl"},{line,666}]}]}}
如果我们使用 R14B01 中的 BEAM 编译器编译 example
模块,则该模块将没有任何行号信息
1> example:m([-1,0,1,2,not_a_number]).
** exception error: bad argument
in function abs/1
called as abs(not_a_number)
in call from example:f/1
in call from lists:map/2 (lists.erl, line 948)
in call from lists:map/2 (lists.erl, line 948)
in call from example:m/1
2> catch example:m([-1,0,1,2,not_a_number]).
{'EXIT',{badarg,[{erlang,abs,[not_a_number],[]},
{example,f,1,[]},
{lists,map,2,[{file,"lists.erl"},{line,948}]},
{lists,map,2,[{file,"lists.erl"},{line,948}]},
{example,m,1,[]},
{erl_eval,do_apply,5,[{file,"erl_eval.erl"},{line,482}]},
{erl_eval,expr,5,[{file,"erl_eval.erl"},{line,276}]},
{shell,exprs,7,[{file,"shell.erl"},{line,666}]}]}}
异常中缺少行号信息是许多初学者的主要障碍,并且对经验丰富的 Erlang 程序员来说也是浪费时间。
为了缓解缺少行号信息的问题,经常重复给出的一条建议是编写较小的函数。 在某种程度上,这是一个好主意,但是某些函数最自然地编写为具有许多子句的单个函数。 一个示例是 gen_server
进程的 handle_call/3
回调。 另一个示例是测试套件。 在典型的测试套件中,每一行都会测试一个条件,并且可能失败。 将每一行可能失败的行放在一个单独的函数中是不切实际的。
基于 common_test
的测试套件会自动通过解析转换运行,该转换在发生异常时提供行号信息。 解析转换在每行代码之前插入代码,该代码将当前函数名称和行号保存在进程字典中。 发生异常时,可以检索并显示行号。
此方法的一个问题是,测试套件的运行速度会变慢,如果被测系统中的超时过期,可能会导致测试用例失败。 另一个问题是,默认情况下,解析转换仅在测试模块本身上运行,因此发生在代码其他部分(用于测试的支持库或产品本身)中的异常没有任何行信息。
我们已选择让 erlang:get_stacktrace/0
和 catch
操作符返回包含文件名和行号信息的堆栈跟踪(而不是引入一个名为 erlang:get_full_stacktrace/3
的新函数)。 这意味着简单地传递堆栈跟踪的代码(到 erlang:raise/3
)不需要更新。 例如,以下捕获异常、记录异常并将其传递的代码不需要更新
try
some_call_that_may_fail()
catch
Class:Reason ->
Stk = erlang:get_stacktrace(),
log(Class, Reason, Stk),
erlang:raise(Class, Reason, Stk)
end
另一方面,这意味着假定堆栈跟踪仅包含三元组的代码将不再有效,需要进行更新。
有几个原因要求默认加载行号信息(而不是通过提供选项来排序)。
在实际系统中,代码大小通常不是问题,因为它被用于进程堆、堆外二进制文件和 ETS 表的内存所掩盖。 因此,代码大小增加 10%(在参考实现中测得)对大多数用户而言不是问题,但拥有行号信息的好处可能是巨大的。
Erlang 的新手最需要行号信息,他们应该在不提供任何特殊选项的情况下获取它。 如果需要一个选项,那么关于如何从哪个源代码行找到引发异常的问题将继续浪费时间。
如果必须提供一个选项,即使知道它的开发人员也可能忘记提供该选项,因此最终可能不得不调查没有行号信息的异常。(如果问题不容易重现,则可能会浪费大量时间。)
因此,最好是那些负担不起任何已加载代码大小增加的开发人员必须提供一个选项来关闭加载行号信息。
检查堆栈跟踪并假定其包含三元组的应用程序必须更新。 erlang:raise/3
BIF 仍然接受三元组(它会将这些三元组转换为第四个元素为空列表的四元组); 因此,并非强制性更新对 erlang:raise/3
的调用。
可以从 Github 获取参考实现,如下所示
git fetch git://github.com/bjorng/otp.git bjorn/line-numbers-in-exceptions
这是实现的概述
BEAM 编译器会在可能生成异常的每个构造之前以及在堆栈跟踪中将包含的每个调用之前插入 line
指令。(本地尾递归调用不需要 line
指令,但是外部尾递归调用需要 line
指令,因为它们可能是对 BIF 的调用。)
line
指令具有单个操作数,即行号表中的索引。 行号表存储在 BEAM 文件中的 “Line” 块中。“Line” 块和行指令使 BEAM 文件的文件大小增加了约百分之五。
加载程序将从将要执行的代码中删除 line
指令,但会记住它们的位置并创建一个按地址顺序排序的表,该表将程序计数器映射到行号信息。 当需要构建堆栈跟踪时,运行时系统将对导致异常的指令和每个延续指针的程序计数器进行二进制搜索。
为了使在内存受限的空间中运行的嵌入式系统受益,可以使用 '+L' 选项启动运行时系统,以禁用加载行号信息。 该代码仍将比没有行号信息编译的代码大约大百分之一,因为编译器无法对导致异常的指令(例如 badmatch
指令)进行代码共享优化。
在当前实现中,行号信息使已加载代码的大小增加了大约百分之十。
本文档已置于公共领域。