赵走x博客
网站访问量:151506
首页
书籍
软件
工具
古诗词
搜索
登录
6、C/C++扩展模块
5、核心模块
4、Node的模块实现
3、CommonJS规范
2、模块机制
1、Node简介
5、核心模块
资源编号:76279
书籍
深入浅出NodeJS
热度:105
前面提到,Node的核心模块在编译成可执行文件的过程中被编译进了二进制文件。核心模块其实分为C/C++编写的和JavaScript编写的两部分,其中C/C++文件存放在Node项目的src目录下,JavaScript文件存放在lib目录下。
前面提到,Node的核心模块在编译成可执行文件的过程中被编译进了二进制文件。核心模块其实分为C/C++编写的和JavaScript编写的两部分,其中C/C++文件存放在Node项目的src目录下,JavaScript文件存放在lib目录下。 # 1 JavaScript核心模块的编译过程 在编译所有C/C++文件之前,编译程序需要将所有的JavaScript模块文件编译为C/C++代码,此时是否直接将其编译为可执行代码了呢?其实不是。 ### 转存为C/C++代码 Node采用了V8附带的js2c.py工具,将所有内置的JavaScript代码(src/node.js和`lib/*.js`)转换成C++里的数组,生成node_natives.h头文件,相关代码如下: ``` 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 }, ... }; } ``` 在这个过程中,JavaScript代码以字符串的形式存储在node命名空间中,是不可直接执行的。在启动Node进程时,JavaScript代码直接加载进内存中。在加载的过程中,JavaScript核心模块经历标识符分析后直接定位到内存中,比普通的文件模块从磁盘中一处一处查找要快很多。 ### 编译JavaScript核心模块 lib目录下的所有模块文件也没有定义require、module、exports这些变量。在引入JavaScript核心模块的过程中,也经历了头尾包装的过程,然后才执行和导出了exports对象。与文件模块有区别的地方在于:获取源代码的方式(核心模块是从内存中加载的)以及缓存执行结果的位置。 JavaScript核心模块的定义如下面的代码所示,源文件通过process.binding('natives')取出,编译成功的模块缓存到NativeModule._cache对象上,文件模块则缓存到Module._cache对象上: ``` function NativeModule(id) { this.filename = id + '.js'; this.id = id; this.exports = {}; this.loaded = false; } NativeModule._source = process.binding('natives'); NativeModule._cache = {}; ``` # 2 C/C++核心模块的编译过程 在核心模块中,有些模块全部由C/C++编写,有些模块则由C/C++完成核心部分,其他部分则由JavaScript实现包装或向外导出,以满足性能需求。后面这种C++模块主内完成核心,JavaScript主外实现封装的模式是Node能够提高性能的常见方式。通常,脚本语言的开发速度优于静态语言,但是其性能则弱于静态语言。而Node的这种复合模式可以在开发速度和性能之间找到平衡点。 这里我们将那些由纯C/C++编写的部分统一称为内建模块,因为它们通常不被用户直接调用。Node的buffer、crypto、evals、fs、os等模块都是部分通过C/C++编写的。 ### 内建模块的组织形式 在Node中,内建模块的内部结构定义如下: ``` struct node_module_struct { int version; void *dso_handle; const char *filename; void (*register_func) (v8::Handle
target); const char *modname; }; ``` 每一个内建模块在定义之后,都通过NODE_MODULE宏将模块定义到node命名空间中,模块的具体初始化方法挂载为结构的register_func成员: ``` #define NODE_MODULE(modname, regfunc) \ extern "C" { \ NODE_MODULE_EXPORT node::node_module_struct modname ## _module = \ { \ NODE_STANDARD_MODULE_STUFF, \ regfunc, \ NODE_STRINGIFY(modname) \ }; \ } ``` node_extensions.h文件将这些散列的内建模块统一放进了一个叫node_module_list的数组中,这些模块有: ``` node_buffer node_crypto node_evals node_fs node_http_parser node_os node_zlib node_timer_wrap node_tcp_wrap node_udp_wrap ` node_pipe_wrap node_cares_wrap node_tty_wrap node_process_wrap node_fs_event_wrap node_signal_watcher ``` 这些内建模块的取出也十分简单。Node提供了get_builtin_module()方法从node_module_list数组中取出这些模块。 内建模块的优势在于:首先,它们本身由C/C++编写,性能上优于脚本语言;其次,在进行文件编译时,它们被编译进二进制文件。一旦Node开始执行,它们被直接加载进内存中,无须再次做标识符定位、文件定位、编译等过程,直接就可执行。 ### 内建模块的导出 在Node的所有模块类型中,存在着如图2-4所示的一种依赖层级关系,即文件模块可能会依赖核心模块,核心模块可能会依赖内建模块。  图2-4 依赖层级关系 通常,不推荐文件模块直接调用内建模块。如需调用,直接调用核心模块即可,因为核心模块中基本都封装了内建模块。那么内建模块是如何将内部变量或方法导出,以供外部JavaScript核心模块调用的呢? Node在启动时,会生成一个全局变量process,并提供Binding()方法来协助加载内建模块。Binding()的实现代码在src/node.cc中,具体如下所示: ``` static Handle
Binding(const Arguments& args) { HandleScope scope; Local
module = args[0]->ToString(); String::Utf8Value module_v(module); node_module_struct* modp; if (binding_cache.IsEmpty()) { binding_cache = Persistent
::New(Object::New()); } Local
exports; if (binding_cache->Has(module)) { exports = binding_cache->Get(module)->ToObject(); return scope.Close(exports); } // Append a string to process.moduleLoadList char buf[1024]; snprintf(buf, 1024, "Binding %s", *module_v); uint32_t l = module_load_list->Length(); module_load_list->Set(l, String::New(buf)); if ((modp = get_builtin_module(*module_v)) != NULL) { exports = Object::New(); modp->register_func(exports); binding_cache->Set(module, exports); } else if (!strcmp(*module_v, "constants")) { exports = Object::New(); DefineConstants(exports); binding_cache->Set(module, exports); #ifdef __POSIX__ } else if (!strcmp(*module_v, "io_watcher")) { exports = Object::New(); IOWatcher::Initialize(exports); binding_cache->Set(module, exports); #endif } else if (!strcmp(*module_v, "natives")) { exports = Object::New(); DefineJavaScript(exports); binding_cache->Set(module, exports); } else { return ThrowException(Exception::Error(String::New("No such module"))); } return scope.Close(exports); } ``` 在加载内建模块时,我们先创建一个exports空对象,然后调用get_builtin_module()方法取出内建模块对象,通过执行register_func()填充exports对象,最后将exports对象按模块名缓存,并返回给调用方完成导出。 这个方法不仅可以导出内建方法,还能导出一些别的内容。前面提到的JavaScript核心文件被转换为C/C++数组存储后,便是通过process.binding('natives')取出放置在NativeModule._source中的: ``` NativeModule._source = process.binding('natives'); ``` 该方法将通过js2c.py工具转换出的字符串数组取出,然后重新转换为普通字符串,以对JavaScript核心模块进行编译和执行。 # 3 核心模块的引入流程 前面讲述了核心模块的原理,也解释了核心模块的引入速度为何是最快的。 从图2-5所示的os原生模块的引入流程可以看到,为了符合CommonJS模块规范,从JavaScript到C/C++的过程是相当复杂的,它要经历C/C++层面的内建模块定义、(JavaScript)核心模块的定义和引入以及(JavaScript)文件模块层面的引入。但是对于用户而言,require()十分简洁、友好。  图2-5 os原生模块的引入流程 # 4 编写核心模块 核心模块被编译进二进制文件需要遵循一定规则。作为Node的使用者,尽管几乎没有机会参与核心模块的开发,但是了解如何开发核心模块有助于我们更加深入地了解Node。 核心模块中的JavaScript部分几乎与文件模块的开发相同,遵循CommonJS模块规范,上下文中除了拥有require、module、exports外,还可以调用Node中的一些全局变量,这里不做描述。 下面我们以C/C++模块为例演示如何编写内建模块。为了便于理解,我们先编写一个极其简单的JavaScript版本的原型,这个方法返回一个Hello world!字符串: ``` exports.sayHello = function () { return 'Hello world!'; }; ``` 编写内建模块通常分两步完成:编写头文件和编写C/C++文件。 将以下代码保存为node_hello.h,存放到Node的src目录下: ``` #ifndef NODE_HELLO_H_ #define NODE_HELLO_H_ #include
namespace node { // 预定义方法 v8::Handle
SayHello(const v8::Arguments& args); } #endif ``` 编写node_hello.cc,并存储到src目录下: ``` #include
#include
#include
namespace node { using namespace v8; // 实现预定义的方法 Handle
SayHello(const Arguments& args) { HandleScope scope; return scope.Close(String::New("Hello world!")); } // 给传入的目标对象添加sayHello方法 void Init_Hello(Handle
target) { target->Set(String::NewSymbol("sayHello"), FunctionTemplate::New(SayHello)->GetFunction()); } } // 调用NODE_MODULE()将注册方法定义到内存中 NODE_MODULE(node_hello, node::Init_Hello) ``` 以上两步完成了内建模块的编写,但是真正要让Node认为它是内建模块,还需要更改src/node_extensions.h,在NODE_EXT_LIST_END前添加NODE_EXT_LIST_ITEM(node_hello),以将node_hello模块添加进node_module_list数组中。 其次,还需要让编写的两份代码编译进执行文件,同时需要更改Node的项目生成文件node.gyp,并在'target_name': 'node'节点的sources中添加上新编写的两个文件。然后编译整个Node项目,具体的编译步骤请参见附录A。 编译和安装后,直接在命令行中运行以下代码,将会得到期望的效果: ``` $ node > var hello = process.binding('hello'); undefined > hello.sayHello(); 'Hello world!' > ``` 至此,原生编写过程中需要注意的细节都已表述过了。可以看出,简单的模块通过JavaScript来编写可以大大提高生产效率。这里我们写作本节的目的是希望有能力的读者可以深入Node的核心模块,去学习它或者改进它。