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

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

需要关注的只要那个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. Python学习--和 Oracle 交互

    python 连接oracle 数据库 1.安装 cx_oracle pip install cx_oracle 2.出现 cx_Oracle.DatabaseError: DPI-1047: 64- ...

  2. C#: 以管理员权限运行包含有cd命令的.bat文件

    最近在做项目的时候遇到一种情:用C#程序以管理员权限去执行一个bat文件,且此bat文件里面有cd命令来进入文件的下一级目录,比如: echo test begin cd test1 setup1.e ...

  3. 2019年微服务实践第一课,网易&谐云&蘑菇街&奥思技术大咖深度分享

    微服务的概念最早由Martin Fowler与James Lewis于2014年共同提出,核心思想是围绕业务能力组织服务,各个微服务可被独立部署,服务间是松耦合的关系,以及数据和治理的去中心化管理.微 ...

  4. ffplay源码分析5-图像格式转换

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10311376.html ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg ...

  5. 【BZOJ2328】 [HNOI2011]赛车游戏

    BZOJ2328 [HNOI2011]赛车游戏 前言 这道题目我真的佛了,卡精度+卡时间这就是下一个聊天鬼才. Solution 首先可以二分出最大速度,然后考虑下坡的话可能有更好的解,然后这样子算一 ...

  6. Python-使用PyQT生成图形界面

    1.安装PyQT5以及QT Designer工具包 pip install PyQt5 pip install PyQt5-tools -i http://pypi.douban.com/simple ...

  7. AndroidStudio配置LitePal

    配置,许多书上还有教程都忽略了将LitePal下载下来和拷贝的过程,这里写一个详细的课程 首先,前往GitHub,下载LitePal的包. 然后解压,会看到这个 进入download 自己选个版本,然 ...

  8. Unity MVC 个人想法

    Unity MVC 个人想法 Model 想要实现效果 保存服务器数据 没有业务逻辑 接受发送消息 代码实现 缓存服务器发来的数据 注册消息接口 提供消息发送接口 View 想要实现效果 实现单元测试 ...

  9. odoo 开发基础 -- 视图之widget

    Odoo 中的widget many2many_tags one2many_list selection progressbar selection statusbar handle monetary ...

  10. Scala之隐式转换implicit详解

    假设我们有一个表示文本的行数的类LineNumber: class LineNumber ( val num : Int ) 我们可以用这个类来表示一本书中每一页的行数: val lineNumOfP ...