作者
Björn Gustavsson <bjorn(at)erlang(dot)org>
状态
已接受/19.0-we 提案的 -warning 和 -error 指令已在 OTP 版本 19 中实现
类型
标准跟踪
创建
2015年9月30日
Erlang 版本
OTP-19.0
发布历史
2015年10月16日,2015年10月22日,2015年10月29日

EEP 44: 附加的预处理器指令 #

摘要 #

本 EEP 提议扩展预处理器以允许更强大的条件编译。现有的 -ifdef 指令提供了进行条件编译的最基本功能,但它通常需要来自外部工具(如 autoconf)的帮助。

规范 #

我们将引入一个新的预定义宏和四个新的预处理器指令。

OTP_RELEASE 宏 #

将有一个名为 OTP_RELEASE 的新的预定义宏。它的值将是一个整数,表示运行编译器的运行时系统的发布号。在 OTP 19 中,它的值将是 19

必须在 OTP 18 和 OTP 19 中都工作的代码可以使用以下构造

-ifdef(OTP_RELEASE).
  %% Code that will work in OTP 19 or higher.
-else.
  %% Code that will work in OTP 18.
-endif.

OTP_RELEASE,可以推断出关于运行时系统的最小功能的信息。它对于测试主要新功能(尤其是语言功能)的存在特别有用。

作为一个假设的例子,假设 OTP_RELEASE 在 OTP 17 中可用,如果 ?OTP_RELEASE == 17 的计算结果为 true,我们将知道支持映射。

-if 和 -elif 指令 #

新指令的语法如下

-if(Expression).
   .
   .
   .
-elif(Expression).
   .
   .
   .
-else.
   .
   .
   .
-endif.

-elif 指令可以重复任意次数。

表达式类似于 guard 中允许的表达式类型,但有一些差异

  • 只允许一个表达式。不能使用 ,;。请改用 andalsoorelse

  • 除了 guard 中允许的 guard BIF 之外,-if-elif 的表达式中还允许使用几个额外的函数。这些函数在下一节中描述。

  • 调用未知函数不会导致编译错误,而是会导致评估失败,这将导致跳过 -if-elif 之后的行。请参阅“示例”部分,了解为什么这很有用。调用不是 guard BIF 的 BIF(例如 integer_to_list/1)将导致编译错误。

-if/-elif 中的内置函数 #

以下函数可在 -if-elif 表达式中使用(并且在那里)

  • defined(符号)
  • is_deprecated(模块, 函数, 元数)
  • is_exported(模块, 函数, 元数)
  • is_header(头文件)
  • is_module(模块)
  • version(应用)

每个 if 内置函数的描述如下。

defined/1 #

defined(符号) 测试是否定义了预处理器符号,就像 -ifdef(Symbol) 一样。

is_deprecated/3 #

is_deprecated(模块, 函数, 元数) 测试 函数/元数 是否已弃用。当且仅当编译器会为该函数生成弃用警告时,它才返回 true

为了澄清,函数可以通过两种方式被弃用。

  • 一种是使用 -deprecated() 属性。这是您用来弃用函数的方式,Xref 工具也知道这一点。编译器不知道,is_deprecated/3 也不知道。

  • 另一种方式是在编译器 otp_internal 模块中的弃用函数表中列出该函数。这是 is_deprecated/3 查询的内容。当且仅当 M:F/A 列在该表中时,is_deprecated(M, F, A) 为 true;nowarn_deprecated 选项对此决定没有影响。

is_exported/3 #

is_exported(模块, 函数, 元数) 测试 函数/元数 是否从 模块 导出。

模块 必须已经编译。如果 模块 尚未加载,is_exported/3 将首先调用 code:ensure_loaded/1 来加载 模块。如果 模块 未加载且 code:ensure_loaded/1 无法加载它,则 is_exported/3 将返回 false。当已知 模块 已加载时,is_exported/3 将测试 函数/元数 是否从 模块 导出。

is_header/1 #

is_header(头文件) 测试是否存在头文件 头文件。它以与 -include_lib 相同的方式搜索头文件。

is_module/1 #

is_module(模块) 测试是否存在模块 模块

模块 必须已经编译。如果 模块 尚未加载,is_module/1 将调用 code:ensure_loaded/1 来加载 模块。当且仅当 code:ensure_loaded/1 返回 {module,模块} 时,is_module/1 才会返回 true

version/1 #

version(应用) 返回给定应用 应用 的版本号,作为整数和字符串列表。

首先,版本号字符串将在每个“.”处拆分,以生成字符串列表。然后,将尝试使用 list_to_integer/1 将列表中的每个字符串转换为整数。如果转换失败,则保留字符串。

这是一个示例

"1.10.7"

首先,将拆分字符串

["1","10","7"]

然后,列表中的每个字符串都将转换为整数

[1,10,7]

这是另一个示例

"1.6.0c"

首先,将拆分字符串

["1","6","0c"]

然后,version/1 将尝试将每个字符串转换为整数

[1,6,"0c"]

最后一个字符串不是数字,因此将其保留。

版本字符串是从应用程序的 app 文件中获取的。如果在代码路径中找不到该应用程序,或者无法读取 app 文件,或者文件中没有 vsn 记录,则返回值将为 []

-error 指令 #

-error 指令的语法为

-error(Term).

该指令将导致编译错误。错误消息将如下所示

file.erl:Line: -error(Term).

这是一个示例

-module(example).
-error("This is wrong").
-error(wrong).
-error("Macros will be expanded: " ?MODULE_STRING).

错误消息将是

example.erl:2: -error("This is wrong").
example.erl:3: -error(wrong).
example.erl:4: -error("Macros will be expanded: example").

-warning 指令 #

-warning 指令的语法为

-warning(Term).

该指令将生成警告,但编译将继续。警告消息将如下所示

file.erl:Line: Warning -warning(Term).

这是一个示例

-module(example).
-warning("This module is obsolete").
-warning("Macros will be expanded: " ?MODULE_STRING).

警告消息将是

example.erl:2: Warning: -warning("This module is obsolete").
example.erl:3: Warning: -warning("Macros will be expanded: example").

示例 #

这是一个可以在 OTP 18 到 OTP 20 中运行的代码示例。如果在 OTP 21 或更高版本中尝试编译该代码,则会发生编译错误。

-ifndef(OTP_RELEASE).
  %% Code that will work in OTP 18.
-else.
  %% OTP 19 or higher.
  -if(?OTP_RELEASE =:= 19).
    %% Code that will work in OTP 19.
  -elif(?OTP_RELEASE =:= 20).
    %% Code that will work in OTP 20.
  -else.
    -error("Unsupported OTP release").
  -endif.
-endif.

(请注意,预处理器的当前版本对 -if 有部分支持,它可以跳过 -if-endif 结构。因此,此代码示例将在 OTP 18 中工作。)

这是一个假设的示例,展示了过去如何解决问题(请参阅 预定义的 Erlang 版本宏)。

-if(is_module(ssh_daemon_channel)).
  %% R16B: use new ssh behaviour
  -behavior(ssh_daemon_channel).
-else.
  %% R15: use old ssh behaviour
  -behaviour(ssh_channel).
-endif.

这是一个处理新引入的头文件的示例。

-if(is_header("stdlib/include/assert.hrl")).
  -include_lib("stdlib/include/assert.hrl").
-else.
  %% Define dummy macros just so that our code will compile.
  -define(assert(E),ok).
  -define(assertNot(E),ok).
-endif.

这是一个假设的示例,展示了我们如何测试映射的存在

-if(not is_map(a)).
  %% The guard BIF is_map/1 exists, i.e. maps are supported.
-else.
  %% No support for maps in this release.
-endif.

请注意,如果 is_map/1 是支持的 guard BIF,则 not is_map(a) 的计算结果为 true。如果 is_map/1 不是支持的 guard BIF,则调用 is_map/1 将生成一个异常,这将使表达式失败。

这是一个涉及假设的 foobar 应用程序的示例。由于它不包含在 OTP 中,因此可能尚未编译,并且 is_exported/3 可能因错误的原因返回 false。为了防止这种情况,如果 foobar 模块不存在,我们将中止编译

-if(not is_module(foobar)).
-error("The foobar application has not been compiled").
-endif.

-if(is_exported(foobar, new_feature, 1)).
%% Do something smart with the new feature.
-else.
%% Do as best as we can without the new feature.
-endif.

动机 #

许多开源应用程序(或库)通常至少使用 OTP 的两个主要版本:当前版本和上一个版本。应用程序可能还依赖于其他第三方库,并且可能需要与这些库的不同版本一起使用。

某些应用程序可以通过避免使用两个版本都不可用的功能来支持多个版本。根据应用程序的目的,这可能并非总是可行。例如,如果用于漂亮打印 Erlang 术语的工具不支持其运行版本中的所有数据类型,那么它就不是很有用。

还有另一个问题。现代应用程序应该

  • 在没有任何警告的情况下进行编译。许多开发人员使用 -Werror 将警告转换为编译错误。这意味着必须禁止或消除已弃用函数的警告。例如,now/0 BIF 在 OTP 18 中被标记为已弃用。建议的替代 BIF 在同一版本中引入。

  • 不会在 Dialyzer 中引起任何警告,并且所有导出函数都具有良好的类型规范,以帮助查找错误。类型规范必须在所有支持的版本中编译,并且不得引起警告。

在许多情况下,支持多个 OTP 版本最实用的解决方案是条件编译,也就是说,如果满足某些条件,则会编译源文件的某个部分,否则会编译另一个部分。例如,为了处理 now/0 的弃用

-ifdef(NOW_DEPRECATED).
  %% Use the recommended replacement functions.
  sys_time() ->
    erlang:timestamp().
  uniq_name() ->
    Uniq = erlang:unique_integer([positive]),
    lists:flatten(io_lib:format("name_~w", [Uniq]));
-else.
  %% Use now/0.
  sys_time() ->
    now().
  uniq_name() ->
    {A,B,C} = now(),
    lists:flatten(io_lib:format("name_~w_~w_~w", [A,B,C])).
-endif.

这种方法可行,但是如果 now/0 已被弃用,则某些外部工具(例如 autoconf)必须安排将 -DNOW_DEPRECATED 添加到 erlc 的命令行中。

我们关于扩展预处理器的建议有助于在没有任何外部工具的情况下使用条件编译。假设扩展的预处理器早些时候可用,则可以将先前的示例重写为

-if(is_exported(erlang, timestamp, 0)).
  %% Use the recommended replacement functions.
  sys_time() ->
    erlang:timestamp().
  uniq_name() ->
    Uniq = erlang:unique_integer([positive]),
    lists:flatten(io:lib_format("name_~w", [Uniq])).
-else.
  %% Use now/0.
  sys_time() ->
    now().
  uniq_name() ->
    {A,B,C} = now(),
    lists:flatten(io:lib_format("name_~w_~w_~w", [A,B,C])).
-endif.

或者,可以编写第一个 -if

-if(is_deprecated(erlang, now, 0)).

原理 #

预处理器声誉不佳,那么为什么要扩展预处理器呢?

快速 谷歌搜索“预处理器邪恶” 似乎表明,预处理器中被认为是邪恶的是宏扩展,而不是条件编译部分。

尽管如此,条件编译的主要缺陷在于,如果代码在与编译时不同的环境中运行,可能会出现行为异常。 当前预处理器中的 -ifdef 指令已经存在这种潜在问题。 条件编译的用户有责任确保代码在与编译环境兼容的环境中运行。

预处理器可以做一件事,而且只有预处理器可以做:跳过语法上不正确的代码(例如,使用 map 语法的代码)。 因此,似乎没有办法绕过使用预处理器。 我们可以发明一个新的预处理器,但这并非本 EEP 的目的。

使用特性检测而不是测试版本号怎么样?

我们都赞成这样做。 如果有更好的方法,应尽可能避免针对版本号进行测试。 例如,要测试是否存在新行为 ssh_daemon_channel,请使用 is_module(ssh_daemon_channel)

是否最好使用内置的 supported 函数来测试与语言相关的功能,而不是测试 OTP 版本号?

-if(supported(maps)).
%% Map code.
-endif.

也许吧。 似乎要使这项工作正常进行,必须为每个版本仔细维护和记录 supported 接受的受支持功能名称列表。 然后,用户必须查找要使用的适当功能名称。 此外,如何命名类型规范语法或语言本身的小改动可能并不明显。 生成的代码可能并不比针对版本号的测试更容易理解。

允许表达式中使用未知函数的原理 #

表达式的规则指出以下示例是合法的,因为 foobar/0 是一个未知函数

-if(foobar()).
%% Always skipped.
-endif.

原因是,否则某些表达式在某些版本中是合法的,但在其他版本中则不是。 例如,-if(is_map(a)) 在支持 map 的 OTP 版本中是合法的,但在其他版本中会导致编译错误。 此外,作为副作用,测试 guard BIF 可以用来测试新特性,而不是测试 OTP_RELEASE

向后兼容性 #

定义 OTP_RELEASE 宏的模块将无法编译,并显示类似于此的消息

example.erl:4: redefining predefined macro 'OTP_RELEASE'

同样,尝试使用 -D 从命令行定义 OTP_RELEASE 也会失败。

具有类似 -error(Term)-warning(Term) 属性的模块需要更新,因为 -error(Term) 现在会导致编译错误,而 -warning(Term) 会导致编译警告。

函数 epp:parse_erl_form/1 现在除了之前的返回值外,还可以返回 {warning,Info}。 调用 epp:parse_erl_form/1 的应用程序需要更新以处理新的返回值。 同样,epp:parse_file() 系列函数现在可以在返回的表单列表中包含 {warning,Info} 元组。

实现 #

参考实现可以像这样从 Github 获取

git fetch git://github.com/bjorng/otp.git bjorn/preprocessor-extensions

版权 #

本文档已置于公共领域。