1_q9myzo5Au8OfsaSrCodNmw7.png

node的原生扩展原理

node中除了内建原生模块和链接模块外,还有一种使用原生的语言编写的模块,node的c++扩展模块。node的原生扩展模块是一种动态扩展node的功能,因为它使用的是原生语言区编写,所以对比用户模块,它执行的速度更快。更重要的是,它可以执行系统调用。这是它最重要的一点。
node的c++扩展实际上是一个动态链接库(在windows上为dll文件,在linux上为.so文件,为了统一名称在node中统一以.node为后缀)。和链接模块不同的是。node的c++扩展并不需要和node源代码一起打包构建。可以在项目中动态加载。node通过在不同平台的api把该动态链接库的导出函数导出到node中使用。
下面我们通过一个例子去看一下dll的加载和node的c++扩展的开发。winStrCmp 。在windows上有一个叫shlwapi.dll ,在dll中有一个导出函数StrCmpLogicalW。用来字符串进行比较,这个函数不是使用字母的编码进行比较。在中文的环境下,它使用了中文的拼英的字母进行排序。

#include <node.h>
#include<uv.h>
#include<shlwapi.h>
#include<vector>  
#include <algorithm> 
#include<string>
#include <iostream>


enum compareType {
    DESC,
    ASC
};

void compare(const v8::FunctionCallbackInfo<v8::Value>& args, compareType type) {

    v8::Isolate* isolate = args.GetIsolate();
    v8::Local<v8::Array> array = args[0].As<v8::Array>();
    v8::Local<v8::Function> callBack = args[1].As<v8::Function>();
    // 加载windows的dll函数
    HINSTANCE hDLL = LoadLibrary("shlwapi.dll");

    if (hDLL == 0)
    {
        return;
    }

    int (*strCompare)(PCWSTR, PCWSTR);

    // 获取dll的函数签名
    void* fun= GetProcAddress(hDLL,"StrCmpLogicalW");

    // 强制转型
    strCompare = (int (*)(PCWSTR, PCWSTR))fun;

    size_t len = array->Length();

    std::vector<std::string> vec;

    // 参数类型转换
    for (size_t i = 0; i < len; i++)
    {
        v8::String::Utf8Value utf8_value(isolate, array->Get(i));
        std::string str(*utf8_value);
        vec.push_back(str);
    }
    // 排序
    std::sort(vec.begin(), vec.end(), [=](std::string& front_str, std::string& next_str) {
        std::wstring front_wstr;
        front_wstr.assign(front_str.begin(), front_str.end());
        std::wstring next_wstr;
        next_wstr.assign(next_str.begin(), next_str.end());

        int result = strCompare(front_wstr.data(), next_wstr.data());
        return  type == compareType::ASC ? result < 0 : result > 0;
        });

    // 把c++的数组转换成v8的javaScript数据类型
    for (size_t i = 0; i < len; i++)
    {
        array->Set(i, v8::String::NewFromUtf8(isolate, vec[i].c_str()));
    }

    const int argc = 1;
    v8::Local<v8::Value> argv[argc] = {array};
    // 执行回调函数
    callBack->Call(v8::Null(isolate), 1, argv);
    // 释放dll
    FreeLibrary(hDLL);
}

void DESCMethod(const v8::FunctionCallbackInfo<v8::Value>& args) {
    compare(args, compareType::DESC);
}

void ASCMethod(const v8::FunctionCallbackInfo<v8::Value>& args) {
    compare(args, compareType::ASC);
}

void Initialize(v8::Local<v8::Object> exports) {
    // 降序排序
    NODE_SET_METHOD(exports, "strCompareDesc", DESCMethod);
    // 升序排序
    NODE_SET_METHOD(exports, "strCompareAsc", ASCMethod);
}


NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

编写一份javaScript文件作为该c++扩展的中间层,该中间层只要是为了参数数据类型校验。


const winStrCmp = require('../build/Release/winStrCmp.node');


function validArgs(array,callBack) {
    if(!Array.isArray(array) || array.length === 0|| typeof callBack !== 'function') {
        throw Error("参数错误");
    }

    for(let i = 0; i < array.length;++i) {
        if(typeof array[i] !== 'string') {
            throw Error(`参数错误,${array[i]}不是字符串`);
        }
    }
}

exports.strCompareAsc = function(array,callBack) {
    validArgs(array,callBack);
    winStrCmp.strCompareAsc(array,callBack);
}

 exports.strCompareDesc = function(array,callBack) {
     validArgs(array,callBack);
     winStrCmp.strCompareDesc(array,callBack);
}

最后的使用。

let strCmp =  require('../lib/index.js');


strCmp.strCompareAsc(["2","1"],function(array){
    console.log(array);
});

strCmp.strCompareDesc(["2","1"],function(array){
    console.log(array);
});

在上面的例子可以知道。在windows系统下,要加载dll的某个函数。首先先加载dll。再通过函数名称去获取dll中导出的函数地址。最后通过函数签名把导出的函数进行强制类型转换。最后需要释放dll。windows动态链接库的加载流程
windows动态链接库dll的加载过程.png
在用户模块的加载中我们知道。当我们使用require(‘c++原生扩展的时候’),最后的处理该模块的执行函数为internal\modules\cjs\loader.jsModule._extensions['.node']

Module._extensions['.node'] = function(module, filename) {
  // 安全策略,这里不做讲解
  if (policy?.manifest) {
    const content = fs.readFileSync(filename);
    const moduleURL = pathToFileURL(filename);
    policy.manifest.assertIntegrity(moduleURL, content);
  }
  //通过process.dlopen去获取
  return process.dlopen(module, path.toNamespacedPath(filename));
};

接着通过调用process.dlopen方法进行模块的加载。在node的启动流程中我们知道。在执行internal\bootstrap\node.js内建javaScript模块的时候。为process对象挂载了大量的的属性和方法。其中dlopen方法就是在这里进行挂载。

const rawMethods = internalBinding('process_methods');
{
  process.dlopen = rawMethods.dlopen;
  process.uptime = rawMethods.uptime;
}

process.dlopen()的指向为rawMethods.dlopen()rawMethods模块为内建原生模块。模块的地址的文件为node_process_methods.cc

// 内容有省略
static void InitializeProcessMethods(Local<Object> target,
                                     Local<Value> unused,
                                     Local<Context> context,
                                     void* priv) {
  Environment* env = Environment::GetCurrent(context);
  env->SetMethodNoSideEffect(target, "cwd", Cwd);
  env->SetMethod(target, "dlopen", binding::DLOpen);
}
NODE_MODULE_CONTEXT_AWARE_INTERNAL(process_methods,
                                   node::InitializeProcessMethods)

内建模块process_methods()的最终调用为binding::DLOpen()

void DLOpen(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  auto context = env->context();

  CHECK_NULL(thread_local_modpending);

  // 参数校验,必须为2个以上参数,
  // 参数1为javaScript传递过来的module对象。参数二为模块的绝对路径。参数3为模块的标记
  if (args.Length() < 2) {
    return THROW_ERR_MISSING_ARGS(env, "process.dlopen needs at least 2 arguments");
  }

  int32_t flags = DLib::kDefaultFlags;

  // 构建v8的javaScript对象
  // module和exports,并设置对象module的exports熟悉指向exports对象 
  if (args.Length() > 2 && !args[2]->Int32Value(context).To(&flags)) {
    return THROW_ERR_INVALID_ARG_TYPE(env, "flag argument must be an integer.");
  }

  Local<Object> module;
  Local<Object> exports;
  Local<Value> exports_v;

  // 把从javaScript传递过来的module对象转成c++层的v8对象
  if (!args[0]->ToObject(context).ToLocal(&module) ||
      !module->Get(context, env->exports_string()).ToLocal(&exports_v) ||
      !exports_v->ToObject(context).ToLocal(&exports)) {
    return;  // Exception pending.
  }

  node::Utf8Value filename(env->isolate(), args[1]);
  // 加载动态链接库
  env->TryLoadAddon(*filename, flags, [&](DLib* dlib) {
    static Mutex dlib_load_mutex;
    Mutex::ScopedLock lock(dlib_load_mutex);

    // 加载dll
    const bool is_opened = dlib->Open();

    // 包含 node v14 或更高版本模块的对象将自行注册
    // 采用了动态链接库的全局构造函数段执行。
    node_module* mp = thread_local_modpending;
    thread_local_modpending = nullptr;

    // 如果加载dll失败
    if (!is_opened) {
      std::string errmsg = dlib->errmsg_.c_str();
      dlib->Close();
      THROW_ERR_DLOPEN_FAILED(env, errmsg.c_str());
      return false;
    }
    // 如果采用的是自动注册的方式。获取当前的node_module
    if (mp != nullptr) {
      if (mp->nm_context_register_func == nullptr) {
        if (env->force_context_aware()) {
          dlib->Close();
          THROW_ERR_NON_CONTEXT_AWARE_DISABLED(env);
          return false;
        }
      }
      mp->nm_dso_handle = dlib->handle_;
      // 保存到全局变量global_handle_map中
      dlib->SaveInGlobalHandleMap(mp);
    } else {

      // node的c++扩展有多种加载方式。
      // 普通的模块加载。n_api的加载
      if (auto callback = GetInitializerCallback(dlib)) {
        callback(exports, module, context);
        return true;
      } else if (auto napi_callback = GetNapiInitializerCallback(dlib)) {
        // 通过n_api的方式加载
        napi_module_register_by_symbol(exports, module, context, napi_callback);
        return true;
      } else {
        // 从全局的变量global_handle_map中获取
        mp = dlib->GetSavedModuleFromGlobalHandleMap();
        if (mp == nullptr || mp->nm_context_register_func == nullptr) {
          dlib->Close();
          char errmsg[1024];
          snprintf(errmsg,
                   sizeof(errmsg),
                   "Module did not self-register: '%s'.",
                   *filename);
          THROW_ERR_DLOPEN_FAILED(env, errmsg);
          return false;
        }
      }
    }

    // 检验node_module的版本。nm_version == -1 是n_api的使用
    // 如果该模块不是n_api的模块,和模块的版本和当前的node版本不对。
    if ((mp->nm_version != -1) && (mp->nm_version != NODE_MODULE_VERSION)) {

      // 尝试加载
      if (auto callback = GetInitializerCallback(dlib)) {
        callback(exports, module, context);
        return true;
      }

      // 加载失败后抛出异常
      char errmsg[1024];
      snprintf(errmsg,
               sizeof(errmsg),
               "The module '%s'"
               "\nwas compiled against a different Node.js version using"
               "\nNODE_MODULE_VERSION %d. This version of Node.js requires"
               "\nNODE_MODULE_VERSION %d. Please try re-compiling or "
               "re-installing\nthe module (for instance, using `npm rebuild` "
               "or `npm install`).",
               *filename,
               mp->nm_version,
               NODE_MODULE_VERSION);
      dlib->Close();
      THROW_ERR_DLOPEN_FAILED(env, errmsg);
      return false;
    }

    // 执行模块导出
    Mutex::ScopedUnlock unlock(lock);
    if (mp->nm_context_register_func != nullptr) {
      mp->nm_context_register_func(exports, module, context, mp->nm_priv);
    } else if (mp->nm_register_func != nullptr) {
      mp->nm_register_func(exports, module, mp->nm_priv);
    } else {
      dlib->Close();
      THROW_ERR_DLOPEN_FAILED(env, "Module has no declared entry point.");
      return false;
    }

    return true;
  });
}

nod::DLOpen()是获取node的c++扩展的动态链接库的方法,为了旧版本的扩展的兼容,在该函数里做了大量的兼容处理。下面我们逐步讲解一个node的c++扩展动态链接库的加载过程。

  1. 在windows上的dll加载流程中我们知道。要获取dll的导出函数,首先需要的就是加载dll。在这里所有的逻辑被封装在DLib里。在Environment::TryLoadAddon()的函数中我们看到,TryLoadAddon()根据dll的名称构建了一个Dlib对象,并把对象作为参数回调回调函数。

    inline void Environment::TryLoadAddon(
     const char* filename,
     int flags,
     const std::function<bool(binding::DLib*)>& was_loaded) {
    loaded_addons_.emplace_back(filename, flags);
    if (!was_loaded(&loaded_addons_.back())) {
     loaded_addons_.pop_back();
    }
    }
    

    在回调函数中通过调用了DLib::open()函数去加载动态链接库。

    bool DLib::Open() {
    int ret = uv_dlopen(filename_.c_str(), &lib_);
    if (ret == 0) {
     handle_ = static_cast<void*>(lib_.handle);
     return true;
    }
    errmsg_ = uv_dlerror(&lib_);
    uv_dlclose(&lib_);
    return false;
    }
    

    接着通过libuv提供的跨平台获取dll的方法uv_dlopen()去加载dll,在这里我们以window系统为例。

    int uv_dlopen(const char* filename, uv_lib_t* lib) {
    WCHAR filename_w[32768];
    
    lib->handle = NULL;
    lib->errmsg = NULL;
    // 把utf-8编码的字符转换成utf-16,因为windows系统使用了utf-16进行字符编码
    // windows很多系统api也是需要的参数也是以utf-16的编码。
    if (!MultiByteToWideChar(CP_UTF8,
                            0,
                            filename,
                            -1,
                            filename_w,
                            ARRAY_SIZE(filename_w))) {
     return uv__dlerror(lib, filename, GetLastError());
    }
    // 把获取dll的句柄储存在 uv_lib_t的handle属性上。
    lib->handle = LoadLibraryExW(filename_w, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
    
    // 如果句柄为空,代表着加载dll失败
    if (lib->handle == NULL) {
     return uv__dlerror(lib, filename, GetLastError());
    }
    
    return 0;
    }
    
  2. 在node14版本或14版本后,node的c++扩展类型的dll的注册为主动注册,什么叫主动注册?就是打开dll的时候。直接执行dll导出函数。链接模块章节中我们知道。链接模块也是采用了自动注册的方式。但和链接模块不同的是,链接模块会以静态库的形式最后链接到node.exe可执行文件中,在全局构造函数中执行自动注册。新版本的node的c++扩展也是通过全局构造的方式进行模块的注册。因为node的c++扩展时一个动态链接库,在应用程序中属于动态加载。所以它的注册时机是在动态链接库打开的时候。我们先看一下如何创建一个node的c++扩展。 ```cpp

static void Init(v8::Local exports, v8::Local module, v8::Local context) { exports->Set( context, v8::String::NewFromOneByte(isolate, reinterpret_cast(“key”)) .ToLocalChecked(), v8::String::NewFromOneByte(isolate, reinterpret_cast(“value”)) .ToLocalChecked()) .FromJust(); } NODE_MODULE(test_module, Init)

和其他原生模块一样。提供一个模块名称,和一个模块加载函数。通过宏`NODE_MODULE`进行模块的注册。宏展开后得到。
```cpp
  extern "C" {                                                        
    static node::node_module _module =                                
    {                                                                 
      NODE_MODULE_VERSION,                                            
      0,                                                          
      NULL,                                                              
      __FILE__,                                                      
      Init,                          
      NULL,                                                             
      "test_module",                                        
      NULL,                                                           
      NULL        
    };       
    static void __cdecl _register_test_module(void);                             
      __declspec(dllexport, allocate(".CRT$XCU"))                         
      void (__cdecl* _register_test_module_)(void) = _register_test_module;                              
      static void __cdecl _register_test_module(void) {
        node_module_register(&_module);         
    }                                                             
  }

展开后基本和链接模块一样。也是通过链接windows crt在全局构造函数中进行了自动执行。最后通过执行 node_module_register()函数进行了注册。关键在这里无论是内建模块,链接模块,还是node的c++扩展模块,最终都是调用了node_module_register()

static thread_local node_module* thread_local_modpending;

bool node_is_initialized = false;

extern "C" void node_module_register(void* m) {
  struct node_module* mp = reinterpret_cast<struct node_module*>(m); 
  // 用于内建原生模块
  if (mp->nm_flags & NM_F_INTERNAL) {
    mp->nm_link = modlist_internal;
    modlist_internal = mp;
  } else if (!node_is_initialized) {
    // 链接模块
    mp->nm_flags = NM_F_LINKED;
    mp->nm_link = modlist_linked;
    modlist_linked = mp;
  } else {
    // node的c++扩展模块
    thread_local_modpending = mp;
  }
}

node_module_register我们知道,mp->nm_flags & NM_F_INTERNAL用于判断是否是内建模块,!node_is_initialized用于判断是否是链接模块,在node的启动流程我们知道,在执行node的初始化后,才执行全局变量node_is_initialized= true。因为链接模块在全局构造函数中执行了。所以此处的node_is_initialized还为默认值false。node的c++扩展是在node运行时才去加载,这是后的node已经进行了初始化。因此可以通过这几种情况区分了不同的模块。与内建模块和链接模块的不同,node的c++扩展使用了全局线程本地node_module变量保存了node的原生扩展的node_module指针变量thread_local node_module* thread_local_modpending上。

  1. 在node::DLOpen()函数中通过获取全局变量thread_local node_module* thread_local_modpending从而获取了node的扩展模块。

    // 获取全局对象上的模块指针
    node_module* mp = thread_local_modpending;
    // 重新设置为空指针
    thread_local_modpending = nullptr;
    
  2. 由于历史原因,node的原生扩展的加载进行了对多次的修改变化。为了兼容旧版本的非自动加载node原生扩展。node进行了多种情况的加载。

    if (mp != nullptr) {
       if (mp->nm_context_register_func == nullptr) {
         if (env->force_context_aware()) {
           dlib->Close();
           THROW_ERR_NON_CONTEXT_AWARE_DISABLED(env);
           return false;
         }
       }
       mp->nm_dso_handle = dlib->handle_;
       dlib->SaveInGlobalHandleMap(mp);
     } else {
       // node的c++扩展有多种加载方式。
       // 普通的模块加载。n_api的加载
       if (auto callback = GetInitializerCallback(dlib)) {
         callback(exports, module, context);
         return true;
       } else if (auto napi_callback = GetNapiInitializerCallback(dlib)) {
         // 通过n_api的方式加载
         napi_module_register_by_symbol(exports, module, context, napi_callback);
         return true;
       } else {
         // 从全局的对象上获取
         mp = dlib->GetSavedModuleFromGlobalHandleMap();
         if (mp == nullptr || mp->nm_context_register_func == nullptr) {
           dlib->Close();
           char errmsg[1024];
           snprintf(errmsg,
                    sizeof(errmsg),
                    "Module did not self-register: '%s'.",
                    *filename);
           THROW_ERR_DLOPEN_FAILED(env, errmsg);
           return false;
         }
       }
    }
    
  • 通过判断模块对象mp是否为空从判断该模块是否为自动注册的模块。如果该模块为自动注册模块,最终会通过DLib::SaveInGlobalHandleMap()把模块保存在全局对象global_handle_map的属性里。
  • 如果模块非自动加载模块.会进入模块兼容加载逻辑。首先使用了 GetInitializerCallback()最开始node直接使用v8 api实现的的扩展方式。GetInitializerCallback()的主要逻辑也是通过windows api GetProcAddress区获取dll的导出方法。在dll的加载流程中我们知道GetProcAddress()需要dll的句柄和导出函数的名称、在加载dll的时候获取了dll的句柄并保存了起来。导出函数名为node_register_module_v + 版本号。获取导出的函数指针后最后通过强制类型转换转换成模块注册函数的类型。 ```cpp

using InitializerCallback = void ()(Local exports, Local module, Local context); inline InitializerCallback GetInitializerCallback(DLib dlib) { const char* name = “node_register_module_v” STRINGIFY(NODE_MODULE_VERSION); return reinterpret_cast(dlib->GetSymbolAddress(name)); }

```cpp
void* DLib::GetSymbolAddress(const char* name) {
  void* address;
  if (0 == uv_dlsym(&lib_, name, &address)) return address;
  return nullptr;
}
int uv_dlsym(uv_lib_t* lib, const char* name, void** ptr) {
  *ptr = (void*)(uintptr_t) GetProcAddress(lib->handle, name);
  return uv__dlerror(lib, "", *ptr ? 0 : GetLastError());
}

由此可以知道。旧版本的c++原生扩展的动态链接库,以node_register_module_v + 版本号为导出函数名,模块注册函数为导出函数。实现了动态链接库的导出逻辑。如果导出的函数不为空。即通过该导出函数实现模块的执行。如果为空往下执行其他导出dll函数的逻辑。

  • 接着通过GetNapiInitializerCallback()函数执行旧版本的n_api实现的模块的函数导出。
    ```cpp

inline napi_addon_register_func GetNapiInitializerCallback(DLib dlib) { // napi_register_module_v + 版本号 const char name = STRINGIFY(NAPI_MODULE_INITIALIZER_BASE) STRINGIFY(NAPI_MODULE_VERSION); return reinterpret_cast( dlib->GetSymbolAddress(name)); }

`GetNapiInitializerCallback`函数逻辑和只是使用v8 api实现的模块的逻辑一样,只是使用的导出函数名不同。如果成功获取导出函数。就会使用`napi_module_register_by_symbol()` 函数进行模块的注册,这个函数只要是做了v8 api的转换。具体实现细节和原理,下面章节做出解析。如果导出的函数为空,继续往下做兼容加载。

- 通过在全局对象`global_handle_map`中查找。如果查找不到做异常处理
```cpp
// 从缓存中获取。
mp = dlib->GetSavedModuleFromGlobalHandleMap();
if (mp == nullptr || mp->nm_context_register_func == nullptr) {    
    dlib->Close();
    char errmsg[1024];
    snprintf(errmsg,
             sizeof(errmsg),
             "Module did not self-register: '%s'.",
             *filename);
    THROW_ERR_DLOPEN_FAILED(env, errmsg);
    return false;
}
  1. 执行模块的注册。
    if (mp->nm_context_register_func != nullptr) {
       mp->nm_context_register_func(exports, module, context, mp->nm_priv);
    } else if (mp->nm_register_func != nullptr) {
      mp->nm_register_func(exports, module, mp->nm_priv);
    } else {
      dlib->Close();
      THROW_ERR_DLOPEN_FAILED(env, "Module has no declared entry point.");
      return false;
    }
    
    总结上面的加载流程,可以归纳一下node的原生扩展的加载流程
    node的原生扩展的加载流程分析.png