作者
Björn Gustavsson <bjorn(at)erlang(dot)org>
狀態
已接受/19.0-we 提案的 -warning 和 -error 指令已在 OTP 版本 19 中實作。
類型
標準追蹤
建立時間
2015-09-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 指令可以重複多次。

運算式 與保護中允許的運算式種類相似,但有一些差異

  • 只允許單一運算式。, 和 ‘;’ 不能使用。請改用 andalsoorelse

  • 除了保護中允許的保護 BIF 之外,-if-elif 的運算式中還允許使用幾個額外的函數。這些函數將在下一節中說明。

  • 呼叫未知函數不會導致編譯錯誤,而是會導致評估失敗,這將導致跳過 -if-elif 後面的行。請參閱「範例」章節中的範例,了解為何這很有用。呼叫不是保護 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"]

最後一個字串不是數字,因此會保留。

版本字串是從應用程式的應用程式檔中擷取。如果在程式碼路徑中找不到該應用程式,或無法讀取應用程式檔,或檔案中沒有 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 是支援的保護 BIF,則 not is_map(a) 的結果將為 true。如果 is_map/1 不是支援的保護 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.

此方法可行,但某些外部工具(例如 autoconf)必須安排在 now/0 已棄用時將 -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)).

原理 #

預處理器的名聲不好,那麼為什麼要擴展預處理器?

快速Google 搜尋「預處理器邪惡」似乎表示預處理器中被認為是邪惡的是巨集展開,而不是條件編譯部分。

話雖如此,條件編譯的主要缺陷在於,如果程式碼在與編譯時不同的環境中執行,可能會產生錯誤行為。目前的預處理器中使用 -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 發行版本中是合法的,但在其他版本中則會導致編譯錯誤。此外,作為副作用,可以使用測試守衛 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

版權 #

本文件已置於公共領域。