重新審視了一下上一篇的內容,配合源碼發現有些地方說的不太對,或者不太嚴謹。
主要是關於內置模塊引入的問題,當時我是這樣描述的:
需要關注的只要那個RegisterBuiltinModules方法,從名字也可以看出來,就是加載內置模塊。
然而並不是啊……從名字可以看出來,這只是一個注冊方法。
Register:登記、注冊。
因此,這里並不會真正加載內置模塊,而只是做一個登記,表示有哪些模塊一會要加載,統計一下。
上一節簡單看了下該方法的宏,是一個_register_XX方法的批量調用,而該方法的定義地點還是在一個宏里面,源碼如下所示:
#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags) \ // 模塊結構體 static node::node_module _module = { \ NODE_MODULE_VERSION, /*模塊版本*/ \ flags, /*模塊類型:builtin、internal、linked*/ \ nullptr, /*不懂*/ \ __FILE__, /*不懂*/ \ nullptr, /*注冊方法*/ \ (node::addon_context_register_func) (regfunc), /*注冊方法上下文*/ \ NODE_STRINGIFY(modname), /*模塊名*/ \ priv, /*私有*/ \ nullptr /*指針*/ \ }; \ // _register_函數定義 跳到真正的注冊方法 void _register_ ## modname() { \ node_module_register(&_module); \ } // 這個宏的調用地點在另一個C++文件里 #define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc) \ NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)
看下面那個宏,其中第一個參數就是模塊名,比如fs、os等等。第二個參數是指定模塊的特殊方法,先暫不做深入研究。
宏在調用注冊某一個方法前,會根據模塊定義一個靜態結構體,然后將對應指針傳入真正的注冊方法中去,結構體包含了模塊的具體信息。
注冊的方式非常簡單,源碼如下:
static node_module* modlist_builtin; extern "C" void node_module_register(void* m) { // 定義一個新結構體指針 struct node_module* mp = reinterpret_cast<struct node_module*>(m); // 判斷類型並轉換成鏈表 if (mp->nm_flags & NM_F_BUILTIN) { mp->nm_link = modlist_builtin; modlist_builtin = mp; } // 其余類型模塊的處理 }
在頭部有一個默認的靜態指針,然后每次注冊定義了一個新的模塊指針,用nm_link做鏈接,最后生成一個鏈表,圖示如下:
這樣,通過一個靜態指針,即可訪問到所有注冊的內置模塊。
注冊完后,還是需要加載的,而這個加載地點仍然是上一節提到的一個方法:LoadEnviornment。
這個方法中包裝了一個get_binging_fn方法,也就是上一節提到的C++注入參數的第二個,如下:
// Create binding loaders v8::Local<v8::Function> get_binding_fn = env->NewFunctionTemplate(GetBinding)->GetFunction(env->context()) .ToLocalChecked();
關鍵點就是那個GetBinding方法。這里需要通過JS代碼來輔助講解,首先假設調用了require('fs'),先走JS文件。
從上一節可以得知,由於加載的是內部模塊,會走另一套邏輯,相關代碼如下:
NativeModule.require = function(id) { if (id === loaderId) { return loaderExports; } // 取緩存 const cached = NativeModule.getCached(id); if (cached && (cached.loaded || cached.loading)) { return cached.exports; } // 不合法的模塊名 if (!NativeModule.exists(id)) { // ... } moduleLoadList.push(`NativeModule ${id}`); // 這里進行模塊加載 const nativeModule = new NativeModule(id); // 編譯並緩存 nativeModule.cache(); nativeModule.compile(); return nativeModule.exports; };
代碼非常簡單,可以看到在加載的時候會生成一個新的NativeModule對象,這個對象跟webpack的十分相似:
// Set up NativeModule function NativeModule(id) { this.filename = `${id}.js`; this.id = id; this.exports = {}; this.loaded = false; this.loading = false; }
屬性比較簡單,這里就不做解釋。主要問題放在那個編譯方法上,相關代碼如下:
const ContextifyScript = process.binding('contextify').ContextifyScript; NativeModule._source = getBinding('natives'); NativeModule._cache = {}; NativeModule.wrap = function(script) { return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; }; NativeModule.wrapper = [ '(function (exports, require, module, process) {', '\n});' ]; NativeModule.prototype.compile = function() { // return NativeModule._source[id] let source = NativeModule.getSource(this.id); // 代碼包裝 source = NativeModule.wrap(source); this.loading = true; try { // 執行JS代碼 const script = new ContextifyScript(source, this.filename); // Arguments: timeout, displayErrors, breakOnSigint const fn = script.runInThisContext(-1, true, false); const requireFn = this.id.startsWith('internal/deps/') ? NativeModule.requireForDeps : NativeModule.require; fn(this.exports, requireFn, this, process); this.loaded = true; } finally { this.loading = false; } };
可以看出,比較關鍵的幾步都調用了process.binding或者getBinding方法,這兩個方法正是來源於C++的代碼注入。
同時這里還解釋了為什么代碼中的exports、require、module、process四個變量都是默認可用的,因為代碼會被node自動進行包裝,然后同樣通過C++代碼注入對應的函數參數。
因此,JS層面的代碼都只是普通的方法分發邏輯,真正的調用都來源於底層的C++。
現在回到C++,直接看關鍵方法getBinding,只取關鍵代碼:
static void GetBinding(const FunctionCallbackInfo<Value>& args) { // ... // 從鏈表獲取對應模塊信息 node_module* mod = get_builtin_module(*module_v); // 新建輸出對象 Local<Object> exports; if (mod != nullptr) { // 生成指定模塊 exports = InitModule(env, mod, module); } // ...其他情況 args.GetReturnValue().Set(exports); }
在這里,獲取對應模塊信息就需要用到剛剛生成的注冊信息鏈表,代碼很簡單,如下:
// name即模塊名 node_module* get_builtin_module(const char* name) { return FindModule(modlist_builtin, name, NM_F_BUILTIN); } 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) { // strcmp比較兩個字符串 if (strcmp(mp->nm_modname, name) == 0) break; } // 檢測一下 沒找到mp就是空指針 CHECK(mp == nullptr || (mp->nm_flags & flag) != 0); return mp; }
這樣,就得到了內置模塊的信息,下一步就是模塊加載。
之前在講解模塊結構體時提到過,除了模塊名,還有一個指定模塊的注冊函數被一並添加進去了,這個地方就會用到對應的方法,如下:
static Local<Object> InitModule(Environment* env, node_module* mod, Local<String> module) { // 模塊輸出對象 Local<Object> exports = Object::New(env->isolate()); // 檢測是否有對應的注冊函數 CHECK_EQ(mod->nm_register_func, nullptr); CHECK_NE(mod->nm_context_register_func, nullptr); Local<Value> unused = Undefined(env->isolate()); // 編譯生成對應的內置模塊 mod->nm_context_register_func(exports, unused, env->context(), mod->nm_priv); return exports; }
就這樣,在C++內部成功的加載了內置模塊並返回,最后傳到了JS代碼層。
雖然對於模塊注冊函數來源、模塊生成過程、JS2C的過程、C2JS的過程等等具體細節沒有進行說明,但是對於內置模塊的引入總體已經有了一個大概的印象,剩下的可以一步一步慢慢剖析。