這裡的犀牛長的不太一樣!#ifdef in JavaScript?

這裡的犀牛長的不太一樣!#ifdef in JavaScript?

Firefox OS 裡的 Gecko 層,主要是由 C++ 和 JavaScript 兩種程式語言編寫而成。不過 Gecko 裡的 JS 程式碼長相和我們常見的 client-side JS 非常不一樣。剛開始閱讀程式碼時,常常會看到一些奇妙的玩意。
比方說,JS 裡頭竟然會有 #ifdef !?

原來這是 Mozilla 程式師在內部施展的一點小魔法,讓工程師在 Gecko 層撰寫 JS 程式碼時,能如同寫 C/C++ 一樣,直覺地使用 #ifdef/#endif 來決定編譯的區塊。這麼一來,不僅寫法簡單,而且讓 C++ 和 JS 原始碼能共用相同的識別符號 (identifier),實在是非常方便。

以下就一個簡單的例子,分享在 Gecko JS 裡面使用 #ifdef 的小撇步。

在 JavaScript 裡加入 #ifdef/#endif

當時我的一個任務是修改 DOM API 的 TCPSocket,每當有 TCP 流量傳送時,必須呼叫 networkStatsServiceProxy 提供的 API 把流量統計給記錄下來。問題是,networkStatsServiceProxy 是 B2G 獨有的服務元件,在桌面版並沒有這個元件。此時,我們的作法就是在 JavaScript 裡面用 #ifdef/#endif 把這段程式碼給包起來,以確保 Mozilla 程式庫在其他平台上 build 不會失敗。

以下是在 dom/network/src/TCPSocket.js (簡化過的)程式碼片段,重點在於這段程式是在 JS 裡面,用 #ifdef/#endif 括起來的:

#ifdef MOZ_WIDGET_GONK
  _saveNetworkStats: function ts_saveNetworkStats() {
    let nssProxy = Cc["@mozilla.org/networkstatsServiceProxy;1"]
                     .getService(Ci.nsINetworkStatsServiceProxy);
    nssProxy.saveAppStats(this._appId, this._activeNetwork, Date.now(),
                          this._rxBytes, this._txBytes);
    // Reset the counters once the statistics is saved.
    this._txBytes = this._rxBytes = 0;
  }, 
#endif

在 moz.build 裡面加入 EXTRA_PP_COMPONENTS

嗯,很好,#ifdef/#endif 的寫法看起來和 C/C++ 完全沒兩樣。然後我們要如何讓這個前置處理指令發揮效用呢?非常簡單,只要修改相同目錄下的 moz.build,加入一個特殊的 EXTRA_PP_COMPONENTS 區塊即可。
以 TCPSocket.js 的例子而言,此時我們只要修改 dom/network/src/moz.build,加入以下幾行就可以囉!

EXTRA_PP_COMPONENTS += [
    'TCPSocket.js',
]

That’s it!此時我們的 TCPSocket.js 就具有能夠使用前置處理指令 #ifdef/#endif 的能力了(當然也可以用 #ifndef, #elif)。可是,你是否仍然疑惑,JavaScript 沒有編譯階段更沒有前置處理階段呀,這一切是怎麼辦到的?
其實 JavaScript 程式碼仍舊是在執行期間時,直譯器 (interpreter) 才會去解析的。這個小魔法是在 build 階段,當 builder 發現 JavaScript 原始檔被宣告在 EXTRA_PP_COMPONENTS 裡面時,就會自動去修改原本的 JavaScript 原始碼的內容。

檢查 JavaScript 原始碼是否有被修改過

在 build.sh [gecko] 指令執行完之後,我們可以到 [obj_dir]/dist/bin/components 目錄下查看 JS 檔案。一般來說,此目錄下的 JS 檔案只是 soft link,指到原本程式庫裡真正的原始碼檔案。但是經過 builder “前置處理" 過的 JavaScript 檔案,就變成此目錄下的實體檔案而非連結檔了。以上述的例子而言,只有 TCPSocket.js 不是連結檔。

$ cd /dist/bin/components
$ ls -l *TCP*
   75 10:37 TCPServerSocket.js -> /dom/network/src/TCPServerSocket.js
29082 10:37 TCPSocket.js
    75 10:37 TCPSocket.manifest -> /dom/network/src/TCPSocket.manifest
    87 10:37 TCPSocketParentIntermediary.js -> /dom/network/src/TCPSocketParentIntermediary.js

此時打開 TCPSocket.js,會發現原本宣告為 #ifdef/#endif 的兩行程式碼,已被自動置換成註解囉。

//@line 328 "/home/ethan/workspace/hg/mozilla-central/dom/network/src/TCPSocket.js"
  _saveNetworkStats: function ts_saveNetworkStats(enforce) {
    let nssProxy = Cc["@mozilla.org/networkstatsServiceProxy;1"]
                     .getService(Ci.nsINetworkStatsServiceProxy);
    nssProxy.saveAppStats(this._appId, this._activeNetwork, Date.now(),
                          this._rxBytes, this._txBytes);
    // Reset the counters once the statistics is saved to NetworkStatsServiceProxy.
    this._txBytes = this._rxBytes = 0;
  },
//@line 358 "/home/ethan/workspace/hg/mozilla-central/dom/network/src/TCPSocket.js"