Engineer's Way

主にソフトウェア関連について色々書くブログです。

Node.jsのfsモジュールを少し深く読んでみた

  f:id:matsnow:20180122012948p:plain

はじめに

プロダクションでNode.jsを使っている時、OSネイティブの部分で何をしているか理解しないといけないことが多々あり、その都度Node.jsのソースコードを読んでいます。
Node.jsはいかんせんソフトウェアとして大きく、読むのがなかなか大変です。
そこで、自分なりのNode.jsの中身の読み方を、備忘の意味も込めて残しておきます。
以下は例としてWindowsにおける fs.access の処理を最後まで追いかけてみます。

ファイル構成の簡単な説明

Node.jsは上部がJavaScript、下部がC++で書かれています。
ディレクトリで主に見るべきなのは以下の3つです。

  • lib : requireで読み込むJavaScriptモジュール
  • srcC++で書かれた低レイヤーの実装
  • depsv8uv などの依存ライブラリ

JavaScript部分

JavaScript部分はシンプルで、1つのソースファイル、1関数しかありません。

fs.access

JavaScript側の fs.access のコードは以下の通りです。

function access(path, mode, callback) {
  if (typeof mode === 'function') {
    callback = mode;
    mode = F_OK;
  }

  path = getPathFromURL(path);
  validatePath(path);

  mode = mode | 0;
  const req = new FSReqWrap();
  req.oncomplete = makeCallback(callback);
  binding.access(pathModule.toNamespacedPath(path), mode, req);
}

https://github.com/nodejs/node/blob/v10.5.0/lib/fs.js#L171-L184

コードは非常にシンプルです。
binding.access から先はC++の領域に入っていきます。

C++部分

C++部分はマクロも使われており、割とあちらこちらを行き来しないと把握することができません。

クロスプラットフォーム部分

NODE_BUILTIN_MODULE_CONTEXT_AWARE

まず前提として、NODE_BUILTIN_MODULE_CONTEXT_AWARE マクロによって、node::fs::Initialize関数の実行結果が JavaScriptのfsモジュールとして登録されています。

NODE_BUILTIN_MODULE_CONTEXT_AWARE(fs, node::fs::Initialize)

https://github.com/nodejs/node/blob/v10.5.0/src/node_file.cc#L1996

マクロの内容は src/node_internals.h に定義されています。

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

https://github.com/nodejs/node/blob/v10.5.0/src/node_internals.h#L165-L166

NODE_BUILTIN_MODULE_CONTEXT_AWARE はさらに 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);                                           \
  }

https://github.com/nodejs/node/blob/v10.5.0/src/node_internals.h#L148-L162

node_module_register(&_module); で登録がおこなれています。

node::fs::Initialize

fsモジュールを生成するための node::fs::Initialize では JavaScript側のメソッド名 と C++の関数が SetMethod で紐づけられています。

void Initialize(Local<Object> target,
                Local<Value> unused,
                Local<Context> context,
                void* priv) {
  Environment* env = Environment::GetCurrent(context);

  env->SetMethod(target, "access", Access);
 :
 (中略)

https://github.com/nodejs/node/blob/v10.5.0/src/node_file.cc#L1868

今回は fs.access を追いかけたいので、次は Access を読みます。

node::fs::Access

node::fs::Access は引数を解析して、型チェックなどを行います。
その後、非同期呼び出し(access)なら AsyncCall 、同期呼び出し(accessSync)なら SyncCall を実行します。
この時、uv_fs_access という識別子を AsyncCall、もしくは SyncCall に引数渡ししています。

void Access(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args.GetIsolate());
  HandleScope scope(env->isolate());

  const int argc = args.Length();
  CHECK_GE(argc, 2);

  CHECK(args[1]->IsInt32());
  int mode = args[1].As<Int32>()->Value();

  BufferValue path(env->isolate(), args[0]);
  CHECK_NOT_NULL(*path);

  FSReqBase* req_wrap_async = GetReqWrap(env, args[2]);
  if (req_wrap_async != nullptr) {  // access(path, mode, req)

    // 非同期呼び出しの場合
    AsyncCall(env, req_wrap_async, args, "access", UTF8, AfterNoArgs,
              uv_fs_access, *path, mode);
  } else {  // access(path, mode, undefined, ctx)
    CHECK_EQ(argc, 4);
    FSReqWrapSync req_wrap_sync;
    FS_SYNC_TRACE_BEGIN(access);

    // 同期呼び出しの場合
    SyncCall(env, args[3], &req_wrap_sync, "access", uv_fs_access, *path, mode);
    FS_SYNC_TRACE_END(access);
  }
}

https://github.com/nodejs/node/blob/v10.5.0/src/node_file.cc#L688-L712

node::fs::AsyncCall

AsyncCallAsyncDestCall を呼びだします。
引数 Func fn には先ほどの uv_fs_access が入っています。

template <typename Func, typename... Args>
inline FSReqBase* AsyncCall(Environment* env,
    FSReqBase* req_wrap,
    const FunctionCallbackInfo<Value>& args,
    const char* syscall, enum encoding enc,
    uv_fs_cb after, Func fn, Args... fn_args) {
  return AsyncDestCall(env, req_wrap, args,
                       syscall, nullptr, 0, enc,
                       after, fn, fn_args...);
}

https://github.com/nodejs/node/blob/v10.5.0/src/node_file.cc#L640-L649

node::fs::AsyncDestCall

AsyncDestCall の中で Func fn が実行されます。 今回の場合、uv_fs_access が実行されることになります。
この uv_ で始まる関数はlibuvの関数を表しており、プラットフォーム(Windows / Linux / Mac)によって異なるファイルに関数が定義されています。

template <typename Func, typename... Args>
inline FSReqBase* AsyncDestCall(Environment* env,
    FSReqBase* req_wrap,
    const FunctionCallbackInfo<Value>& args,
    const char* syscall, const char* dest, size_t len,
    enum encoding enc, uv_fs_cb after, Func fn, Args... fn_args) {
  CHECK_NOT_NULL(req_wrap);
  req_wrap->Init(syscall, dest, len, enc);

  // ここでAcyncCallから渡ってきた関数を実行
  int err = req_wrap->Dispatch(fn, fn_args..., after); 
  if (err < 0) {
    uv_fs_t* uv_req = req_wrap->req();
    uv_req->result = err;
    uv_req->path = nullptr;
    after(uv_req);  // after may delete req_wrap if there is an error
    req_wrap = nullptr;
  } else {
    req_wrap->SetReturnValue(args);
  }

  return req_wrap;
}

https://github.com/nodejs/node/blob/v10.5.0/src/node_file.cc#L617-L637

libuvの関数が使われているため、次は deps/uvの下を見に行くことになります。

Windows固有部分

ソースファイルは deps/uv/src/win/fs.c です。
deps/uv/src/win/ の中には、 fs.c 以外にも signal.cpipe.c などが格納されていますので、どのように抽象化されているか興味があるなら見てみると良いと思います。

uv_fs_access

Windowsでの uv_fs_access では、まず INIT マクロで初期化が行われています。 その後、pathを取得する処理を挟んで POST マクロが実行されます。

int uv_fs_access(uv_loop_t* loop,
                 uv_fs_t* req,
                 const char* path,
                 int flags,
                 uv_fs_cb cb) {
  int err;

  INIT(UV_FS_ACCESS);
  err = fs__capture_path(req, path, NULL, cb != NULL);
  if (err)
    return uv_translate_sys_error(err);

  req->fs.info.mode = flags;
  POST;
}

https://github.com/nodejs/node/blob/v10.5.0/deps/uv/src/win/fs.c#L2372-L2386

POST

POST マクロの中では、コールバック関数の有無によって、イベントループに登録するか即実行するかが決まります。 どちらの場合でも、static関数 uv__fs_work で最終的に実行される関数名が解決されます。

#define POST                                                                  \
  do {                                                                        \
    if (cb != NULL) {                                                         \
      uv__req_register(loop, req);                                            \
      uv__work_submit(loop, &req->work_req, uv__fs_work, uv__fs_done);        \
      return 0;                                                               \
    } else {                                                                  \
      uv__fs_work(&req->work_req);                                            \
      return req->result;                                                     \
    }                                                                         \
  }                                                                           \
  while (0)

https://github.com/nodejs/node/blob/v10.5.0/deps/uv/src/win/fs.c#L54-L65

ちなみに、do {} while (0) というのはマクロを書くときのテクニックのようです。
Node.jsのコードを読んで初めて知りました。

do {} while(0)
do {} while (0); の意味と目的【do while false イディオムの利点】 | MaryCore

uv__fs_work

uv__fs_work では、引数を元に XX マクロで関数名を解決した後、switch-case で呼び出したい関数にディスパッチしています。

static void uv__fs_work(struct uv__work* w) {
  uv_fs_t* req;

  req = container_of(w, uv_fs_t, work_req);
  assert(req->type == UV_FS);

#define XX(uc, lc)  case UV_FS_##uc: fs__##lc(req); break;
  switch (req->fs_type) {
    XX(OPEN, open)
  :
  (中略)
  :
    XX(ACCESS, access)

https://github.com/nodejs/node/blob/v10.5.0/deps/uv/src/win/fs.c#L1943-L1983

XX マクロを抜き出すと、以下のように書かれています。

#define XX(uc, lc)  case UV_FS_##uc: fs__##lc(req); break;

ぱっと見では分かりづらいですが、例えば XX(ACCESS, access)
case UV_FS_ACCESS: fs__access(req); break; となります。

よって、上のswitchは

switch(req->fs_type) {
:
case UV_FS_ACCESS: fs__access(req); break;

と展開され、fs__access 関数が呼び出されます。

fs_access

と、ここまで非常に長かったですが、最終的にWindows環境で実行されるコードが、この fs__access となります。

static void fs__access(uv_fs_t* req) {
  DWORD attr = GetFileAttributesW(req->file.pathw);

  if (attr == INVALID_FILE_ATTRIBUTES) {
    SET_REQ_WIN32_ERROR(req, GetLastError());
    return;
  }

  /*
   * Access is possible if
   * - write access wasn't requested,
   * - or the file isn't read-only,
   * - or it's a directory.
   * (Directories cannot be read-only on Windows.)
   */
  if (!(req->fs.info.mode & W_OK) ||
      !(attr & FILE_ATTRIBUTE_READONLY) ||
      (attr & FILE_ATTRIBUTE_DIRECTORY)) {
    SET_REQ_RESULT(req, 0);
  } else {
    SET_REQ_WIN32_ERROR(req, UV_EPERM);
  }
}

https://github.com/nodejs/node/blob/v10.5.0/deps/uv/src/win/fs.c#L1459-L1482

Windows APIGetFileAttributesW 関数を呼び出し、その結果を返しています。

まとめ

というわけで、fs.access の呼び出しは、 node_file.cc の各種関数やマクロを経由した上で、 libuvfs__access に行き着きました。 Node.js上でソースコードを書く分には fs.access を呼び出すだけですが、その中で何をやっているのかを追いかけていくと非常に奥深く、いろんな抽象化やテクニックが使われていることが分かります。
ちょっと長すぎない? という気もしますが...
もしNode.jsの中身を把握しないといけなくなったというような人がいたら、参考になれば幸いです。

余談

Node.jsの作者 Ryan Dahl氏は、Node.jsのダメだったところを反省して新しいJavaScriptランタイム Deno を絶賛開発中らしいです。
こちらはどうなるのでしょうね。 thenewstack.io

内部リンク

matsnow.hatenablog.com matsnow.hatenablog.com