Skip to content

エンジンとランタイム

「JavaScript を動かす」と一口に言っても、そこには性質の異なる二つの層が同居しています。言語そのもの、すなわち構文と意味論を解釈して実行するエンジンと、そのエンジンを取り巻いて、タイマー・I/O・console のような実用上の能力を与えるランタイムです。両者はしばしば混同されますが、責務の境界ははっきりしており、その境界を意識するかどうかは、エンジンを設計するうえでも、それを何かに組み込むうえでも効いてきます。この章では、エンジンとランタイムのそれぞれが何を担うのかを定義し、エンジンを埋め込むホストアプリケーションが負う責務の全体像を一望します。個々の機構の深い話は既存の章に譲り、ここではまず地図を描くことに徹します。次章の第V部「埋め込みと C API」は、この地図のうち「ホストとエンジンの境界」を細部まで掘り下げるものであり、本章はその概念的な導入を兼ねます。

JavaScript エンジンとは

JavaScript エンジンは、言語コアの実行系です。ソース文字列を受け取って字句解析・構文解析でその構造を取り出し、バイトコードなどの内部表現へコンパイルし、それを実行します。実行の過程で生まれるオブジェクトのためにヒープを管理し、到達不能になったメモリをガベージコレクション(GC)で回収するのもエンジンの仕事です。加えて、ECMA-262 が定める組み込み、すなわち ObjectArrayMathJSON といった標準オブジェクトや、Promise の解決に伴うジョブの生成といった、言語仕様が規定する振る舞いまでを含みます。ここでいうジョブは、Promise のコールバックを後で実行するために積む仕事の単位で、いつ何を積むか(生成順序)は ECMA-262 が定めています。

重要なのは、エンジンは基本的に言語の内側で完結するという点です。数値を足す、オブジェクトのプロパティを引く、関数を呼ぶ、例外を巻き戻す、Promise のジョブを積む——これらはすべて言語仕様の範囲で閉じています。エンジン単体は、ファイルを読む・ネットワークに繋ぐ・時間が経ったら関数を呼ぶ・画面に何かを出すといった、外界との I/O の手段を持ちません。Promise を解決するジョブをどう「積む」かはエンジンが知っていても、その積まれたジョブをいつ「取り出して実行する」かは、エンジンの外側が駆動します。エンジンは、外界を持たない純粋な計算機に近いものだと捉えると見通しがよくなります。

JavaScript ランタイムとは

JavaScript ランタイムは、このエンジンに、ホスト環境が提供する能力を足したものです。エンジンが言語コアの計算機だとすれば、ランタイムはその計算機を実世界に接続する配線と周辺装置にあたります。具体的には、次のようなものがランタイムの側にあります。

  • イベントループ: 積まれたジョブ(マイクロタスク)や、後述するマクロタスクを、しかるべき順序で取り出して実行し続ける駆動機構。
  • タイマー: setTimeout のように、一定時間後に処理を実行させる仕組み。
  • I/O: ファイル・ネットワーク・標準入出力など、外界との入出力。
  • モジュールの解決・取得: import 指定子を実体へ解決し、ソースを取得してエンジンに渡す仕組み。
  • ホストグローバルと標準ライブラリ: consolefetchprocess のような、言語仕様には存在しないがそのホストで使えるグローバルや API。

これらはいずれも ECMA-262 の外側にあります。setTimeoutconsolefetch も、言語仕様の条文には現れません。それらを定めるのはホスト——ブラウザであり、サーバサイドランタイムであり、エンジンを組み込んだアプリケーション——です。だからこそ、console.log があるかどうか、fetch が使えるかどうかは、走らせる環境によって変わります。そして同じエンジンが、異なるランタイムに載りうるという事実がここから導かれます。言語コアは共通のまま、周りに足す能力だけを差し替えれば、同一のエンジンをブラウザにもサーバにも組込みにも据えられます。

層の関係

エンジンとランタイムの関係は、内側から「言語仕様 → エンジン → ランタイム(ホスト環境)」という入れ子で捉えられます。言語仕様が定める振る舞いをエンジンが実装し、そのエンジンをホスト環境が包んで実用的な能力を足す、という三重の入れ子です。

flowchart TB
    subgraph runtime["JS ランタイム(ホスト環境)"]
        subgraph engine["JS エンジン"]
            core["言語コア<br/>字句解析・構文解析・コンパイル・実行・GC・ECMA-262 組み込み"]
        end
        host["ホストが足すもの<br/>イベントループ・タイマー・I/O・モジュールローダ・ホストグローバル"]
    end

エンジンは言語コアを実行し、ランタイムはそれにホスト環境の能力を足したもの。同じエンジンが複数のランタイムに載りうる。

具体例

同じエンジンが載る先として、代表的なランタイムを三つ挙げます。いずれも「エンジン+ホストが足すもの」という構図は共通で、足すものの中身が違うだけです。

  • ブラウザ: エンジンに、DOM 操作・fetch・タイマー・各種イベントなど、文書とネットワークを扱う能力を足したもの。信頼できないページのスクリプトを安全に走らせる制約も、この層が課します。
  • サーバサイドランタイム: エンジンに、ファイルシステム・ネットワーク・プロセス・標準入出力など、サーバ上で必要な能力を足したもの。DOM の代わりに OS 寄りの機能が並びます。
  • アプリ組込み: アプリケーションがエンジンを部品として抱え込み、そのアプリ固有のホスト関数(たとえばゲームなら実体の生成やスコア操作)を JS から呼べる形で足したもの。足す能力はアプリの目的にぴったり絞られます。

この三者を並べると、何がどの層の責任かが見えてきます。

誰が定めるか
言語コア ECMA-262(言語仕様) ObjectArrayMathPromise のジョブ生成
ホスト環境 ホスト(ブラウザ/サーバサイドランタイム/組込みアプリ) イベントループ・setTimeoutconsolefetch・I/O

ホストアプリケーションの責務

エンジンを埋め込むホストは、言語コアの外側を丸ごと引き受けます。エンジンが受け身の部品である以上、それを実際に「使える」状態にする責務はホストに集まります。ここでは、その責務を一覧に集約します。各項目の機構的な詳細は、それぞれの章に譲ります。

  • 実行の駆動: イベントループを回し、タイマー・I/O などのマクロタスクをエンジンに供給する。エンジンが積んだジョブを取り出して実行し続けるのはホストの役目です。→ 第IV部「イベントループとマイクロタスク」
  • ホスト機能の提供: ネイティブのホスト関数やホストグローバルを登録し、言語の外側にある能力を JS へ渡す。C ABI に沿ったコールバックとして関数を公開するのが典型です。→ 第V部「埋め込みと C API」
  • モジュールの解決・取得: import 指定子を実体へ解決し、ソースを取得してエンジンに渡す。どこから何を読むかはホストのポリシーです。→ 第IV部「モジュール」
  • 値の寿命管理: ホストが保持する JS 値を、ハンドルやルートとして GC から守り、所有権と解放の契約を明文化する。エンジンから見えないネイティブ側の参照は、放っておくと回収されます。→ 第V部「埋め込みと C API」、第III部「ネイティブコードとの相互作用と GC 安全性」
  • サンドボックスと資源制限: 信頼できないスクリプトに備え、メモリ上限や実行時間上限(割り込み)を課して、暴走からホスト自身を守る。→ 第V部「埋め込みと C API」
  • 生成と破棄: エンジン・コンテキスト・レルムを初期化し、不要になったら破棄する。実行環境のライフサイクルそのものをホストが握ります。
責務 概要 詳細章
実行の駆動 イベントループを回しマクロタスクを供給する 第IV部「イベントループとマイクロタスク」
ホスト機能の提供 ホスト関数・ホストグローバルを登録する 第V部「埋め込みと C API」
モジュールの解決・取得 指定子から実体へ解決し取得する 第IV部「モジュール」
値の寿命管理 保持する値をハンドル/ルートで守り契約を明文化する 第V部「埋め込みと C API」、第III部「ネイティブコードとの相互作用と GC 安全性」
サンドボックスと資源制限 メモリ上限・実行時間上限で暴走を防ぐ 第V部「埋め込みと C API」
生成と破棄 エンジン/コンテキスト/レルムを初期化・破棄する 第V部「埋め込みと C API」

なぜこの分離が効くのか

エンジンを言語コアに絞り、外界への依存をホストへ押し出す設計には、はっきりした利点があります。まず、ECMA-262 が定める言語コアを、多様なホストで共有できます。言語の意味論は一つに保ったまま、周りに足す能力だけをホストごとに用意すればよいからです。次に、エンジンを軽量に保てます。ファイルやネットワークやタイマーといった外界の都合を抱え込まないぶん、エンジン本体は小さく、移植しやすく、テストしやすくなります。そして、その同じエンジンを、ブラウザにもサーバサイドランタイムにもアプリ組込みにも載せられます。載せ替えのたびに言語コアを作り直さずに済むのは、この分離があってこそです。

この分離は、機能を「持たない」判断とも表裏です。国際化(ECMA-402)や名前付きタイムゾーンをエンジンの目標に含めず、必要ならホストや外部ライブラリに委ねるのも、外界依存や重い付随データをエンジンの外へ出して言語コアを軽く保つ、同じ方針の一例です(第V部「日付・時刻」を参照)。何をエンジンに入れ、何をホストへ出すかという線引きそのものが、この分離の運用にほかなりません。

まとめ

  • エンジンは言語コアの実行系で、字句解析・構文解析・コンパイル・実行・GC・ECMA-262 組み込みまでを担い、外界の I/O は持たない。
  • ランタイムはエンジンにホスト環境の能力を足したもので、イベントループ・タイマー・I/O・モジュールローダ・ホストグローバルなど ECMA-262 の外側を含む。
  • 「言語仕様 → エンジン → ランタイム」の入れ子であり、同じエンジンが異なるランタイムに載りうる。
  • 言語コアの外側は、実行の駆動・ホスト機能の提供・モジュールの解決・値の寿命管理・資源制限・生成と破棄として、ホストに集約される。
  • この分離により言語コアを多様なホストで共有でき、エンジンを軽量に保て、載せ替えが容易になる。機能を持たない判断も同じ方針の延長にある。

参考文献