簡単な紹介
通常の開発では、APIのセット(requireおよびmodules.exports)を使用して、モジュール解析フレームワークを使用せずにユーザーソースモジュール(xxx.js)を管理することはめったにありませんが、実際、Nodejsには合計で4つのモジュール、つまりC ++ネイティブモジュール、ネイティブネイティブjsモジュール、ユーザーjsモジュール、ユーザーC ++拡張機能。この記事は、process.binding APIから始まり、nodejsの基盤となるC++ネイティブモジュールの読み込み原理を紹介します。
モジュールの原理を理解する必要があるのはなぜですか?
基盤となるnodejsを深く理解するための基盤を築く
nodejsの最下層を理解するには、まずさまざまなファイルの読み込みエントリを理解してから、モジュールの概念を導入する必要があります。そうすれば、nodejsのソースコード構造が比較的明確になり、その後のnodejsの学習に役立ちます。ソースコード。一般的に、 cppネイティブモジュールの登録は通常NODE_MODULE_CONTEXT_AWARE_BUILTINであり、モジュールを説明するデータノードをモジュールリストに追加するために使用されます。これはモジュールであり、osモジュールから次のように取得されます。 nodejsの最下層を理解する必要がありますか?node.jsで直接開発するのは良くありませんか?ここで私はいくつかの理由を挙げます:
- nodejsを使用してデスクトップアプリケーション(electronなど)を構築している場合、単純な開発を行っているだけでは、nodejsの最下層を理解することはあまり役に立たないかもしれませんが、デスクトップアプリケーションの機能を拡張したい場合は、 nodejsネイティブモジュールを手動で拡張して記述し、拡張機能でオペレーティングシステムのAPIを直接呼び出すことができます。
さらに、cpp拡張モジュールを使用して、node.jsおよび開発されたcppプログラムと通信するための散水レイヤーを作成できるため、nodejsを介して他の開発されたcppプログラムを直接呼び出すことができます。2.一部のnodejsフレームワークは、再構築(ビルド)する必要があるか、nodejsバージョンが変更されたときに直接実行できない場合があります。理由を理解するには、読者が理解する必要のある前提条件があります。nodejsの各バージョンでは、内部で公開されているAPIが変更される可能性があります。これは、nodejsがv8を使用してjavascriptを解析するためです。v8の基盤となるAPIが頻繁に変更されると、nodejs apiが頻繁に変更されるため、異なるnodejsバージョンを切り替えると、元のAPIが呼び出されます。フレームワークは効果がない可能性があり、この頻繁に変更される機能のために、NANが誕生しました。これは、nodejsの一連のAPI(マクロを使用)をカプセル化し、ユーザーに公開しますが、基盤となる頻繁な変更の問題を完全に回避することはできません。 nodejsのAPIは、異なるものに応答するため、nodejsバージョンでは、NANが前処理を行うため、拡張モジュールを再構築する必要があります。これは、node-sassオープンソースフレームワーク(NANで構築)などの例です。node-sassはnodejsのバージョンに大きく依存していることがわかります
cppの拡張機能
より高い効率?
インターネットでよくあることわざは、cpp拡張モジュールはコンピューティングを多用するタスクを処理できるということです。このタスクを拡張機能に渡して完了し、結果をjsレイヤーに転送して効率を向上させることができますが、これは実際には場合?テストは次のとおりです。cppを使用して、フィボナッチ数列を計算するアルゴリズムを記述します。
long long int F(int n) //由于后面数值结果较大,可使用longlong类型
{
int fibOne = 0;
int fibTwo = 1;
int fibN = 0;
for (int i = 2; i <= n; i++)
{
fibN = fibOne + fibTwo;
fibOne = fibTwo;
fibTwo = fibN;
}
return fibN;
}
复制代码
JavaScriptでもう1つ書く
function F(n){
let fibOne = 0;
let fibTwo = 1;
let fibN = 0;
for (let i = 2; i <= n; i++)
{
fibN = fibOne + fibTwo;
fibOne = fibTwo;
fibTwo = fibN;
}
return fibN;
}
function test(i){
const now=new Date();
F(i);
console.log( (new Date()-now)/1000);
}
test(100)
复制代码
結果:
データ量 | 1e8 | 1e9 | 1e10 |
---|---|---|---|
js | 0.513 | 4.679 | 45.629 |
c ++ | 0.029 | 0.276 | 0.547 |
cppはすでに効率の点でjsを押しつぶしていることがわかります...しかし、これはcppでコードを書くことを奨励するものではありませんが、このような大量の計算でcppを使用する方が合理的です。
急速な拡大
現在のjsプロジェクトに接続するために開発されたcppプロジェクトがすでに手元にある場合、どうしますか?一般的な治療法は次のとおりです。
- 両方のアプリケーションは、httpサービスを使用して相互に通信します。
毕竟是通过http报文通信,这样就会受限于http本身的限制,且程序效率一般会比直接调用低,这种通信方式github开源项目mirai(一种qq机器人框架)就有使用,具体是为了方便别的语言的使用者进行mirai开发,在它的核心内置了一个http通信插件(mirai-http-api),其他语言通过http请求,来和核心通信,然后调用核心提供的功能。 2. 直接在js里把cpp项目翻译进去。 如果直接翻译,对程序员的要求就太高了,而且会有时间成本的风险,无法快速对接。 3. 为cpp项目做一层扩展,供js调用。 优于以上两种方式,直接调用效率更高,而且开发时间会更短。
从os模块入手
process.binding是nodejs全局对象process上的一个api,作用是加载cpp源码模块,本小节以os模块为例,它是nodejs原生cpp模块之一,用于查看系统的一些基本信息。 os cpp源码模块打印结果[nodejs v6.9.4环境下]
process.binding('os')
// BaseObject {
// getHostname: [Function: getHostname],
// getLoadAvg: [Function: getLoadAvg],
// getUptime: [Function: getUptime] {
// [Symbol(Symbol.toPrimitive)]: [Function (anonymous)]
// },
// getTotalMem: [Function: getTotalMem] {
// [Symbol(Symbol.toPrimitive)]: [Function (anonymous)]
// },
// getFreeMem: [Function: getFreeMem] {
// [Symbol(Symbol.toPrimitive)]: [Function (anonymous)]
// },
// getCPUs: [Function: getCPUs],
// getInterfaceAddresses: [Function: getInterfaceAddresses],
// getHomeDirectory: [Function: getHomeDirectory],
// getUserInfo: [Function: getUserInfo],
// setPriority: [Function: setPriority],
// getPriority: [Function: getPriority],
// getOSInformation: [Function: getOSInformation],
// isBigEndian: false
// }
复制代码
你或许曾经想过,os这个模块到底是什么,在哪里?下面就让我们直接揭开谜底,nodejs底层的cpp可以当作javascript的伪代码来看,能当作javascript伪代码看也是有原因的,因为nodejs利用v8的解释器来运行js脚本,实际上js的每一个数据类型,函数定义在底层v8都有对应关系,你或许完全没有看过v8代码,但是在操作js的时候,你已经相当于接触v8了。
nodejs的cpp源码模块os,[位置:"src/node_os.cc"]
#include "node.h"
#include "v8.h"
/...一大堆头文件定义
namespace node {
namespace os {
...
//一大堆函数定义 getHostname,GetUserInfo等等
...
void Initialize(Local<Object> target, //target就是外部传进来的exports
Local<Value> unused,
Local<Context> context) {
Environment* env = Environment::GetCurrent(context);
env->SetMethod(target, "getHostname", GetHostname); //target.getHostname=GetHostName,GetHostname.name="getHostName"
env->SetMethod(target, "getLoadAvg", GetLoadAvg);
env->SetMethod(target, "getUptime", GetUptime);
env->SetMethod(target, "getTotalMem", GetTotalMemory);
env->SetMethod(target, "getFreeMem", GetFreeMemory);
env->SetMethod(target, "getCPUs", GetCPUInfo);
env->SetMethod(target, "getOSType", GetOSType);
env->SetMethod(target, "getOSRelease", GetOSRelease);
env->SetMethod(target, "getInterfaceAddresses", GetInterfaceAddresses);
env->SetMethod(target, "getHomeDirectory", GetHomeDirectory);
env->SetMethod(target, "getUserInfo", GetUserInfo);
target->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "isBigEndian"),
Boolean::New(env->isolate(), IsBigEndian()));
}
}
}
NODE_MODULE_CONTEXT_AWARE_BUILTIN(os, node::os::Initialize) //模块组册宏
复制代码
这里相当于在当前环境下的target对象上注册一个个函数,而后文中我们很快就能知道target实际上就是exports 即env->SetMethod(exports, "函数名", 函数); 而这个环境env就跟JavaScript的函数上下文环境一个道理,是Initialize的上下文环境。 根据上面JavaScript打印的process.binding('os')和下面nodejs的模块注册代码, 我们可以看出,二者无异 因此可以下结论:process.binding就是连接js和cpp的跨空间之门! 对了,看不出也没有关系,我还有解释上面的伪代码
function GetLoadAvg(){...}
...
function initialize(target,unused,context){
target.getLoadAvg=GetLoadAvg
target.getUptime=GetUptime
target.getTotalMem=GetTotalMemory
target.getFreeMem=GetFreeMemory
target.getCPUs=GetCPUInfo
target.getOSType=GetOSType
target.getOSRelease=GetOSRelease
target.getInterfaceAddresses=GetInterfaceAddresses
target.getHomeDirectory=GetHomeDirectory
target.isBigEndian=false;
}
os.initialize=initialize
NODE_MODULE_CONTEXT_AWARE_BUILTIN(os,os.initalize);
复制代码
如果要你自己设计一个模块,你会怎么设计?
首先,我们观察process.binding
接受一个'os'字符串作为模块标识
我们进一步想,光靠一个字符串当然不能找到模块,那么我们就需要设计一个'找'的过程,与'找'的目标 首先明确'找'的目标,应该是一个数据结构,这个数据结构光有名字name可不行,还得有它的输出对象exports。 现在我们就可以开始动手设计这个数据结构了。
'找'的目标node_module
struct node_module{
const char * name;
Object exports; //Object这里就不设计了,读者理解成一个定义对象的数据结构即可
}
复制代码
现在这个node_module数据结构已经设计好了,我们还得想办法存储这些模块,因为模块不止一个,这么多模块节点我们得统一管理,这里可以用链表,所以我们这里只需要预先全局定一个表头节点, 同时,在node_module新增一个成员link指向下一个节点
struct node_module{
const char * name;
Object exports;
struct node_module* link;
}
node_module *modlist_builtin;
复制代码
'找'的过程find
然后我们得设计'找'这个动作,node_module是一个节点对象,链表,我们就可以很自然的想象到遍历链表查找节点,实际上nodejs内部也是这么做的,这个'找'接受一个模块名字符串,然后输出模块
struct node_module* find(const char* name) {
struct node_module* mp
//mp是查找时的中间节点,modlist_builtin是全局已经注册好的模块列表头结点
for (mp = modlist_builtin; mp != nullptr; mp = mp->link) {
if (strcmp(mp->name, name) == 0)
break;
}
return (mp);
复制代码
在find的时候你可能会疑惑,我们的模块链表是怎么来的?链表中的模块又是怎么添加到模块链表中的? 那么接下来,我们就要设计注册的过程,将模块链表补充完整
'注册'
顺理成章,注册的时候自然就是补充modlist_builtin头结点的内容了,分两种情况,一种是头结点为空,一种是不为空 所以实现如下:
void node_module_register(node_module *m) {
if (modlist_builtin!=nullptr) {
m->link = modlist_builtin; //下一个节点指向modlist_buitin头节点
modlist_builtin = m;
}else{
modlist_builtin = m;
}
}
复制代码
注册和查找都做完了,最后还剩下便是加载,也就是上文所描述的时空之门process.binding,这个binding函数
加载
struct node_module* get_builtin_module(char* name) {
struct node_module* mp;
for (mp = modlist_builtin; mp != nullptr; mp = mp->link) {
//比较mp.name和name是否相同,strcmp是一个比较字符串的函数,它在相同的时候才会返回0
if (strcmp(mp->name, name) == 0) break;
}
return mp;
}
Object Binding(char *name) {
node_module *mod = get_builtin_module(name);
return mod->exports;
}
复制代码
这样就完成了一个最简单的模块,但是细心的小伙伴可能会发现了,nodejs中的模块应该是按需加载,而这里的node_module结构中包含了已经加载好的exports,这不符合按需的思想,那么接下来我们就要进行一些小改造, 首先,我们在struct node_module中不应该直接获取exports,而是获取一个注册exports的init函数,然后Binding改造成加载时运行注册函数结果即返回注册好的exports
struct node_module {
char * name;
Object (*init)(Object);
struct node_module* link;
};
Object Binding(char *name) {
node_module *mod = get_builtin_module(name);
Object exports;
return mod->init(exports);
}
复制代码
最终我们设计完了我们的模块,如果要注册一个模块,用户侧使用如下:
binding源码原理
这里的binding就是nodejs的process.binding,接下来就是直接讲述nodejs源码中的模块加载了。 nodejs源码Binding[位置:"src/node.cc"]
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; //声明exports对象
//如果缓存中有当前模块,就直接返回;
if (cache->Has(env->context(), module).FromJust()) {
exports = cache->Get(module)->ToObject(env->isolate());
args.GetReturnValue().Set(exports);
return;
}
...
//获得cpp核心源码模块,这里很重要,后面会细说。
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.
...
Local<Value> unused = Undefined(env->isolate());
mod->nm_context_register_func(exports, unused,
env->context(), mod->nm_priv); //很重要,mod->prive运行注册os模块时的initial函数
cache->Set(module, exports); //cache.module=exports
} else if (!strcmp(*module_v, "constants")) {
...
} else if (!strcmp(*module_v, "natives")) {
//获取native本地js模块,native模块的位置在“lib/”下
} else {
...
}
args.GetReturnValue().Set(exports); //将exports返回到nodejs本地环境,这时候process.binding('os')得到了返回值(一个对象)
}
复制代码
如果看不懂上面,也没关系,可以看看我写好的伪代码
function binding(args){
const env=Environment.getCurrent(args);
let module=args[0];
let module_v=module.toUtf8Value();
let cache=env.cache;//从全局获取缓存对象。
//查询cache,如果已经加载过模块了,就直接从缓存中返回
if(cache(module)){
return module.exports;
}
//重点逻辑,获取cpp核心模块
let mod=get_builtin_module(module_v);
if(mod){
let exports={};
mod.init(exports,undefined,env.context,mod.nm_priv); //exports被init初始化
}
return exports;//返回exports,到nodejs命令行环境中。
}
复制代码
接下来比较重要的是,get_builtin_module如何获取cpp核心源码模块?
get_builtin_module获取核心模块
这个函数内部的工作就是在一个名为modlist_builtin的c++核心模块链表上对比文件标识,从而返回相应的模块。
struct node_module* get_builtin_module(const char* name) {
struct node_module* mp;
//modlist_builtin是一个链表,链接着一个个已经注册好的模块,注册的过程后续会继续讲。
for (mp = modlist_builtin; mp != nullptr; mp = mp->nm_link) {
//比较nm_modname和name是否相同,strcmp是一个比较函数,比较诡异的是,它在相同的时候才会返回0
if (strcmp(mp->nm_modname, name) == 0)
break;
}
return (mp);
复制代码
最后,我们的疑惑应该只剩下,modlist_builtin是怎么来的了?请看下面分解: 还记得一开始os底层cpp源码里面最后有一句
NODE_MODULE_CONTEXT_AWARE_BUILTIN(os, node::os::Initialize)
复制代码
源码逻辑并不复杂,我们只需要知道这句话就是注册到链表上就好了
cpp源码模块注册
osソースコードモジュールは最終的にNODE_MODULE_CONTEXT_AWARE_BUILTIN登録モジュールを呼び出します
NODE_MODULE_CONTEXT_AWARE_BUILTIN(os, node::os::Initialize)
复制代码
上記のマクロ呼び出し
#define NODE_MODULE_CONTEXT_AWARE_BUILTIN(modname, regfunc) \
NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL, NM_F_BUILTIN) \
复制代码
また、NODE_MODULE_CONTEXT_AWARE_Xは、モジュールを登録するためのキーマクロです。実際には、モジュール構造を定義し、関数node_module_registerを介してモジュール構造を登録し、次のように定義します。
#define NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, priv, flags) \
static node::node_module _module = \
{ \
NODE_MODULE_VERSION, \
flags, \
NULL, \
__FILE__, \
NULL, \
(node::addon_context_register_func) (regfunc), \
NODE_STRINGIFY(modname), \
priv, \
NULL \
}; \
node_module_register(&_module); \
}
//node_module的定义
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;
};
复制代码
構造体_moduleは上記で定義されており、モジュールの基本情報が含まれており、node_module_registerを介してモジュールリストに登録されます。
static node_module* modlist_builtin; //头结点
void node_module_register(void* m) {
if (m->nm_flags & NM_F_BUILTIN) {
//很经典的单链表指法
m->nm_link = modlist_builtin; //下一个节点指向modlist_buitin头节点
modlist_builtin = m; //mp成为新的头节点
}
...
}
复制代码