
はじめに
プロダクションで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部分はシンプルで、1つのソースファイル、1関数しかありません。
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++部分はマクロも使われており、割とあちらこちらを行き来しないと把握することができません。
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 は引数を解析して、型チェックなどを行います。
その後、非同期呼び出し(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) {
AsyncCall(env, req_wrap_async, args, "access", UTF8, AfterNoArgs,
uv_fs_access, *path, mode);
} else {
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);
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);
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の下を見に行くことになります。
ソースファイルは deps/uv/src/win/fs.c です。
deps/uv/src/win/ の中には、 fs.c 以外にも signal.c や pipe.c などが格納されていますので、どのように抽象化されているか興味があるなら見てみると良いと思います。
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 関数が呼び出されます。
と、ここまで非常に長かったですが、最終的に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;
}
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
内部リンク
matsnow.hatenablog.com
matsnow.hatenablog.com