You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// Loads a module at the given file path. Returns that module's// `exports` property.Module.prototype.require=function(id){if(typeofid!=='string'){thrownewERR_INVALID_ARG_TYPE('id','string',id);}if(id===''){thrownewERR_INVALID_ARG_VALUE('id',id,'must be a non-empty string');}returnModule._load(id,this,/* isMain */false);};
代码前面时一些path校验,那么Module._load做了什么呢?
// Check the cache for the requested file.// 1. If a module already exists in the cache: return its exports object.// 2. If the module is native: call `NativeModule.require()` with the// filename and return the result.// 3. Otherwise, create a new module for the file and save it to the cache.// Then have it load the file contents before returning its exports// object.Module._load=function(request,parent,isMain){if(parent){debug('Module._load REQUEST %s parent: %s',request,parent.id);}if(experimentalModules&&isMain){asyncESM.loaderPromise.then((loader)=>{returnloader.import(getURLFromFilePath(request).pathname);}).catch((e)=>{decorateErrorStack(e);console.error(e);process.exit(1);});return;}varfilename=Module._resolveFilename(request,parent,isMain);varcachedModule=Module._cache[filename];// 如果在缓存if(cachedModule){updateChildren(parent,cachedModule,true);returncachedModule.exports;}// 原生模块if(NativeModule.nonInternalExists(filename)){debug('load native module %s',request);returnNativeModule.require(filename);}// 创建新module// Don't call updateChildren(), Module constructor already does.varmodule=newModule(filename,parent);if(isMain){process.mainModule=module;module.id='.';}Module._cache[filename]=module;tryModuleLoad(module,filename);returnmodule.exports;};
NativeModule.require=function(id){if(id===loaderId){returnloaderExports;}constcached=NativeModule.getCached(id);// 判断是否缓存if(cached&&(cached.loaded||cached.loading)){returncached.exports;}if(!NativeModule.exists(id)){// Model the error off the internal/errors.js model, but// do not use that module given that it could actually be// the one causing the error if there's a bug in Node.js// eslint-disable-next-line no-restricted-syntaxconsterr=newError(`No such built-in module: ${id}`);err.code='ERR_UNKNOWN_BUILTIN_MODULE';err.name='Error [ERR_UNKNOWN_BUILTIN_MODULE]';throwerr;}moduleLoadList.push(`NativeModule ${id}`);constnativeModule=newNativeModule(id);nativeModule.cache();nativeModule.compile();returnnativeModule.exports;};
最初的想法是去写一个node C++ addon,但在了解如何去写node addon的过程中,发现想要知根知底的去使用node addon,需要对node架构、V8、libuv、模块加载等要点都有一个了解。所以本文的目的就是梳理如何清楚的使用node addon?
本文将从以下几点进行阐述:
node架构
node的架构相信大家都不陌生,以模块加载为角度架构图如下:
V8 engine是Google开发的javascript引擎,是一个独立运行的虚拟机,node以第三方依赖的形式引入V8(与libuv等依赖放在deps目录下)。除了作为Javascript运行引擎外,V8提供了嵌入API,为编译和执行JS脚本, 访问 C++ 方法和数据结构, 错误处理, 开启安全检查等提供了函数接口,承担着是node中js与C++桥接的重要作用。
libuv是专门为node开发的库,提供跨平台的异步I/O能力。其基于异步的、事件驱动模型,提供一个event-loop,还有基于I/O和其它事件通知的回调函数。
Builtin modules是node提供的C++模块。
Native module是node提供的js模块,其被使用者直接调用,并且有些Native module会借助下层的Builtin module。在native模块中使用builtin模块,利用的是node提供的process.binding方法(后面模块加载会有介绍)。
Addon是一个用C++写的node的动态链接库,使用者可以直接使用
require()
方法进行加载,当然前提是addon已经编译好。Addon主要用来扩展node的底层能力,具体应用可能是计算密集型的模块(C++的运行性能高,可以利用libuv异步和事件循环的能力,同时可以使用多进程、多线程)。V8
V8提供了对外的使用API,可以参考V8嵌入指南。
下面主要对其中主要概念进行简要梳理
Isolate
是一个独立的V8实例,也可以说一个独立虚拟机,其中可以包含一个或多个线程,但同一时间,只有一个线程是执行状态。Context
代表一个执行上下文(执行环境),它使得可以在一个 V8 实例中运行相互隔离且无关的 JavaScript 代码. 你必须为你将要执行的 JavaScript 代码显式的指定一个 context。Context支持嵌套。Handle
是一个指向堆内存的指针,在V8中JavaScript的值和对象也都存放在堆中,Handle提供了一个JS对象在堆内存中的地址的引用。有人会有疑问我们直接操作JS变量指针不可以嘛?由于V8的GC策略,可能会对堆中的JS变量移动其内存位置,Handle的出现可以跟踪相应变量的地址。Handle Scope
是一个Handle的容器,为了解决一个个释放handle过于繁琐,将一些handle接入handle scope中,方便统一管理(释放等)。下图主要是为了大家理解Isolate、Context、Handle Scope、Handle的大小关系,在细节上不够准确。
libuv
libuv是一个跨平台的异步I/O库。其架构图如下:
上图的左侧是网络相关的I/O,使用的都是各个平台比较有效率的多路I/O模型,Linux上的epoll,OSX和BSD类OS上的kqueue,SunOS上的event ports以及Windows上的IOCP机制。
右侧File类型的I/O,基于线程池的方式来实现异步的请求和处理。
具体讲解可参考libuv 教程。
node 模块加载
node模块可分为Native Module、Builtin Module、Constants。
Native Module在下载node源码并编译后,会在
out/Release/obj/gen
目录下node_natives.h。该文件由 js2c.py 生成,其会将node源代码中的lib目录下所有js文件以及src目录下的node.js文件中每一个字符转换成对应的ASCII码,并存放在相应的数组里面。Builtin模块会被main()之前加载到modlist_builtin中,当使用时,从链表中将模块取出即可。在每个builtin模块中,都会通过宏
NODE_BUILTIN_MODULE_CONTEXT_AWARE
预编译阶段将其转为函数_register_ ## modname
,函数会调用node_module_register
方法将其加载进modlist_builtin。tcp_wrap中宏定义如下:NODE_BUILTIN_MODULE_CONTEXT_AWARE(tcp_wrap, node::TCPWrap::Initialize)
模块加载
我们加载node C++ addon时,可以直接使用
process.binding
方法。其实process.binding是node require()的基础,所以后面也介绍了require
的实现process.binding()
process.binding()做了什么呢?
process.binding()主要做了对不同类型的模块做了不同的处理:
require
首先node中require()方法做了什么呢?
代码前面时一些path校验,那么Module._load做了什么呢?
Module._load()
主要做了三件事:我们再深度遍历代码到
NativeModule.require
NativeModule.require
主要做了两件事:nativeModule.compile()
做了什么呢?nativeModule.compile()
就是将源码wrap起来,使用script.runInThisContext
去运行。script.runInThisContext
做了什么呢?Contextify
中的runInThisText如何实现的呢?Node Addon
加载
上面讲解了node的模块加载,那么Node Addon为什么能够正确加载呢?
原因在于每个Node Addon模块入口中,需要
#include <node.h>
,node.h
包含者一些宏定义,其中有:我们又看到了我们熟悉的
node_module_register
方法,我们再写一个addon时调用的NODE_MODULE
方法,实际上就是将该模块编译后的结果加入到modlist_builtin
,我们调用process.binding()
就可以将其加载。NAN
为什么要有NAN呢?
原因在于随着Node.js和V8的版本迭代,其底层API可能会发生变化,我们写的这些原生模块又依赖了变化了的API的话,包就作废了。除非包的维护者去支持新版的API,不过这样依赖,老版Node.js下就又无法编译通过新版的包了。
为了解决这种尴尬的局面,NAN出现了,其在
nan.h
中定义了许多判断宏,会判断当前node版本,我们使用nan.h
宏定义中的方法,编译器会将其展开成不同的结果。Node v8.0之后,官方推出了N-API,它与NAN的区别在于,NAN在适配不同版本时,需要每次重新编译,而N-API将其底层接口抽象,所有版本都适用一套API即可。
The text was updated successfully, but these errors were encountered: