深入出不來nodejs源碼-內置模塊引入初探


  重新審視了一下上一篇的內容,配合源碼發現有些地方說的不太對,或者不太嚴謹。

  主要是關於內置模塊引入的問題,當時我是這樣描述的:

需要關注的只要那個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的過程等等具體細節沒有進行說明,但是對於內置模塊的引入總體已經有了一個大概的印象,剩下的可以一步一步慢慢剖析。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM