node模块引入机制

2019-06-22

Node.js模块分类

node.js模块主要分为以下几种:

  • C++核心模块(Built-in)
  • Node内置模块(Native Module)
  • 用户源码模块
  • C++扩展模块

本文试图按照自己的理解将这四个模块的引入机制解释清楚。

加载过程

C++核心模块

C++ 核心模块其实就是在Node.js源码中用纯C++编写并未经JavaScript封装过的模块;也叫 built-in模块,一般我们不直接调用,而是在内置模块中通过process.binding(“module”)的方式被调用。

这里我们以源码下的./src/node_file.cc模块为例解释一下这个过程。

  • 首先我们翻到代码的最后,我们看看最后一行代码:
    1
    NODE_MODULE_CONTEXT_AWARE_BUILTIN(fs, node::InitFs)

这样其实就是注册往链表中注册一个fs的模块,这个fs模块在哪里调用呢,我们代开./lib/fs.js,其中又下面这行代码:

1
const binding = process.binding('fs');

这样很清楚了C++核心模块的使用。

我们接下来看看InitFs干了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
env->SetMethod(target, "access", Access);
env->SetMethod(target, "close", Close);
env->SetMethod(target, "open", Open);
env->SetMethod(target, "read", Read);
env->SetMethod(target, "fdatasync", Fdatasync);
env->SetMethod(target, "fsync", Fsync);
env->SetMethod(target, "rename", Rename);
env->SetMethod(target, "ftruncate", FTruncate);
env->SetMethod(target, "rmdir", RMDir);
env->SetMethod(target, "mkdir", MKDir);
env->SetMethod(target, "readdir", ReadDir);
...

打开Node.js的fs模块的api:http://nodejs.cn/api/fs.html, 会发现大部分接口都能在这个这个C++核心模块找到(在./lib/fs.js中进行了一些增补)。

那么NODE_MODULE_CONTEXT_AWARE_BUILTIN干了些什么呢?在C++中,这是一个宏(什么时候宏自行百度…)。展开后是下面这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extern "C" {                                                        \
static node::node_module _module = \
{ \
NODE_MODULE_VERSION, \
flags, \
NULL, \
__FILE__, \
NULL, \
(node::addon_context_register_func) (node::InitFs), \
NODE_STRINGIFY(fs), \
priv, \
NULL \
}; \
NODE_C_CTOR(_register_ ## fs) { \
node_module_register(&_module); \
} \
}

看到这里,我们关注下node_module_register函数,从字面上看注册一个模块,事实上确实如此;我们大致看看这个函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
} else if (!node_is_initialized) {
// "Linked" modules are included as part of the node project.
// Like builtins they are registered *before* node::Init runs.
mp->nm_flags = NM_F_LINKED;
mp->nm_link = modlist_linked;
modlist_linked = mp;
} else {
modpending = mp;
}
}

该函数表达了如果传入的标识位模块是内置模块(mp->nm_flags & NM_F_BUILTIN),那么将模块注册到我们C++核心模块的链表中;否则是其它模块。

上面是我们C++内置模块的注册过程;我们之前提到C++内置模块是通过process.binding(mod)的方式被调用的,那么binding函数是怎么实现这个过程的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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(module)) {
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(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);
}

其中

1
2
Local<String> module = args[0]->ToString(env->isolate());
node::Utf8Value module_v(env->isolate(), module);

这两行代码可以理解为从参数中获取文件标识符(或者可以理解文件名),转换成字符串后传给module_v,拿到标识字符串之后,通过get_builtin_module函数获取模块内容:

1
node_module* mod = get_builtin_module(*module_v);

get_builtin_module源码:

1
2
3
4
5
6
7
8
9
10
11
 struct node_module* get_builtin_module(const char* name) {
struct node_module* mp;

for (mp = modlist_builtin; mp != nullptr; mp = mp->nm_link) {
if (strcmp(mp->nm_modname, name) == 0)
break;
}

CHECK(mp == nullptr || (mp->nm_flags & NM_F_BUILTIN) != 0);
return (mp);
}

看到modlist_builtin是不是很熟悉,对,就是在node_module_register注册过的链表。

到此C++内置模块的注册和引用已经清晰了。

Node内置模块

Node内置模块使我们编码过程中通过require方式引入的模块,基本上等同于管饭文档上开发出来的那些模块。这些模块大多是在源码的lib目录下以同名的JavaScript代码的实现的;实际上node.js的内置模块是对C++核心模块的封装,具体的功能还是由C++核心模块完成。

我们先回到node.js的运行过程。前面提到我们在输入node app.js的时候,实际上并不是直接运行app.js,而是通过lib/node.js引入app.js间接运行我们的应用,那么node.js具体干了些什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91


(function(process) {
this.global = this;

function startup() {

....


if (process.argv[1] !== '--debug-agent')
startup.processChannel();

startup.processRawDebug();

process.argv[0] = process.execPath;

....

function NativeModule(id) {
this.filename = id + '.js';
this.id = id;
this.exports = {};
this.loaded = false;
}

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

NativeModule.require = function(id) {
if (id == 'native_module') {
return NativeModule;
}

var cached = NativeModule.getCached(id);
if (cached) {
return cached.exports;
}

if (!NativeModule.exists(id)) {
throw new Error('No such native module ' + id);
}

process.moduleLoadList.push('NativeModule ' + id);

var nativeModule = new NativeModule(id);

nativeModule.cache();
nativeModule.compile();

return nativeModule.exports;
};

NativeModule.getCached = function(id) {
return NativeModule._cache[id];
};

NativeModule.exists = function(id) {
return NativeModule._source.hasOwnProperty(id);
};


NativeModule.getSource = function(id) {
return NativeModule._source[id];
};

NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];

NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);

var fn = runInThisContext(source, { filename: this.filename });
fn(this.exports, NativeModule.require, this, this.filename);

this.loaded = true;
};

NativeModule.prototype.cache = function() {
NativeModule._cache[this.id] = this;
};

startup();
});

在这里随便提一下NativeModule.wrap这个方法,它把我们的模块包裹起来起来,我们开发过程用到的exports, require, module, __filename, __dirname这几个变量就有了,现在了解了,其实他妈并不是全局变量,而是编译的时候注入进来的。

其实这个启动文件的代码挺简单的,主要进行一堆初始化;包括我们的本地模块,我们找到本地模块的构造函数NativeModule,很简单,就是filenameidexportsloaded四个属性,从字面意思很好理解,关键的是require这个静态函数,我们忽略缓存和一些必要的判断,到实例的compile,这个函数是重中之重,在compile函数中我们看到NativeModule.getSource(this.id)获取模块的源代码,追本溯源,我们看到关键的还是process.binding('natives')的这一句,这个方法帮我们注入node.js的内置模块(是不是和内置模块引入C++模块方法很相似?)。

我们回到Binding函数的介绍上,最后有这么一句

1
2
3
4
5
6
...
else if (!strcmp(*module_v, "natives")) {
exports = Object::New(env->isolate());
DefineJavaScript(env, exports);
cache->Set(module, exports);
...

关键在DefineJavaScript这行,它把我们所有的内置模块都返回了,那这个函数有什么作用呢?我们找到定义它的文件node_javascript.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 void DefineJavaScript(Environment* env, Local<Object> target) {
HandleScope scope(env->isolate());

for (int i = 0; natives[i].name; i++) {
if (natives[i].source != node_native) {
Local<String> name = String::NewFromUtf8(env->isolate(), natives[i].name);
Local<String> source = String::NewFromUtf8(env->isolate(),
natives[i].source,
String::kNormalString,
natives[i].source_len);
target->Set(name, source);
}
}
}

上述代码主要做的就是遍历natives数组,将数组里面的每一项的文件名、模块源码字符串添加到目标对象,最后返回目标对象。

到这里也有就有疑惑了,那么natives数组从何而来?我们在整个源码找也没有找到它的定义,继续往下看。

我们找到js2c.py文件中终于找到了关于natives的定义

1
2
3
4
5
6
7
static const struct _native natives[] = {}
....
def main():
natives = sys.argv[1]
source_files = sys.argv[2:]
JS2C(source_files, [natives])
...

这是一个Python脚本,帮我们吧lib目录下的js文件转换成./lib/node_natives,所以我们在DefineJavaScript函数中就能读到整个值了。

上面是这个node.js内置模块的实现,那我们来看看我们在require的时候的具体实现;打开源码的module模块,
其中有一个require的方法(这就是我们模块中的’require’方法),这简单,其实这个方法等同于该构造函数的’_load’静态方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Module._load = function(request, parent, isMain) {
if (parent) {
debug('Module._load REQUEST ' + (request) + ' parent: ' + parent.id);
}

// REPL is a special case, because it needs the real require.
...
var filename = Module._resolveFilename(request, parent);

var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}

if (NativeModule.nonInternalExists(filename)) {
debug('load native module ' + request);
return NativeModule.require(filename);
}
var module = new Module(filename, parent);

if (isMain) {
process.mainModule = module;
module.id = '.';
}

Module._cache[filename] = module;

var hadException = true;

try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
...
return module.exports;
};

首先是调用_resolveFilename->NativeModule.nonInternalExists方法获取文件名,然后是通过NativeModule.nonInternalExists检查是否存在该内置模块,如果存在,那么立即返回,没有继续查找是否是用户模块,等等…

到现在,node.js内置模块的这个内幕,有点绕,但是仔细看,却发现也没那么难。

用户源码模块

用户源码模块就是我们自己开发的模块,这个就简单了,主要是通过文件的引入,中间一堆判断,没啥可以说的。

C++扩展模块

C++模块就是我们用C++实现的一些摸,最终编译成以.node问后缀的模块,它是怎么加载的呢,我们会到_load这个静态函数,看下load这个实例方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
Module.prototype.load = function(filename) {
debug('load ' + JSON.stringify(filename) +
' for module ' + JSON.stringify(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;
};

我们看到,它最终执行的Module._extensions[extension]这个静态方法,下面是它的赋值,

1
Module._extensions['.node'] = process.dlopen;

可以看到,执行的其实是dlopen这个方法,我们可以理解成为打开动态链接库(事实上C++模块就是以动态链接库的形式存在的),这个方法在我们之前提到的入口文件./src/node.cc挂载上去的

1
env->SetMethod(process, "dlopen", DLOpen);

DLOpen方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
 void DLOpen(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
uv_lib_t lib;
...

Local<Object> module = args[0]->ToObject(env->isolate()); // Cast
node::Utf8Value filename(env->isolate(), args[1]); // Cast
const bool is_dlopen_error = uv_dlopen(*filename, &lib);//打开动态链接库

// module per object is supported.
node_module* const mp = modpending;
modpending = nullptr;

if (is_dlopen_error) {
Local<String> errmsg = OneByteString(env->isolate(), uv_dlerror(&lib));
uv_dlclose(&lib);
#ifdef _WIN32
// Windows needs to add the filename into the error message
errmsg = String::Concat(errmsg, args[1]->ToString(env->isolate()));
#endif // _WIN32
env->isolate()->ThrowException(Exception::Error(errmsg));
return;
}

if (mp == nullptr) {
uv_dlclose(&lib);
env->ThrowError("Module did not self-register.");
return;
}
if (mp->nm_version != NODE_MODULE_VERSION) {
char errmsg[1024];
snprintf(errmsg,
sizeof(errmsg),
"Module version mismatch. Expected %d, got %d.",
NODE_MODULE_VERSION, mp->nm_version);

// NOTE: `mp` is allocated inside of the shared library's memory, calling
// `uv_dlclose` will deallocate it
uv_dlclose(&lib);
env->ThrowError(errmsg);
return;
}
if (mp->nm_flags & NM_F_BUILTIN) {
uv_dlclose(&lib);
env->ThrowError("Built-in module self-registered.");
return;
}

mp->nm_dso_handle = lib.handle;
mp->nm_link = modlist_addon;
modlist_addon = mp;

Local<String> exports_string = env->exports_string();
Local<Object> exports = module->Get(exports_string)->ToObject(env->isolate());

if (mp->nm_context_register_func != nullptr) {
mp->nm_context_register_func(exports, module, env->context(), mp->nm_priv);
} else if (mp->nm_register_func != nullptr) {
mp->nm_register_func(exports, module, mp->nm_priv);
} else {
uv_dlclose(&lib);
env->ThrowError("Module has no declared entry point.");
return;
}

// Tell coverity that 'handle' should not be freed when we return.
// coverity[leaked_storage]
}

首先通过uv_dlopen打开动态链接库,然后通过nm_dso_handle将句柄转移到node_module结构体的实例上来(前文提过):

1
2
3
4
5
6
7
8
9
10
11
struct node_module {
int nm_version;
unsigned int nm_flags;
void* nm_dso_handle;
const char* nm_filename;
node::addon_register_func nm_register_func;
node::addon_context_register_func nm_context_register_func;
const char* nm_modname;
void* nm_priv;
struct node_module* nm_link;
};

关于C++内置模块的注入过程在后期C++模块的开发过程中我再详细到来。

本文总结了Node.js四种模块的加载过程,希望对于有需要的有朋友有帮助。

Tags: nodejs
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章