Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Node模块加载原理 #12

Open
Open
Labels
@wython

Description

导读

文章不介绍commonjs细节。会分两部分去介绍node模块加载的原理,第一部分是抽象模型,这部分是希望用最简单的语言去概括node模块运行状态。第二部分是对第一部分的补充,需要结合源码去分析其中的细节。

术语约定

Node编译时:指node安装时候编译的过程
Node启动时:指通过node命令开启一个node进程的过程
Node运行时:指node启动后,node脚本运行的过程

1. 抽象模型

模块边界划分

模块边界划分
Node内部的模块加载过程一般会分为两种:核心模块外部模块
核心模块:指的是系统内部的,由c/c++或者js编写。
外部模块:指的是运行时非node原生定义的模块,一般支持.node, .js , .json三种后缀。其中.node后缀是由自建c++编译得到的模块文件。

c/c++内建核心模块

如前面所说,这部分是由c/c++编写的原生的代码,放源码的src文件夹下。在Node编译时会将内建的c/c++代码提取编译成二进制代码,通过一个数组关联。在启动Node代码时,这部分二进制代码会随着Node启动一起运行。在Node脚本运行时,如果引入相关的模块,会直接提取使用和缓存。
其过程可以如下抽象:
内建模块

js核心模块

这部分是由js编写的原生模块代码,放在源码的lib文件夹下。在Node编译时,这部分代码会被v8提供的js2c.py脚本提取到c++的一个头文件node_natives.h中。提取内容为模块和js源码字符串的对应关系。源码字符串指的是,不像c++,js代码并不会提前解释,所以存储在内存的是js的字符串。启动时,和c++相关部分模块一起关联在核心模块中,平时我们在使用node模块时候,并不会去注意其本身是c++还是js编写。运行时如果使用相关模块,会获取js源码字符串,这个和前面的c++相关模块对比,获取时候会判断是c++内置模块还是js核心模块,如果是c++的直接通过binding函数获取运行,如果是js则由compile方法通过v8解释运行。同时使用过核心模块会添加到NativeModule对象的_cache缓存一个模块id。
其过程可以如下抽象:
js核心模块

外部模块

Node能解析三种后缀的模块 .js、.node、.json。Node在解析外部模块过程中,先要解析模块的路径,尝试获取文件,如果没有或尝试在node_module中获取。

  1. .js类型
    js模块的加载过程,首先通过fs解析路径,获取文件代码,然后包装,最后通过v8进行编译解释运行,对于解释过的外部模块,会添加的Module对象的_cache缓存。

  2. .json类型
    json模块实际上只是加载json数据的过程,解析完文件,将json文件用JSON.parse运行转换成js数据
    格式。

  3. .node类型
    .node类型模块是c/c++代码通过node-gyp程序将代码编译成二进制的node文件,node中通过dlopen方法可以加载.node模块进行运行。在写c/c++模块时候,需要先编译成.node类型才能被node程序识别,node无法直接识别c++模块

  4. 其他
    其他后缀会被node当成js程序执行。

2. 具体过程细节

上面有个大体的流程,node新版和旧版本的代码必然是不一样的。

v6版本node

启动过程

node启动代码在src的node_main.cc的c++文件下。node_main.cc

int main(int argc, char *argv[]) {
 // Disable stdio buffering, it interacts poorly with printf()
 // calls elsewhere in the program (e.g., any logging from V8.)
 setvbuf(stdout, nullptr, _IONBF, 0);
 setvbuf(stderr, nullptr, _IONBF, 0);
 return node::Start(argc, argv);
}

调用了node.cc文件的Start方法, Start方法中调用了StartNodeInstance,然后调用了LoadEnvironment, 在LoadEnvironment中,拿到bootstrap_node.js运行。

Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
 "bootstrap_node.js");

使用require加载

平时使用node,都是像下面这样用require加载模块,可能是http这样的模块,也可能是相对路径的文件模块。

require('http')
require('./utils.js')

require的方法定义在Module.js对象上。module.js文件在lib对于文件夹下。关于require的流程细节就像代码展示的一样:

Module.prototype.require = function(path) {
 assert(path, 'missing path');
 assert(typeof path === 'string', 'path must be a string');
 return Module._load(path, this, /* isMain */ false);
};
...
Module._load = function(request, parent, isMain) {
 if (parent) {
 debug('Module._load REQUEST %s parent: %s', request, parent.id);
 }
 var filename = Module._resolveFilename(request, parent, isMain);
 var cachedModule = Module._cache[filename];
 if (cachedModule) {
 return cachedModule.exports;
 }
 if (NativeModule.nonInternalExists(filename)) {
 debug('load native module %s', request);
 return NativeModule.require(filename);
 }
 var module = new Module(filename, parent);
 if (isMain) {
 process.mainModule = module;
 module.id = '.';
 }
 Module._cache[filename] = module;
 tryModuleLoad(module, filename);
 return module.exports;
};

可以看到,所有核心的模块是用NativeModule.require(filename)获取,外部模块通过tryModuleLoad(module, filename)方法获取。

外部模块的获取细节

在上面的代码中,可以看到,通过tryModuleLoad(module, filename)获取了外部模块,而在tryModuleLoad中,其实还是调用了Module._extensions对象来按文件后缀引入外部模块。平时在我们在写模块文件中,可以拿到module,exports, require等对象,可以通过console.log(require.extensions)打印extensions的结构。下面看看tryModuleLoad的流程代码是怎么走:

function tryModuleLoad(module, filename) {
 var threw = true;
 try {
 module.load(filename);
 threw = false;
 } finally {
 if (threw) {
 delete Module._cache[filename];
 }
 }
}
Module.prototype.load = function(filename) {
 debug('load %j for module %j', filename, this.id);
 assert(!this.loaded);
 this.filename = filename;
 this.paths = Module._nodeModulePaths(path.dirname(filename));
 var extension = path.extname(filename) || '.js';
 if (!Module._extensions[extension]) extension = '.js';
 Module._extensions[extension](this, filename);
 this.loaded = true;
};

_extensions定义了三个方法分别是.js,.json,.node:

Module._extensions['.js'] = function(module, filename) {
 var content = fs.readFileSync(filename, 'utf8');
 module._compile(internalModule.stripBOM(content), filename);
};
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
 var content = fs.readFileSync(filename, 'utf8');
 try {
 module.exports = JSON.parse(internalModule.stripBOM(content));
 } catch (err) {
 err.message = filename + ': ' + err.message;
 throw err;
 }
};
//Native extension for .node
Module._extensions['.node'] = function(module, filename) {
 return process.dlopen(module, path._makeLong(filename));
};

1. js文件

正如前面的抽象模型所表示的,js文件会调用_compile方法编译,_compile会调用vm对象通过v8解释代码,关于vm接口可以在node文档中查阅。

var compiledWrapper = vm.runInThisContext(wrapper, {
 filename: filename,
 lineOffset: 0,
 displayErrors: true
 });

2. json

这个没什么好说的。
module.exports = JSON.parse(internalModule.stripBOM(content));

3. node

也没什么好说的: process.dlopen(module, path._makeLong(filename));

核心模块

核心模块的流程走的是NativeModule的require方法。

js核心模块

前面说到,在编译时候,js核心模块会被js2c.py脚本转换成一个c++头文件。生成位置是在out/release/obj/gen目录下。结构如:

namespace node {
 const char node_native[] = { 47, 47, ..};
 const char dgram_native[] = { 47, 47, ..}; 
 const char console_native[] = { 47, 47, ..}; 
 const char buffer_native[] = { 47, 47, ..};
 const char querystring_native[] = { 47, 47, ..}; 
 const char punycode_native[] = { 47, 42, ..}; 
 ...
 struct _native { 
 const char* name; 
 const char* source; 
 size_t source_len;
 };
 static const struct _native natives[] = {
 { "node", node_native, sizeof(node_native)-1 },
 { "dgram", dgram_native, sizeof(dgram_native)-1 },
 ...
 };
}

其中,source对应的是js源码字符串。编译完成后,这个头文件会被编译进node二进制执行文件中(node命令)。
Node启动时候,所有核心模块(js和c++的)都是通过NativeModule对象维护。文件对象定义在bootstrap_node,顾名思义,这个脚本会在启动时执行。
bootstrap_node源码文件
其中跟着脚本一起执行的有:

 NativeModule._source = process.binding('natives');
 NativeModule._cache = {};
 ...
 startup();

binding方法是定义在c++中的方法。实际上就是将上面c++头文件的关系挂载到_source对象下。

c/c++核心模块

c++的模块在安装node时候以及编译成了可执行代码。所以平时使用如果涉及到c++的模块,通过上面NativeModule调用vm对象运行c++的模块,是不需要定位和编译过程,相对js的代码来说效率会更高。

c++的模块,通过NODE_MODULE宏挂载在Node的命名空间里。

#define NODE_MODULE(modname, regfunc)
 extern "C" {
 NODE_MODULE_EXPORT node::node_module_struct modname ## _module =
 {
 NODE_STANDARD_MODULE_STUFF,
 regfunc,
 NODE_STRINGIFY(modname)
 };
}

c++模块的结构体是:

struct node_module_struct {
 int version;
 void *dso_handle;
 const char *filename;
 void (*register_func) (v8::Handle<v8::Object> target); const char *modname;
};

在node底层,可以通过Binding方法直接获取到c++的核心模块(正常都是通过调用js模块,间接调用),Binding方法定义在node.cc中:

static void Binding(const FunctionCallbackInfo<Value>& args) {
 Environment* env = Environment::GetCurrent(args);
 Local<String> module = args[0]->ToString(env->isolate());
 node::Utf8Value module_v(env->isolate(), module);
 Local<Object> cache = env->binding_cache_object();
 Local<Object> exports;
 if (cache->Has(env->context(), module).FromJust()) {
 exports = cache->Get(module)->ToObject(env->isolate());
 args.GetReturnValue().Set(exports);
 return;
 }
 // Append a string to process.moduleLoadList
 char buf[1024];
 snprintf(buf, sizeof(buf), "Binding %s", *module_v);
 Local<Array> modules = env->module_load_list_array();
 uint32_t l = modules->Length();
 modules->Set(l, OneByteString(env->isolate(), buf));
 node_module* mod = get_builtin_module(*module_v);
 if (mod != nullptr) {
 exports = Object::New(env->isolate());
 // Internal bindings don't have a "module" object, only exports.
 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);
 cache->Set(module, exports);
 } else if (!strcmp(*module_v, "constants")) {
 exports = Object::New(env->isolate());
 DefineConstants(env->isolate(), exports);
 cache->Set(module, exports);
 } else if (!strcmp(*module_v, "natives")) {
 exports = Object::New(env->isolate());
 DefineJavaScript(env, exports);
 cache->Set(module, exports);
 } else {
 char errmsg[1024];
 snprintf(errmsg,
 sizeof(errmsg),
 "No such module: %s",
 *module_v);
 return env->ThrowError(errmsg);
 }
 args.GetReturnValue().Set(exports);
}

如代码所示,c++内建的模块通过node_module* mod = get_builtin_module(*module_v);调用,从node_module_list数组中拿出,然后通过register_func注册给exports对象,而前面的js模块则是由'native'统一挂载在NativeModule._source下。

新版本node

参考资料:
深入浅出nodejs.pdf
node github源码
结合源码分析 Node.js 模块加载与运行原理

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

        AltStyle によって変換されたページ (->オリジナル) /