Node.jsのfsモジュールを少し深く読んでみた
はじめに
プロダクションでNode.jsを使っている時、OSネイティブの部分で何をしているか理解しないといけないことが多々あり、その都度Node.jsのソースコードを読んでいます。
Node.jsはいかんせんソフトウェアとして大きく、読むのがなかなか大変です。
そこで、自分なりのNode.jsの中身の読み方を、備忘の意味も込めて残しておきます。
以下は例としてWindowsにおける fs.access
の処理を最後まで追いかけてみます。
ファイル構成の簡単な説明
Node.jsは上部がJavaScript、下部がC++で書かれています。
ディレクトリで主に見るべきなのは以下の3つです。
lib
: requireで読み込むJavaScriptモジュールsrc
: C++で書かれた低レイヤーの実装deps
:v8
やuv
などの依存ライブラリ
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
AsyncCall
は AsyncDestCall
を呼びだします。
引数 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.c
や pipe.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 APIの GetFileAttributesW
関数を呼び出し、その結果を返しています。
まとめ
というわけで、fs.access
の呼び出しは、 node_file.cc
の各種関数やマクロを経由した上で、 libuv
の fs__access
に行き着きました。
Node.js上でソースコードを書く分には fs.access
を呼び出すだけですが、その中で何をやっているのかを追いかけていくと非常に奥深く、いろんな抽象化やテクニックが使われていることが分かります。
ちょっと長すぎない? という気もしますが...
もしNode.jsの中身を把握しないといけなくなったというような人がいたら、参考になれば幸いです。
余談
Node.jsの作者 Ryan Dahl氏は、Node.jsのダメだったところを反省して新しいJavaScriptランタイム Deno
を絶賛開発中らしいです。
こちらはどうなるのでしょうね。
thenewstack.io