jsernews 1.0.1

<~>

ts 132 days ago.

原文链接:https://blog.safia.rocks/post/169888553343/node-module-deep-dive-os

所以,我已经阅读了 Node 代码库了一段时间了,并且我开始“很好地”理解 Node 主进程如何工作,不同模块如何工作以及 C++ 和 JavaScript 部分之间的交互。Node 代码库非常复杂,我一直在尽我所能去熟悉它。

我觉得通过检查其中一个内置模块 os 来弄清楚整个模块生命周期,以及是如何在 Node 中工作应该是很有趣的。

如果你还不熟悉 os,它是一个 Node 模块,它允许开发人员获取系统架构或主机名。以下是我在个人计算机上运行时得到的输出示例。

> const os = require('os');
undefined
> os.arch();
'x64'
> os.homedir();
'/Users/captainsafia'
> os.hostname();
'eniac'

你可以在这里找到 os 模块的 JavaScript 源代码。我目前并不特别感兴趣阅读 JavaScript 代码中的任何函数。我感兴趣的一件事是 process.binding,它绑定到代码的 C++ 部分中创建的模块对象。

const {
  getCPUs,
  getFreeMem,
  getHomeDirectory: _getHomeDirectory,
  getHostname: _getHostname,
  getInterfaceAddresses: _getInterfaceAddresses,
  getLoadAvg,
  getOSRelease: _getOSRelease,
  getOSType: _getOSType,
  getTotalMem,
  getUserInfo: _getUserInfo,
  getUptime,
  isBigEndian
} = process.binding('os');

由此,我们可以看到该对象包含很多函数,稍后将在 os 模块的公共 API 的某些部分中调用这些函数。我想要做的第一件事就是试着弄清楚上面列出的函数是在哪里定义的。我在代码库中进行了一些搜索,在这里发现了 os 函数的 C++ 定义。

例如,这里是上面提到的 GetOSType / getOSType 函数的定义。

static void GetOSType(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  const char* rval;

#ifdef __POSIX__
  struct utsname info;
  if (uname(&info) < 0) {
    CHECK_GE(args.Length(), 1);
    env->CollectExceptionInfo(args[args.Length() - 1], errno, "uname");
    return args.GetReturnValue().SetUndefined();
  }
  rval = info.sysname;
#else  // __MINGW32__
  rval = "Windows_NT";
#endif  // __POSIX__

  args.GetReturnValue().Set(OneByteString(env->isolate(), rval));
}

基本上,这个函数使用 Unix 中的 uname 函数来获取有关操作系统的信息并从中提取操作系统类型。它还有一些条件逻辑来评估它是在 Unix 系统还是在 Windows 系统上运行。

args.GetReturnValue().Set(OneByteString(env->isolate(), rval));

如上所示,return 语句不是您可能在 C++ 代码中看到的传统 return。我从一本之前浏览过的关于如何开发 Native Node 扩展的书中回忆起,这个特殊的返回代码允许代码库的 JavaScript 部分访问来自 C++ 模块的返回数据。代码中最有趣的部分实际上一直处于最底层。

void Initialize(Local<Object> target,
                Local<Value> unused,
                Local<Context> context) {
  Environment* env = Environment::GetCurrent(context);
  env->SetMethod(target, "getHostname", 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()));
}

}  // namespace os
}  // namespace node

NODE_BUILTIN_MODULE_CONTEXT_AWARE(os, node::os::Initialize)

在这里,看起来我们正在初始化我们在 C++ 级别编写的函数与我们(在绑定到的 os 模块)将引用它们的名称之间的映射。我很好奇的一件事是在 NODE_BUILTIN_MODULE_CONTEXT_AWARE 宏函数中发生了什么。它看起来像我们传给它的是 os 命名空间,它包含在这个 C++ 文件中定义的所有函数和初始化函数。我在代码库中进行了一些搜索,并在这里找到了 NODE_BUILTIN_MODULE_CONTEXT_AWARE 的代码。

#define NODE_BUILTIN_MODULE_CONTEXT_AWARE(modname, regfunc)                   \
  NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, nullptr, NM_F_BUILTIN)

因此,它看起来像这个宏函数,调用 NODE_MODULE_CONTEXT_AWARE_CPP 宏函数,该函数在同一个文件中并具有以下定义。

#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags)          \
  static node::node_module _module = {                                        \
    NODE_MODULE_VERSION,                                                      \
    flags,                                                                    \
    nullptr,                                                                  \
    __FILE__,                                                                 \
    nullptr,                                                                  \
    (node::addon_context_register_func) (regfunc),                            \
    NODE_STRINGIFY(modname),                                                  \
    priv,                                                                     \
    nullptr                                                                   \
  };                                                                          \
  void _register_ ## modname() {                                              \
    node_module_register(&_module);                                           \
  }

哦!这个宏函数正在创建我在[上一篇博文]中发现的 node_module 结构体。事实证明,注册模块时调用的注册函数实际上是上面的 Initialize 函数,它将函数关联加载到当前环境中。这一切都开始变得合理!

所以下面是我能理解的内置模块生命周期的故事。

  • 一系列函数在代码的 C++ 部分的特定模块的名称空间下定义。例如,GetOSType 函数在 os 命名空间下定义。
  • 该模块使用一系列宏函数注册到当前进程。这种注册涉及到将上面定义的函数映射到一个名称集合,当我们提取绑定时可以引用它们。
  • 与特定模块关联的 JavaScript 代码使用 process.binding 从正在运行的进程中提取函数的注册名称。
  • 使用 process.binding 提取的函数在作为 JavaScript 模块中的公共 API 的一部分导出的函数中调用。例如,os.type() JavaScript 函数调用最终依赖于引用了 C++ 中定义的 GetOSType 函数的 getOSType 绑定。

到目前为止,这对我来说是合情合理的。肯定有一些我不明白的地方。主要是我很想知道这个 process 对象的作用范围。对我来说,理解原生 C++ 和 JavaScript 代码之间的关系的关键在于对于 process 是什么进行深入的了解。也许我会在另一段时间深入研究...