前言
上篇 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 這個方法誰呼叫的呢,下次來進行分析~