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

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

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

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

  Register:登记、注册。

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

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

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

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

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

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

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

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

  这样,通过一个静态指针,即可访问到所有注册的内置模块。

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

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

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

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

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

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

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

// Set up NativeModule
function NativeModule(id) {
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.loaded = false;
this.loading = false;
}

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

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

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

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

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

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

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

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

// 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) == )
break;
}
// 检测一下 没找到mp就是空指针
CHECK(mp == nullptr || (mp->nm_flags & flag) != );
return mp;
}

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

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

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

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

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

深入出不来nodejs源码-内置模块引入初探的更多相关文章

  1. 深入出不来nodejs源码-内置模块引入再探

    我发现每次细看源码都能发现我之前写的一些东西是错误的,去改掉吧,又很不协调,不改吧,看着又脑阔疼…… 所以,这一节再探,是对之前一些说法的纠正,另外再缝缝补补一些新的内容. 错误在哪呢?在之前的初探中 ...

  2. 深入出不来nodejs源码-V8引擎初探

    原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...

  3. 深入出不来nodejs源码-timer模块(JS篇)

    鸽了好久,最近沉迷游戏,继续写点什么吧,也不知道有没有人看. 其实这个node的源码也不知道该怎么写了,很多模块涉及的东西比较深,JS和C++两头看,中间被工作耽搁回来就一脸懵逼了,所以还是挑一些简单 ...

  4. 深入出不来nodejs源码-编译启动(1)

    整整弄了两天,踩了无数的坑,各种奇怪的error,最后终于编译成功了. 网上的教程基本上都过时了,或者是版本不对,都会报一些奇怪的错误,这里总结一下目前可行的流程. node版本:v10.1.0. 首 ...

  5. 深入出不来nodejs源码-events模块

    这一节内容超级简单,纯JS,就当给自己放个假了,V8引擎和node的C++代码看得有点脑阔疼. 学过DOM的应该都知道一个API,叫addeventlistener,即事件绑定.这个东西贯穿了整个JS ...

  6. 深入出不来nodejs源码-从fs.stat方法来看node架构

    node的源码分析还挺多的,不过像我这样愣头完全平铺源码做解析的貌似还没有,所以开个先例,从一个API来了解node的调用链. 首先上一张整体的图,网上翻到的,自己懒得画: 这里的层次结构十分的清晰, ...

  7. 深入出不来nodejs源码-timer模块(C++篇)

    终于可以填上坑了. 简单回顾一下之前JS篇内容,每一次setTimeout的调用,会在一个对象中添加一个键值对,键为延迟时间,值为一个链表,将所有该时间对应的事件串起来,图如下: 而每一个延迟键值对的 ...

  8. 深入出不来nodejs源码-流程总览

    花了差不多两周时间过了下primer C++5th,完成了<C++从入门到精通>.(手动滑稽) 这两天看了下node源码的一些入口方法,其实还是比较懵逼的,语法倒不是难点,主要是大量的宏造 ...

  9. Django 源码小剖: 初探 WSGI

    Django 源码小剖: 初探 WSGI python 作为一种脚本语言, 已经逐渐大量用于 web 后台开发中, 而基于 python 的 web 应用程序框架也越来越多, Bottle, Djan ...

随机推荐

  1. OpenStack Kilo版加CEPH部署手册

    OpenStack Kilo版加CEPH部署手册 作者: yz联系方式: QQ: 949587200日期: 2015-7-13版本: Kilo 转载地址: http://mp.weixin.qq.co ...

  2. mssql借助链接服务器进行数据快速迁移

    在工作中,遇到了一个任务,需要将A数据库的数据迁移到B数据库,两个数据库的数据结构是一样的.B数据库已经在相当数量的数据了,故而不能通过复制数据库的方式来实现.  旧方法 在对数据迁移的时候,一开始使 ...

  3. Abp添加菜单

    Abp添加菜单 在abp模板中添加菜单,EntityFramework+Angular.js模板,使用的Abp版本为3.8.1. 创建Abp项目模板,例如名称叫做LawAndRegulation. 服 ...

  4. WPF MeasureOverride和 ArrangeOverride做个 页面导航

    public class NavigationPanel:Panel { protected override Size MeasureOverride(Size availableSize) { S ...

  5. Lerning Entity Framework 6 ------ Using a commandInterceptor

    Sometimes, We want to check the original sql statements. creating a commandInterceptor is a good way ...

  6. python项目飞机大战

    实现步骤 1.创建窗口 2.创建一个玩家飞机,按方向键可以左右移动 3.给玩家飞机添加按空格键发射子弹功能 4.创建一个敌机 5.敌机自动左右移动 6.敌机自动发射子弹 1.创建窗口 import p ...

  7. Strust2总结

    1. JavaEE软件三层结构和MVC的区别? JavaEE软件三层机构是由sun公司提供JavaEE开发规范的:Web层(表现层).业务逻辑层.数据持久层.[其中WEB层会使用前端控制器模式] MV ...

  8. 实现域名访问网站—nginx反向代理

    今天在跟项目的时候,是否被耍了三个多小时,最后在我准备号材料准备他人求助的时候,在收集材料的时候,居然访问通了, 别问我为什么,我也不知道 ,哈哈哈哈(苦逼脸...) 分享出来,大家共同学习: 这个是 ...

  9. Linux学习笔记-基本操作1

    1>. 命令解析器2>. Linux快捷键3>. Linux 系统目录结构4>. 用户目录5>. 文件和目录操作6>. 文件和目录的属性7>. 文件权限, 用 ...

  10. WebRTC开发基础(WebRTC入门系列1:getUserMedia)

    什么是WebRTC WebRTC由IETF(Internet Engineering Task Force——互联网工程任务组)和W3C(World Wide Web Consortium——万维网联 ...