jsernews 1.0.1

<~>

ts 136 days ago.

原文链接:https://blog.safia.rocks/post/169813535760/how-does-node-load-built-in-modules

在我发布这篇博文后,我从 Node 的一位维护人员那里得到了一些反馈。在下面的文章中,我和代码中称为“内置模块”的内容实际上是绑定,它们是由 C++ 创建的表示模块的 JavaScript 对象。看起来由于一些遗留原因,它们在实际代码中被称为“内置模块”。实际上,这篇博文的标题应该是“Node 如何注册模块绑定?我不能修改标题而不会搞乱很多超链接,所以我只是在这里留下这个附录。好,享受文章!

所以,在我写的最后一篇博文中,我开始关注 Node 主进程是如何初始化的。我很快发现那里发生了很多事情(并且理所当然!)。引起我注意的一个特别之处是引用了一个函数,它似乎在 Node 主进程的初始化阶段加载了内置模块。

node::RegisterBuiltinModules();

我想更深入地研究这一点,所以我开始在代码库中窥探以了解更多信息。

我已经改变了我在这些博客文章中拼写“code base”的方式。这主要是因为我不知道它应该拼写为“codebase”还是“code base”。我做了一些窥探,在 StackExchange 发现了一个有趣的讨论,似乎表示“codebase”是拼写它的不太模糊的方式,尽管“code base”同样有效。

我最终在这里发现了 node::RegisterBuiltinModule 函数定义。

void RegisterBuiltinModules() {
#define V(modname) _register_##modname();
  NODE_BUILTIN_MODULES(V)
#undef V
}

这段特定的代码利用一些特殊的 C++ 语法来定义宏。基本上,上面代码中的第二行是说每次出现字符串 V(modname) 时,都应该用字符串 _register_##modname() 替换。根据我的理解,这基本上是创建了某种缩写,这样可以重复调用 _register_modname,而不必键入整个函数名称。

接下来我必须弄清楚 NODE_BUILTIN_MODULES 究竟在做什么。我在 Node 代码库的另一个源文件中找到了此函数调用的定义。

#define NODE_BUILTIN_MODULES(V)                                               \
  NODE_BUILTIN_STANDARD_MODULES(V)                                            \
  NODE_BUILTIN_OPENSSL_MODULES(V)                                             \
  NODE_BUILTIN_ICU_MODULES(V)

所以它看起来像 NODE_BUILTIN_MODULES 只是一个轻量包装函数,它调用了一些其他函数(我假设)加载了不同的模块。上面最有趣的一行显然是 NODE_BUILTIN_STANDARD_MODULESNODE_BUILTIN_STANDARD_MODULES 是另一个函数宏,负责加载 Node 中的一些内置模块。如果您查看源文件,您将看到对 fsosbuffer 模块的引用。

此刻,我有点困惑。所有这些漂浮在周围的函数宏,似乎正在加载内置模块,但它们究竟如何连接到正在运行的系统。怎样才能在 Node 中运行 require('fs'),让一切都很方便。我注意到了 NODE_BUILTIN_STANDARD_MODULES 宏定义之上的注释。

// A list of built-in modules. In order to do module registration
// in node::Init(), need to add built-in modules in the following list.
// Then in node::RegisterBuiltinModules(), it calls modules' registration
// function. This helps the built-in modules are loaded properly when
// node is built as static library. No need to depends on the
// __attribute__((constructor)) like mechanism in GCC.

所以这个注释真的很有用。看起来,这些宏实际上是注册阶段发生的事情的前身。那么注册阶段究竟是什么样子?我更多地探索了代码,发现了前面指出的 _register_##modname 函数的引用

void _register_ ## modname() {                                              \
  node_module_register(&_module);                                           \
}

所以它看起来像 _register_ ##modname 基本上只是传递一个需要注册的模块引用,调用 node_module_register。那么 node_module_register 函数中发生了什么?需要更多窥探!

我最终在这里找到了 node_module_register 的函数定义。它看起来像这样。

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;
  } else if (mp->nm_flags & NM_F_INTERNAL) {
    mp->nm_link = modlist_internal;
    modlist_internal = mp;
  } else if (!node_is_initialized) {
    // "Linked" modules are included as part of the node project.
    // Like builtins they are registered *before* node::Init runs.
    mp->nm_flags = NM_F_LINKED;
    mp->nm_link = modlist_linked;
    modlist_linked = mp;
  } else {
    modpending = mp;
  }
}

好家伙!这里有很多事情要做。如果你看过很多 C/C++ 代码,这个函数的第一行是非常标准的。由于 C++ 是类型化语言,因此我们基本上将在参数中传递的非类型化指针 m 转换为特定类型的指向模块的指针 mp

该模块引用 node_module 结构。我决定查看代码库以查看哪些属性存储在 node_module 结构中。我想这会给我一个存储在结构中的不同属性的意义。

如果您不熟悉 C/C++ 但熟悉 JavaScript,则可以将结构视为对象。它基本上存储了标签与其值之间的关联。

我在这里找到了 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;
};

一些字段,如 nm_versionnm_filename,是自解释的,但其他字段不是。什么是 nm_privnm_dso_handle?我只有问题,却没有答案。对应于我目前的探索,看起来与模块相关联的模块注册功能存储在结构本身中。

我觉得我应会回头看看 register_module_name 函数,看看我是否能够辨别结构中的其他字段是否对它有用。我在函数中注意到的一种模式是看起来像这样的代码片段。

mp->nm_link = modlist_builtin;
modlist_builtin = mp;

它看起来像我们将结构中的字段设置为特定值(modlist_builtin),然后将该值设置为我们刚刚修改的 Node 模块结构。通过查看上面结构体中的类型定义,我知道 nm_link 也是一个 node_module 结构体,因此看起来我们在这里创建了一个模块引用链。我试图验证我的假设,方法是查看代码中使用 modlist_builtin 的位置,然后找到此代码段

node_module* get_builtin_module(const char* name) {
  return FindModule(modlist_builtin, name, NM_F_BUILTIN);
}

这绝对证实了我怀疑我们正在创建的 node_modules 结构。然后,当我们想要获取一个特定模块时,我们通过这个链结构进行搜索以返回它。

我仍然想知道什么时候这些模块的实际注册会发生。我试图通过在代码库中搜索字符串“nm_register_func”来解决这个问题。这个函数名称是在上面的 node_module 结构中存储对模块注册函数的引用。我发现了这个函数名称的一些调用,但最明显的是在 Node 库的 DLOpen 函数中。dlopen 是一个函数的通用术语,它负责使可执行程序中的函数标识符可用于调用它们的程序。DLOpen中的“DL”代表动态加载,因为模块在程序开始时并未加载,而是在运行 require 时加载。

所以要把它概括起来,我的理解如下。

  • 当 Node 主进程启动时,它运行内置模块,并创建一个类似于链表的模块结构,存储在 modlist_builtin 中。
  • node_module 结构体包含有关如何注册特定模块的信息。
  • 当通过 require 加载模块时,会调用存储在 node_module 结构中的 nm_register_func

唷!这看起来很简单,一旦你回过头来回顾它。如果我误解了代码的任何部分,或者您对 Node 中的内置模块加载有一些澄清,请告诉我。