• 深入出不来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) {',
        '
    });'
    ];
    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的过程等等具体细节没有进行说明,但是对于内置模块的引入总体已经有了一个大概的印象,剩下的可以一步一步慢慢剖析。

  • 相关阅读:
    codechef May Challenge 2016 CHSC: Che and ig Soccer dfs处理
    codechef May Challenge 2016 FORESTGA: Forest Gathering 二分
    codechef May Challenge 2016 LADDU: Ladd 模拟
    tp5 whereOr
    Null
    验证消息是否来自微信
    layer使用注意事项
    laravel 查询
    laravel form表单提交
    ajax上传文件
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/9182558.html
Copyright © 2020-2023  润新知