ソースコードの観点からのnodejsモジュールの解釈

簡単な紹介

通常の開発では、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モジュールから次のように取得されますoscppネイティブモジュール。 nodejsの最下層を理解する必要がありますか?node.jsで直接開発するのは良くありませんか?ここで私はいくつかの理由を挙げます:

  1. 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オープンソースフレームワーク(NANnode-sassバージョンサポートリスト構築)などの例です。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プロジェクトがすでに手元にある場合、どうしますか?一般的な治療法は次のとおりです。

  1. 両方のアプリケーションは、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成为新的头节点
  }
 ...
}
复制代码

おすすめ

転載: juejin.im/post/7079621521869651998