1_q9myzo5Au8OfsaSrCodNmw2.png

链接模块的实现

链接模块和node的c++扩展类似,都是通过打包成一个动态链接库(windows 下为dll文件。linux为.so文件)。与node原生扩展不同的是,链接扩展是一种静态加载。在windows上。dll文件的加载分为两种。一种叫动态加载,就行node的c++扩展一样。在使用的时候才需要去加载到内存中,另一种叫静态加载。所谓的静态加载,就是在可执行文件被执行的时候就完成加载到可执行文件中。
链接模块模块类型为NM_F_LINKED。该模块是要是为了嵌入式node的应用而生。例如electron。在没有链接模块的时候,当嵌入式应用需要在node中一些内置原生模块的时候只能添加内建模块。但内建模块有被修改的风险。当你使用了node作为嵌入式。并却在内建模块模块中添加了自定义的模块。万一有一天node使用了和你同一个名字的模块。这样你的应用还需要去修改。另外编写编写内建模块的时候不利于嵌入式应用node版本的升级。node的链接模块和内建原生模块都是使用了node_module_register注册函数执行模块注册。但他们注册时机不同。在内建原生模块章节中我们知道。内建模块的注册时机是在node进行初始化的时候在执行函数InitializeNodeWithArgs执行 binding::RegisterBuiltinModules();函数进行内建模块的注册。而链表模块使用的是可执行文件的全局构造函数的方式进行注册(下面做出解析)。
链接模块需要和node一起编译。使用c++编写。在javaScript可以通过进程对象去获取链接模块。

  1. process._linkedBinding('模块名称');

内建原生模块的章节中我们知道,在加载执行internal\bootstrap\loaders.js的时候。为 process对象添加了_linkedBinding()函数。

const bindingObj = ObjectCreate(null); 
process._linkedBinding = function _linkedBinding(module) {
    module = String(module);
    let mod = bindingObj[module];
    if (typeof mod !== 'object')
      mod = bindingObj[module] = getLinkedBinding(module);
    return mod;
 };

_linkedBinding() 函数 通过一个javaScript对象bindingObj 判断是否洋浦缓存,如果没有再调用 c++层传递下来的函数getLinkedBinding() 去获取链接模块。调用getLinkedBinding()函数实际上是调用了c++层的node::GetLinkedBinding()函数。
image.png
node::GetLinkedBinding的具体实现。

void GetLinkedBinding(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);

  Local<String> module_name = args[0].As<String>();

  node::Utf8Value module_name_v(env->isolate(), module_name);
  const char* name = *module_name_v;
  node_module* mod = nullptr;

  Environment* cur_env = env;
  while (mod == nullptr && cur_env != nullptr) {
    Mutex::ScopedLock lock(cur_env->extra_linked_bindings_mutex());
    mod = FindModule(cur_env->extra_linked_bindings_head(), name, NM_F_LINKED);
    cur_env = cur_env->worker_parent_env();
  }

  if (mod == nullptr)
    mod = FindModule(modlist_linked, name, NM_F_LINKED);

  if (mod == nullptr) {
    char errmsg[1024];
    snprintf(errmsg,
             sizeof(errmsg),
             "No such module was linked: %s",
             *module_name_v);
    return THROW_ERR_INVALID_MODULE(env, errmsg);
  }

  // 创建module对象
  Local<Object> module = Object::New(env->isolate());
  // 创建exports 对象  
  Local<Object> exports = Object::New(env->isolate());
  Local<String> exports_prop =  String::NewFromUtf8Literal(env->isolate(), "exports");
  // 设置module的exports属性为 exports对象  
  module->Set(env->context(), exports_prop, exports).Check();

  // 在上面可以知道 node_module 结构体有两个 注册函数的属性。nm_context_register_func 和 nm_register_func
  // 判断链接模块使用了那种注册函数。两种注册函数的区别在于是否传递v8执行上下文  
  if (mod->nm_context_register_func != nullptr) {
    mod->nm_context_register_func(
        exports, module, env->context(), mod->nm_priv);
  } else if (mod->nm_register_func != nullptr) {
    mod->nm_register_func(exports, module, mod->nm_priv);
  } else {
    return THROW_ERR_INVALID_MODULE(
        env,
        "Linked moduled has no declared entry point.");
  }
  // 获取模块的导出  
  auto effective_exports = module->Get(env->context(), exports_prop).ToLocalChecked();
  args.GetReturnValue().Set(effective_exports);
}

链接模块在什么时候创建的呢?
node.h 文件中有一个 NODE_MODULE_LINKED() 宏。这正是链接模块注册的函数。

#define NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, priv, flags)    \
  extern "C" {                                                        \
    static node::node_module _module =                                \
    {                                                                 \
      NODE_MODULE_VERSION,                                            \
      flags,                                                          \
      NULL,  /* NOLINT (readability/null_usage) */                    \
      __FILE__,                                                       \
      NULL,  /* NOLINT (readability/null_usage) */                    \
      (node::addon_context_register_func) (regfunc),                  \
      NODE_STRINGIFY(modname),                                        \
      priv,                                                           \
      NULL  /* NOLINT (readability/null_usage) */                     \
    };                                                                \
    NODE_C_CTOR(_register_ ## modname) {                              \
      node_module_register(&_module);                                 \
    }                                                                 \
  }  
#define NODE_MODULE_LINKED(modname, regfunc)                               \
  /* NOLINTNEXTLINE (readability/null_usage) */                            \
  NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL,                      \
                              node::ModuleFlags::kLinked)

因为链接模块是为了嵌入式node程序中使用,所以在node中没有例子可供讲解。在test/cctest目录下有一个测试文件test_linked_binding.cc 。我们直接使用测试例子看一下链接模块的的创建注册使用的整个流程。

void InitializeBinding(v8::Local<v8::Object> exports,
                       v8::Local<v8::Value> module,
                       v8::Local<v8::Context> context,
                       void* priv) {
  v8::Isolate* isolate = context->GetIsolate();
  exports->Set(
      context,
      v8::String::NewFromOneByte(isolate,
                                 reinterpret_cast<const uint8_t*>("key"))
                                 .ToLocalChecked(),
      v8::String::NewFromOneByte(isolate,
                                 reinterpret_cast<const uint8_t*>("value"))
                                 .ToLocalChecked())
      .FromJust();
}

NODE_MODULE_LINKED(cctest_linkedbinding, InitializeBinding)

和内建模块类似。在文件模块中使用宏去定义一个模块。同时提供了模块名称和初始化回调函数。进行NODE_MODULE_LINKED() 宏展开后。

 extern "C" {                                                        
    static node::node_module _module =                                
    {                                                                 
      NODE_MODULE_VERSION,                                            
      node::ModuleFlags::kLinked,                                                          
      NULL,               
      __FILE__,                                                       
      NULL,               
      (node::addon_context_register_func) (InitializeBinding),                  
      cctest_linkedbinding,                                    
      NULL,                                                           
      NULL                
    }; 
    #pragma section(".CRT$XCU", read)
    // 声明函数
    static void __cdecl _register_ctest_linkedbinding(void);
    // dll的导出 
     __declspec(dllexport, allocate(".CRT$XCU"))        
    void (__cdecl *ctest_linkedbinding _)(void) = _register_ctest_linkedbinding;
    // 定义函数
    static void __cdecl _register_ctest_linkedbinding(void) {
         node_module_register(&_module);      
     }                           
    }                                                                 
  }

在此可以知道链接模块把注册的函数作为动态链接库的导出函数。那在什么时候注册链接模块呢?在说出这个问题之前我们先去了解一下其他的知识。

windows crt

在C++的世界⾥,⼊⼝函数还肩负着另⼀个艰巨的使命,那就是在 main的前后完成全局变量的构造与析构。在不同的编译器实现不一样。在这里我们先讨论msvc下的。

#include <iostream>

int foo(void){
    return 1;
}

int result = foo();

int main(){
    std::cout << result << std::endl;
    return 0;
}

按照我们对c/c++程序认知,在c/c++程序中,程序是从main()函数开始执行。全局基本类型变量result初始化为0。结果应该输出为0。但这里的结果输出为1。按照c++标准。在执行 main() 前必须调用 foo()函数。但在哪里调用呢?
在msvc下,默认情况下,链接器包括 CRT (c running time)库,提供其自己的启动代码。 启动代码初始化 CRT 库,调用全局初始值设定项,然后调用用于控制台应用程序的用户提供的 main() 函数。
CRT 从 Microsoft c + + 编译器中获取函数指针的列表。 当编译器发现全局初始值设定项时,它将在 .CRT$XCU 部分(其中 CRT 是部分名称, XCU 是组名称)中生成一个动态初始值设定项。所以在下面的例子中是不合法的。

#include <iostream>
int result = 0;
void foo(void){
    return result++;
}
foo();
int main(){
    std::cout << result << std::endl;
    return 0;
}

CRT除了在主程序中对全局变量进行初始化。也可以对链接库进行链接Microsoft C 运行时库文件,以及其关联的编译器选项和预处理器指令,使用链接的 CRT 意味着由 C 运行时库保存的任何状态信息对于 CRT 的该实例而言是本地的。

#pragma section( "section-name" [, attributes] )
#pragma section(".CRT$XCU", read)

#pragma 是编译指令。编译器在编译文件时的一些元信息的获取。例如上面指令代表着生成的obj文件里创建名为section-name的段,并具有attributes属性。
.CRT$XCU 代表的是什么?当编译的时候,每一个编译单元都会生成名为.CRT$XCU(U是User的意思)的段,在这个段中编译单元会加入自身的全局初始化函数。当链接的时候,链接器会将所有相同属性的段合并并以数组的方式存储。并程序执行main函数前。通过内置的函数遍历执行。
#pragma section只是去定义了一个段。使用的时候好需要__declspec关键词去使用。
__declspec是msvc扩展的c/c++关键词,__declspec在用户定义类型声明的开头指定的属性适用于该类型的变量。

// 用于导入dll的变量
__declspec( dllimport ) int result;
// 用于导出dll的函数
__declspec( dllexport ) void foo();

// 该allocate声明说明名称,其中的数据项目将被分配一个数据段。
#pragma section("mycode", read)
__declspec(allocate("mycode"))  int i = 0;

看一下例子。在数据段.CRT$XCU定义了函数initialize() ,在程序运行的全局构造函数中运行了initialize()函数。


#include <iostream>
int result = 0;

#pragma section(".CRT$XCU",read)
#define INIT(fun) \
        static void fun(void); \
        __declspec(allocate(".CRT$XCU")) void (*fun##_)(void) = fun; \
        static void fun(void)\

INIT(initialize){
    result++;
}

int main(int argc, char** argv)
{   
    // 输出1
    std::cout << result << std::endl;
    return 0;
}

和上面例子唯一不同的是在__declspec关键词的参数列表中使用了dllexport对链接库进行导出。这两种的区别在于。下面例子中会生成静态库的形式(.lib文件)。

#include <iostream>


int result = 0;


#pragma section(".CRT$XCU",read)
#define INIT(fun) \
        static void fun(void); \
        __declspec(dllexport, allocate(".CRT$XCU")) void (*fun##_)(void) = fun; \
        static void fun(void)\


INIT(initialize){
    result++;
}

int main(int argc, char** argv)
{
    std::cout << result << std::endl;
    return 0;
}

在上面例子中我们看到。在mian函数中并没有执行initialize函数。在打印的时候为1。这是因为该函数在执行main函数之前主动执行了。链接模块就是这个原理。使用了NODE_MODULE_LINKED() 去定义链接模块的时候,模块在编译的时候被编译成一个静态库。在node启动执行main函数前,会先执行静态库的导出函数。导出函数被放在全局构造函数中默认执行。在导出函数中调用了node_module_register(&_module);进行了模块的注册。在内建模块中我们知道, 内建模块在node启动的时候也是调用node_module_register() 函数进行内建模块的注册。和内建模块不同的是,注册的时机不同,链接模块是在全局构造函数中去执行注册。即还没有进入node的main 函数前就已经执行注册。


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) { // 如果node还没有初始化,不管该模块是那种类型,直接把它当链接模块看待。
    mp->nm_flags = NM_F_LINKED;
    mp->nm_link = modlist_linked;
    modlist_linked = mp;
  } else {
    thread_local_modpending = mp;
  }
}

在上面的代码我们知道。在执行node_module_register 会首先判断是否是内建模块。如果不是,判断node是否已经加载 。如果没有加载,直接把当前执行注册的模块当作是链接模块。链接模块的注册流程一样。使用链表存储了模块的指针。最后通过全局变量modlist_linked存储了最后一个链接模块的模块指针。node_is_initialized 是一个全局变量。初始化值为false。当执行node的初始化后即InitializeNodeWithArg函数执行完后,重新设置为true。

// 内容有所省略
int InitializeNodeWithArgs(std::vector<std::string>* argv,
                           std::vector<std::string>* exec_argv,
                           std::vector<std::string>* errors) {

  // 注册c++内建模块
  // Register built-in modules
  binding::RegisterBuiltinModules();

  // 处理全局参数。
  const int exit_code = ProcessGlobalArgs(&env_argv,
                                            nullptr,
                                            errors,
                                            kAllowedInEnvironment);
  if (exit_code != 0) return exit_code;
  // 初始化原生模块,例如fs path等模块
  NativeModuleEnv::InitializeCodeCache();
  // 全局标记
  node_is_initialized = true;
  return 0;
}

创建一个链接模块和内建模块类似。不同的是使用的注册宏不一样。综合上面的流程可以得出链接模块得加载流程。流程图地址
node链接模块的加载流程 (2).png