Read OSS

Node.js の内部構造:コードベース全体マップ

中級

前提知識

  • Node.js が何であるか、どのように使うかについての基本的な理解
  • C++ と JavaScript の基礎的な読み書き能力

Node.js の内部構造:コードベース全体マップ

Node.js は15年以上の歴史を持ち、コミット数は4万を超えるプロジェクトです。コードベースは2つの言語にまたがり、十数個のベンダリングされた依存関係を抱えています。さらに現代的なビルドツールが登場する以前から存在するビルドシステムも含まれています。ソースを読もうとして途方に暮れた経験があるなら、それはあなただけではありません。この記事では、特定のサブシステムへ踏み込む前に知っておくべき「地図」を提供します。

ディレクトリ構成を丁寧に見ていきながら、Node.js が C++ と JavaScript の二刀流を採用している理由を理解します。動作を支える依存関係を整理し、ビルドツールの仕組みを解き明かします。また特定の機能がどこに実装されているかを探すための実践的なガイドもお届けします。

トップレベルのディレクトリ構成

Node.js リポジトリには、一度把握してしまえばシンプルな構成原則があります。まず押さえておきたいのは次のディレクトリです。

ディレクトリ 役割 規模
src/ C++ コア — V8 エンベッダー、libuv バインディング、ネイティブモジュール 約273ファイル
lib/ JavaScript 標準ライブラリ — 公開 API と内部 API 公開モジュール約67個 + internal/
deps/ ベンダリングされたサードパーティ依存関係 V8、libuv、OpenSSL など
test/ テストスイート — 並列、逐次、C++ テスト 4,085件以上のテストファイル
tools/ ビルドツール、リンター、CI スクリプト js2c、GYP など
doc/ Markdown 形式の API ドキュメント モジュールごとのドキュメント
benchmark/ パフォーマンスベンチマーク サブシステムごとのベンチマーク
typings/ 内部 C++ バインディング向け TypeScript 型定義 内部実装への型安全性
graph TD
    ROOT["nodejs/node"]
    ROOT --> SRC["src/ — C++ core"]
    ROOT --> LIB["lib/ — JS standard library"]
    ROOT --> DEPS["deps/ — vendored dependencies"]
    ROOT --> TEST["test/ — test suites"]
    ROOT --> TOOLS["tools/ — build & CI"]
    ROOT --> DOC["doc/ — API docs"]
    
    SRC --> API_DIR["api/ — embedder API"]
    SRC --> PERM["permission/ — permission model"]
    SRC --> CRYPTO_DIR["crypto/ — OpenSSL bindings"]
    
    LIB --> PUB["fs.js, net.js, http.js..."]
    LIB --> INT["internal/ — private modules"]
    INT --> BOOT["bootstrap/ — startup scripts"]
    INT --> MAIN["main/ — entry points"]
    INT --> MOD["modules/ — CJS & ESM loaders"]

src/lib/ の分離こそが、最初に理解すべき最も重要な構造です。Node.js で使うほぼすべての機能は、この2つのディレクトリの両方にコードを持っています — 低レベルの処理は C++ が担い、ユーザー向けの API は JavaScript が担うという分担です。

デュアル言語アーキテクチャ

Node.js は本質的に、V8 JavaScript エンジンを組み込んだ C++ アプリケーションです。JavaScript がネイティブに行えないことはすべて C++ 層が担当します。ファイル I/O、ネットワークソケット、プロセス管理、暗号処理、そしてイベントループそのものも C++ の領域です。一方、JavaScript 層は開発者が実際に使う使いやすい API を提供します。

fs.readFile() を例に考えてみましょう。lib/fs.js の JavaScript は引数の検証、コールバックや Promise の処理、エンコーディングの管理を行います。しかし実際のファイル読み取りは src/node_file.cc で行われており、libuv の uv_fs_read を呼び出してシステムコールを実行します。

flowchart LR
    USER["User Code<br/>fs.readFile('file.txt')"] --> JS["lib/fs.js<br/>Argument validation,<br/>callback handling"]
    JS --> BIND["internalBinding('fs')"]
    BIND --> CPP["src/node_file.cc<br/>FSReqCallback,<br/>uv_fs_read"]
    CPP --> UV["libuv<br/>Platform I/O"]
    UV --> OS["Operating System"]

このアーキテクチャが採用されている理由は3つあります。第一に、JavaScript は API の表面を設計するうえで生産性が高く、エラーハンドリング、オプションのパース、ドキュメント整備がしやすいからです。第二に、OS の API を呼び出したりメモリを正確に管理したりするには C++ が必要だからです。第三に、この分離によってセキュリティ境界が明確になります。JavaScript がネイティブ機能にアクセスできる唯一の経路が internalBinding() ブリッジだということです。

ヒント: Node.js の API でバグを調査するときは、まず lib/ から JavaScript レベルの挙動を把握し、その後 internalBinding() の呼び出しを辿って src/ 内の対応する C++ 実装を探しましょう。

ベンダリングされた依存関係とその役割

Node.js はシステムのライブラリに頼らず、主要な依存関係を deps/ にまとめてベンダリングしています。これにより、プラットフォームをまたいで一貫した動作が保証され、ビルドプロセスも簡潔になります。どの依存関係をどのようにコンパイルするかは、node.gyp ビルドファイルで制御されています。

graph TD
    NODE["Node.js Binary"]
    NODE --> V8["V8 — JavaScript Engine<br/>JIT compilation, GC, ES spec"]
    NODE --> UV["libuv — Async I/O<br/>Event loop, file system,<br/>networking, threads"]
    NODE --> SSL["OpenSSL — Crypto/TLS<br/>Encryption, certificates,<br/>secure connections"]
    NODE --> HTTP["llhttp — HTTP Parser<br/>HTTP/1.1 request/response<br/>parsing"]
    NODE --> H2["nghttp2 — HTTP/2<br/>HTTP/2 framing and<br/>multiplexing"]
    NODE --> ICU["ICU — Internationalization<br/>Unicode, locales,<br/>date/number formatting"]
    NODE --> UNDI["undici — HTTP Client<br/>fetch(), WebSocket,<br/>HTTP client"]
依存関係 場所 役割
V8 deps/v8/ JavaScript エンジン — JIT コンパイル、ガベージコレクション、ES 仕様への準拠
libuv deps/uv/ クロスプラットフォームの非同期 I/O — イベントループ、ファイルシステム、ネットワーク、子プロセス
OpenSSL deps/openssl/ 暗号処理と TLS — crypto モジュールと tls モジュールを支える
llhttp deps/llhttp/ HTTP/1.1 パーサー — TypeScript で記述され C にコンパイル
nghttp2 deps/nghttp2/ HTTP/2 プロトコルの実装
ICU deps/icu-small/ Intl 向けの Unicode および国際化サポート
undici deps/undici/ fetch()WebSocket を支える HTTP クライアント
acorn deps/acorn/ モジュールシステムが使用する JavaScript パーサー
sqlite deps/sqlite/ node:sqlite 向けの組み込みデータベース
npm deps/npm/ Node.js バイナリに同梱されるパッケージマネージャー

node.gyp の機能フラグで何を含めるかを制御できます。たとえば node_use_openssl はデフォルトで 'true'node_use_sqlite'true'node_use_quic'false' になっています。このフラグを使うことで、組み込み用途向けに機能を絞り込んだ Node.js バイナリをビルドすることも可能です。

ビルドシステム

Node.js は GYP (Generate Your Projects) を使っています。これは Google が Chromium のために作ったビルドシステムです。JavaScript エコシステムの多くが別のツールに移行している中、Node.js が GYP を使い続けているのは、Windows、macOS、Linux、さまざまなアーキテクチャにまたがる C++ のコンパイルを統括する必要があるためです。

flowchart TD
    CONFIGURE["configure.py<br/>Feature detection,<br/>generates config.gypi"] --> GYP["GYP<br/>Reads node.gyp + common.gypi<br/>Generates Makefiles / .vcxproj"]
    GYP --> MAKE["make / ninja / msbuild<br/>Compiles C++ sources"]
    
    JS2C["tools/js2c.cc<br/>Bundles lib/*.js into<br/>node_javascript.cc"] --> MAKE
    
    MAKE --> BINARY["node binary"]
    
    subgraph "Build Inputs"
        NODEGYP["node.gyp — source lists,<br/>feature toggles"]
        COMMON["common.gypi — compiler<br/>flags, shared settings"]
        CONFIGPY["configure.py — platform<br/>detection, options"]
    end
    
    NODEGYP --> GYP
    COMMON --> GYP
    CONFIGPY --> CONFIGURE

ビルドの流れは次のようになっています。

  1. configure.py が最初に実行され、プラットフォームや利用可能な機能を検出し、config.gypi を生成します。OpenSSL や ICU などのオプションコンポーネントを確認する Python スクリプトです。

  2. GYPnode.gyp(すべての C++ ソースファイルの一覧)と common.gypi(共通のコンパイラフラグ)を読み込み、プラットフォーム固有のビルドファイルを生成します。

  3. js2c は見落としがちですが、重要なステップです。tools/js2c.cc ツールが lib/ 内のすべての JavaScript ファイルを読み込み、node_javascript.cc 内の C++ 文字列リテラルとしてコンパイルします。つまり JavaScript 標準ライブラリは Node.js バイナリに焼き込まれており、fshttp などの組み込みモジュールをロードするためのファイル I/O は一切不要です。

  4. C++ コンパイラがすべてをリンクして最終的な node バイナリを生成します。

Unix では Makefile がこの一連の処理をラップし、Windows では vcbuild.bat が同様の役割を担います。

ヒント: lib/ 内の JavaScript ファイルを変更した場合、コンパイル済みバイナリに反映させるには再ビルドが必要です。ただし、開発中に素早くイテレーションしたい場合は、環境変数 NODE_BUILTIN_MODULES_PATHlib/ ディレクトリのパスを設定する方法が使えます。

テストの構成とナビゲーションガイド

Node.js のテストスイートは、オープンソースの世界でも屈指の充実度を誇ります。テストは実行戦略によって整理されています。

ディレクトリ 役割 実行方法
test/parallel/ 並列実行できるテスト 約4,085ファイル
test/sequential/ 逐次実行が必要なテスト ポート競合やグローバル状態の管理
test/cctest/ Google Test を使った C++ ユニットテスト C++ 内部実装を直接テスト
test/pummel/ ストレステストや長時間実行テスト 通常の CI では実行しない
test/fixtures/ テスト用データファイル テスト間で共有
test/common/ テスト共通ユーティリティ テストファイルからインポートして使用

ファイルの命名規則は統一されており、test-{module}-{feature}.js という形式です。たとえば test-fs-read-file.jsfs.readFile() を、test-net-connect-timeout.js は TCP 接続のタイムアウトをテストしています。

「X を変更したいときは Y を見る」という実践的なマップを以下にまとめます。

変更したい対象 参照先
公開 API(例:fs.readFile lib/fs.js + src/node_file.cc
require() のモジュール解決 lib/internal/modules/cjs/loader.js
ES モジュールの import 動作 lib/internal/modules/esm/loader.js
HTTP パース処理 lib/_http_*.js + deps/llhttp/
イベントループ src/api/embed_helpers.cc + deps/uv/
起動・ブートストラップの挙動 src/node.cc + lib/internal/bootstrap/*.js
プロセスレベルのオプション(--inspect など) src/node_options.h
パーミッションモデル(--allow-fs-read src/permission/
エラーコード(ERR_* lib/internal/errors.js
タイマーの実装 lib/internal/timers.js

ヒント: テストファイルはエッジケースを知るための最良のドキュメントでもあります。特定のシナリオで API がどう振る舞うか確信が持てないときは、test/parallel/ の中から該当するテストファイルを検索してみましょう。

次のステップ

コードベースの全体像を頭に入れたところで、次はいよいよ node script.js を実行したときに何が起きるかを追っていきましょう。次の記事では、C++ の main() 関数から始まり、V8 isolate の生成、JavaScript のブートストラップチェーン、そしてイベントループへの突入まで — プロセス起動から最初の JavaScript 行が実行されるまでの完全な流れを追います。