威尼斯wns.9778官网 > 计算机教程 > 深入出不来nodejs源码-流程总览

原标题:深入出不来nodejs源码-流程总览

浏览次数:113 时间:2019-05-10

Start

  之前在讲解模块结构体时提到过,除了模块名,还有一个指定模块的注册函数被一并添加进去了,这个地方就会用到对应的方法,如下:

3、事件轮询

  然而并不是啊……从名字可以看出来,这只是一个注册方法。

3、注释随处可见

  因此,JS层面的代码都只是普通的方法分发逻辑,真正的调用都来源于底层的C 。

4、代码结构划分非常清晰,部分看不懂(或者不想看)的可以直接跳过,不影响后续内容理解

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;
    }
    // 其余类型模块的处理
}

  这是node中调用require方法的入口JS文件,理论上引入的应该是上面那个函数,不可能得到NativeModule对象的。

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;
};

  这样把最初的函数简单转换一下,大概就是:

  重新审视了一下上一篇的内容,配合源码发现有些地方说的不太对,或者不太严谨。

{
  SealHandleScope seal(isolate);
  // 循环控制参数
  bool more;
  // 标记事件轮询开始
  env.performance_state()->Mark(
      node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_START);
  // 开始event_loop
  do {
      uv_run(env.event_loop(), UV_RUN_DEFAULT);
      v8_platform.DrainVMTasks(isolate);
      more = uv_loop_alive(env.event_loop());
      if (more)
          continue;
      RunBeforeExit(&env);
      // Emit `beforeExit` if the loop became alive either after emitting
      // event, or after running some callbacks.
      more = uv_loop_alive(env.event_loop());
  } while (more == true);
  // 标记事件轮询结束
  env.performance_state()->Mark(
      node::performance::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT);
}

  注册完后,还是需要加载的,而这个加载地点仍然是上一节提到的一个方法:LoadEnviornment。

  这个函数相关源码如下:

  在这里,获取对应模块信息就需要用到刚刚生成的注册信息链表,代码很简单,如下:

  本来打算从简单一点的API来讲,比如说require方法是如何运作的,但是这东西过于简单,百度一搜一大把,已经是被人写烂的内容,所以不打算开这个坑(更重要的是那玩意是JS,如何对得起我学了两周C )。

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);
}
// require => _load
// 判断传入的字符串是否是内部模块名
if (NativeModule.nonInternalExists(filename)) {
    debug('load native module %s', request);
    // 调用另一套逻辑代码
    return NativeModule.require(filename);
}

  就这样,在C 内部成功的加载了内置模块并返回,最后传到了JS代码层。

  这个JS文件内容看起来只是一个函数定义,但是实际上在启动后已经执行完了,四个参数是通过上述C 代码注入的,依次对应上面的4个东西。

  属性比较简单,这里就不做解释。主要问题放在那个编译方法上,相关代码如下:

  花了差不多两周时间过了下primer C 5th,完成了《C 从入门到精通》。(手动滑稽)

  // Create binding loaders
  v8::Local<v8::Function> get_binding_fn =
      env->NewFunctionTemplate(GetBinding)->GetFunction(env->context())
          .ToLocalChecked();

1、内置常量宏都是全大写多单词下划线连接:FILE_TYPE_UNKNOWN代表无法识别的文件类型,对应16进制数字0x0000

// 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;
}

2、函数名基本上能猜到用处:IsWindow7OrGreater函数判断操作系统是不是win7版本以上

  代码非常简单,可以看到在加载的时候会生成一个新的NativeModule对象,这个对象跟webpack的十分相似:

 

 

  有些宏感觉真是一点鸟用都没有,比如说:

  因此,这里并不会真正加载内置模块,而只是做一个登记,表示有哪些模块一会要加载,统计一下。

  可以看到在require的地方做了特殊处理,会直接返回指定的对象,至于这两个对象是什么,后面再慢慢讲吧。

  主要是关于内置模块引入的问题,当时我是这样描述的:

#define NODE_BUILTIN_STANDARD_MODULES(V)                                      
    V(async_wrap)                                                             
    V(buffer)                                                                 
// 还有很多比如fs、url等

  现在回到C ,直接看关键方法getBinding,只取关键代码:

  不过node的require并不像webpack那么简单粗暴,readFile parse直接得到文件输出对象,而是区分了内部模块与外部模块。对于内部模块,有一套新的逻辑,相关代码如下:

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;
    }
};

  值得注意的是那个注释,这个字符串十分特殊,node并不希望用户获取该模块,因为得到的对象拥有直接调用底层C 代码的能力,十分危险。

// Set up NativeModule
function NativeModule(id) {
    this.filename = `${id}.js`;
    this.id = id;
    this.exports = {};
    this.loaded = false;
    this.loading = false;
}
void Init(int* argc,
    const char** argv,
    int* exec_argc,
    const char*** exec_argv) {
    // Initialize prog_start_time to get relative uptime.
    prog_start_time = static_cast<double>(uv_now(uv_default_loop()));
    // 加载内置模块
    RegisterBuiltinModules();
    // Make inherited handles noninheritable.
    uv_disable_stdio_inheritance();

    // 后面还有很多代码 但是我看不懂 流下了没技术的眼泪……
}

 

const loaderExports = { internalBinding, NativeModule };
const loaderId = 'internal/bootstrap/loaders';
NativeModule.require = function(id) {
    // Do not expose this to user land even with --expose-internals
    // 对此require进行特殊处理
    if (id === loaderId) {
        return loaderExports;
    }
    // ...
}
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;
}

  其中1、2两个都是在同一个函数中执行的,即LoadEnviroment,上一些关键的源码证明下:

  宏在调用注册某一个方法前,会根据模块定义一个静态结构体,然后将对应指针传入真正的注册方法中去,结构体包含了模块的具体信息。

void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
  NODE_BUILTIN_MODULES(V)
#undef V
}

  注册的方式非常简单,源码如下:

  这玩意翻译成JS大概就是:

需要关注的只要那个RegisterBuiltinModules方法,从名字也可以看出来,就是加载内置模块。

  这里有一个小地方稍微讲讲吧,如果在启动时已经执行完了,那么看一下下面的代码:

威尼斯wns.9778官网 1

  而在node中,wmain是windows系统的主函数,main是LINUX系统的主函数。区分系统的关键在于一个特定的宏,windows是_WIN32,这个宏会被自动定义,相关源码如下:

  虽然对于模块注册函数来源、模块生成过程、JS2C的过程、C2JS的过程等等具体细节没有进行说明,但是对于内置模块的引入总体已经有了一个大概的印象,剩下的可以一步一步慢慢剖析。

  这两天看了下node源码的一些入口方法,其实还是比较懵逼的,语法倒不是难点,主要是大量的宏造成直接阅读上的不方便。

#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)

  首先每个C 项目都有一个启动项目,每个项目里有一个主函数,当初我以为只能叫main,后来发现wmain也是OK的。

  关键点就是那个GetBinding方法。这里需要通过JS代码来辅助讲解,首先假设调用了require('fs'),先走JS文件。

  这个函数的定义也很奇妙,简直就是宏函数,如下:

  在头部有一个默认的静态指针,然后每次注册定义了一个新的模块指针,用nm_link做链接,最后生成一个链表,图示如下:

#define NODE_BUILTIN_MODULES(V)                                               
  NODE_BUILTIN_STANDARD_MODULES(V)                                            
  NODE_BUILTIN_OPENSSL_MODULES(V)                                             
  NODE_BUILTIN_ICU_MODULES(V)

威尼斯wns.9778官网,  同时这里还解释了为什么代码中的exports、require、module、process四个变量都是默认可用的,因为代码会被node自动进行包装,然后同样通过C 代码注入对应的函数参数。

// C  :#define V(modname) _register_##modname();
const V = (modname) => `_register_${modname}`();

  看下面那个宏,其中第一个参数就是模块名,比如fs、os等等。第二个参数是指定模块的特殊方法,先暂不做深入研究。

 

  这样,就得到了内置模块的信息,下一步就是模块加载。

  函数的声明相信学JS的也能看懂是什么,主要是函数内容很奇怪,竟然是一个宏定义。

  从上一节可以得知,由于加载的是内部模块,会走另一套逻辑,相关代码如下:

  看到这闪亮亮的格式,应该可以猜到,这三个东西还是宏!!!

  这个方法中包装了一个get_binging_fn方法,也就是上一节提到的C 注入参数的第二个,如下:

  完结,下一节写啥还得再想想。

  上一节简单看了下该方法的宏,是一个_register_XX方法的批量调用,而该方法的定义地点还是在一个宏里面,源码如下所示:

威尼斯wns.9778官网 2

  可以看出,比较关键的几步都调用了process.binding或者getBinding方法,这两个方法正是来源于C 的代码注入。

本文由威尼斯wns.9778官网发布于计算机教程,转载请注明出处:深入出不来nodejs源码-流程总览

关键词:

上一篇:win10系统的“USB选择性暂停设置”怎么打开

下一篇:没有了