nodejs模組載入分析(1).md

妖怪來了發表於2018-09-12

前言

上篇 nodejs 啟動流程分析中,遺留了幾個問題。這一篇,主要講講模組載入流程。大家都應該熟悉 timer 模組的相關功能。我們就以 timer 為引子,一步步看下去吧。

C++ init 方法開始

下列函式都在 src/node.cc 中:

void Init(std::vector<std::string>* argv,
          std::vector<std::string>* exec_argv) {
  ...
  // 註冊內部模組。
  RegisterBuiltinModules();
  ...
}
複製程式碼

乍一看,這個 RegisterBuiltinModules 方法應該就是關鍵所在了。那看看他是什麼吧。

void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
  NODE_BUILTIN_MODULES(V)
#undef V
}
複製程式碼

原來不是方法,而是一個巨集定義。那我們再看看 NODE_BUILTIN_MODULES 這個是什麼吧。下列檔案位於src/node_internals.h 中:

#define NODE_BUILTIN_MODULES(V)                                               \
  NODE_BUILTIN_STANDARD_MODULES(V)                                            \
  NODE_BUILTIN_OPENSSL_MODULES(V)                                             \
  NODE_BUILTIN_ICU_MODULES(V)
複製程式碼

乖乖,又是一個巨集。簡直就是巨集的地獄。讓我們耐著性子,再往下看看。

#define NODE_BUILTIN_STANDARD_MODULES(V)                                      \
    V(async_wrap)                                                             \
    ...
    V(timer)                                                                 \
複製程式碼

其他兩個巨集類似,就不放上來了。在這個巨集裡面,貌似看到了想要查詢的 timer 。那我們把這個三個巨集合成一下,看看是什麼吧。

_register_timer();
複製程式碼

其他類似,不再贅述。原來這個巨集就是依次呼叫了 _register_xx 方法。插一句,在 C++ 巨集裡面,## 代表字串連線。相當於用 ## 之前的字串 拼接上 ## 之後的字串。現在看來,找到這個 _register_timer() 方法就是關鍵了。全文搜尋一下 _register_timer() 方法,發現無法找到。再嘗試搜尋一下 register 這個關鍵字,可以找到一個巨集如下,在檔案src/node_internals.h中:

#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags)          \
  static node::node_module _module = {                                        \
    NODE_MODULE_VERSION,   /*版本號*/                                          \
    flags,                 /*模組型別,builtin,internal,linked*/                \
    nullptr,               /*nm_dso_handle. 未知*/                             \
    __FILE__,              /*檔名*/                                          \
    nullptr,               /*註冊方法*/                                         \
    (node::addon_context_register_func) (regfunc),    /*註冊上下文*/            \
    NODE_STRINGIFY(modname),          /*模組名*/                               \
    priv,                             /*私有*/                                 \
    nullptr                           /*下一個 node_module 模組節點*/            \
  };                                                                          \
  void _register_ ## modname() {                                              \
    node_module_register(&_module);                                           \
  }
複製程式碼

可以看到,這個巨集定義了一個結構體,node_module 是在 src/node.h裡面定義的。這個巨集把模組名,註冊方法,私有指標,模組型別傳遞進來了。然後還定義了一個方法,就是我們要找的 _register_##modname方法。其中又呼叫了 mode_module_register 方法。此處我們先不著急看實現,而是先去看看是誰呼叫了這個巨集。搜尋下來發現,在檔案src/node_internals.h中有這樣一個巨集:

#define NODE_MODULE_CONTEXT_AWARE_INTERNAL(modname, regfunc)                  \
  NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_INTERNAL)
複製程式碼

NODE_MODULE_CONTEXT_AWARE_INTERNAL 這個巨集呼叫了上面的巨集,並且將模組名,註冊方法傳遞進來,私有變數傳null,模組型別傳內建模組型別。接著再看看誰呼叫了NODE_MODULE_CONTEXT_AWARE_INTERNAL。搜尋可以看到很多處呼叫,我們找到我們關心的 src/timers.cc檔案檢視:

void Initialize(Local<Object> target,
                       Local<Value> unused,
                       Local<Context> context) {
  ...
  env->SetMethod(target, "getLibuvNow", GetLibuvNow);
  ...
  target->Set(env->context(),
              FIXED_ONE_BYTE_STRING(env->isolate(), "immediateInfo"),
              env->immediate_info()->fields().GetJSArray()).FromJust();
}
NODE_MODULE_CONTEXT_AWARE_INTERNAL(timers, node::Initialize)
複製程式碼

可以看到,模組名引數就是 timers, 初始化方法引數傳遞的是 Initialize 方法。到這,算是找到呼叫根源了。那我們接著看看 node_module_register 方法幹了些什麼。此方法在 src/node.cc中:

extern "C" void node_module_register(void* m) {
  struct node_module* mp = reinterpret_cast<struct node_module*>(m);
  ...
  else if (mp->nm_flags & NM_F_INTERNAL) {
    mp->nm_link = modlist_internal;
    modlist_internal = mp;
  }
  ...
}
複製程式碼

你會驚奇的發現,這裡僅僅是把所有的模組依次連結起來,形成一個連結串列。沒有呼叫呀?沒有初始化啊?此處注意一下變數modlist_internal,後面會用到。這裡有一個 GetInternalBinding 。他被繫結到 global 物件上,會被 js 呼叫。此方法是必須結合js原始碼才能明白出處。此方法在src/node.cc中:

static void GetInternalBinding(const FunctionCallbackInfo<Value>& args) {
  ...//獲取引數名。
  Local<String> module = args[0].As<String>();
  ...//獲取內部模組。
  node_module* mod = get_internal_module(*module_v);
  if (mod != nullptr) {
      // 初始化。
    exports = InitModule(env, mod, module);
  }
  ...
}
複製程式碼

讓我們看看 get_builtin_module 方法,此方法在src/node.cc中:

node_module* get_internal_module(const char* name) {
  return FindModule(modlist_internal, name, NM_F_INTERNAL);
}

inline struct node_module* FindModule(struct node_module* list,
                                      const char* name,
                                      int flag) {
  struct node_module* mp;
  for (mp = list; mp != nullptr; mp = mp->nm_link) {
    if (strcmp(mp->nm_modname, name) == 0)
      break;
  }
  ...
  return mp;
}
複製程式碼

方法內部呼叫了 FindModule 方法。第一個引數熟悉嗎?**modlist_internal!**就是通過上面的 RegisterBuiltinModules 方法來生成的模組。具體的查詢過程也比較簡單,就是遍歷這個連結串列,一個個比較模組名,是否相同,然後拿到模組。再繼續看一下 InitModule 方法,同樣位於src/node.cc

static Local<Object> InitModule(Environment* env,
                                 node_module* mod,
                                 Local<String> module) {
  ...//直接呼叫node_module模組的註冊上下文的方法,並傳入對應的一些引數。
  mod->nm_context_register_func(exports,
                                unused,
                                env->context(),
                                mod->nm_priv);
  return exports;
}
複製程式碼

回想下上面的 timer.cc 中的 Initialize 方法,此處就是呼叫的地點了。至此,模組就載入到程式當中了。

尾言

分析過程中,會遇到很多的巨集處理,此時可以將巨集一個個展開,然後寫到紙上進行分析。否則一眼很難看的清全貌。

通過本文,應該對 RegisterModules,和如何初始化模組應該有了一個大致的瞭解。但是你可能還是會有一些迷惑,比如 GetInternalBinding 這個方法誰呼叫的呢,下次來進行分析~

相關文章