🛠️

V8 JavaScript Engineをローカルでソースコードからビルドして動かす

に公開

概要

ChromeやNode.jsなどで使用されているV8 JavaScript engineを手元のPCでソースコードからビルドしてみた際の記事になります。V8 JavaScript engine単体では動かせないですが、d8というスタンドアロンで動かせるものが用意されています。今回はそのd8を実際に手元で動かせる所までを試してみました。

https://v8.dev/docs/d8

V8

https://v8.dev/

V8は、C++で書かれたGoogleのオープンソースの高性能JavaScriptおよびWebAssemblyエンジンである。ChromeやNode.jsなどで使用されている。ECMAScriptとWebAssemblyを実装し、x64、IA-32、ARMプロセッサを使用するWindows、macOS、Linuxシステム上で動作する。V8はあらゆるC++アプリケーションに組み込むことができる。

手元で動かせるようになると、ECMAScript (Ecmaインターナショナルにおいて標準化されたJavaScriptの国際規格) 実装なのでECMAScriptそのものを試す事ができ、WebAssemblyエンジンとしても試せる様になります。

開発環境

MBA Apple M3 メモリ 24GB
Sonoma 14.6.1

手元でV8をソースコードからビルドしてみる

まずは手元でビルドするにあたってソースコードのダウンロードを行います。

ソースコードのダウンロード

https://v8.dev/docs/source-code

MacOSの場合 Gitdepot_tools をインストールします。

Git は説明するまでもないと思うので depot_tools のインストールを進めます。

depot_toolsとは?
Chromiumの開発環境を支援/拡張する以下のツール群

  • git-cl
    • チェンジリストを操作するためのすべてのツールのhome
  • git-footers
    • コミットメッセージのフッターとして表現されるメタ情報を抽出
  • git-freeze
    • ブランチ上のすべての変更 (インデックス付きとインデックスなし) を凍結
  • git-hyper-blame
    • git blameのようなものだが、特定のコミットを無視したり回避したりする機能がある
  • git-map-branches
    • upstream階層を持つすべてのローカル git ブランチをターミナル形式でカラー表示
  • git-map
    • 全Branchの履歴をカラー端末形式で表示
  • git-mark-merge-base
    • depot_toolsのmerge-baseマーカーを手動で操作
  • git-nav-downstream
    • 現在チェックアウトしているブランチのダウンストリームブランチをチェックアウト
  • git-nav-upstream
    • 現在チェックアウトしているブランチのアップストリームブランチをチェックアウト
  • git-new-branch
    • 正しいトラッキング情報を持つ新しいブランチを作成し、切り替える
  • git-rebase-update
    • すべてのブランチを更新し、アップストリームからの最新の変更を反映
  • git-rename-branch
    • ブランチの名前を変更しダウンストリームのすべての関係を正しく保持
  • git-reparent-branch
    • 現在のブランチの親(上流)を変更
  • git-retry
    • git コマンドを再試行するブートストラップ関数
  • git-squash-branch
    • ひとつのブランチにあるすべてのコミットを、ひとつのコミットに置き換える
  • git-thaw
    • 凍結されたブランチのすべての変更の凍結を解除
  • git-upstream-diff
    • 現在のブランチとその上流ブランチの差分を表示

以下のリポジトリを git clone します。

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

PATH の先頭に git clone したディレクトリ(/path/to/depot_tools )を追加します。 (~/.bashrc ~/.zshrc などに設定しとく)

まずは、Git/Gerrit の認証情報の診断と設定を行う以下コマンドを実施します。

$ git cl creds-check --global

Gerritとは?
フリーのウェブベースのコード共同管理ツール。チーム内のソフトウェア開発者がソースコードに加えた変更内容を互いにウェブブラウザでレビューし、変更内容を承認または却下することができる。分散型バージョン管理システムのGitと統合する

手元の環境だと ~/.gitconfig に以下が追加されました。

[credential "https://android.googlesource.com"]
  helper = 
  helper = luci
[credential "https://aomedia.googlesource.com"]
  helper = 
  helper = luci
[credential "https://beto-core.googlesource.com"]
  helper = 
  helper = luci
[credential "https://boringssl.googlesource.com"]
  helper = 
  helper = luci
[credential "https://chromium.googlesource.com"]
  helper = 
  helper = luci
[credential "https://chrome-internal.googlesource.com"]
  helper = 
  helper = luci
[credential "https://dawn.googlesource.com"]
  helper = 
  helper = luci
[credential "https://pdfium.googlesource.com"]
  helper = 
  helper = luci
[credential "https://quiche.googlesource.com"]
  helper = 
  helper = luci
[credential "https://skia.googlesource.com"]
  helper = 
  helper = luci
[credential "https://swiftshader.googlesource.com"]
  helper = 
  helper = luci
[credential "https://webrtc.googlesource.com"]
  helper = 
  helper = luci

準備ができたので専用のディレクトリを作成し、ソースコードを取得していきます。

$ mkdir chromium-v8
$ cd chromium-v8
$ fetch --no-history v8

ダウンロードが完了すると以下の様なファイルがダウンロードされていました。

$ erdtree -H -. -L 2
230.4 MiB chromium-v8
230.3 MiB ├─ v8
112.9 MiB │  ├─ test
 68.4 MiB │  ├─ src
 37.5 MiB │  ├─ .git
  7.3 MiB │  ├─ tools
  1.6 MiB │  ├─ third_party
  1.5 MiB │  ├─ include
276.0 KiB │  ├─ BUILD.gn
184.0 KiB │  ├─ infra
180.0 KiB │  ├─ BUILD.bazel
 76.0 KiB │  ├─ testing
 64.0 KiB │  ├─ samples
 60.0 KiB │  ├─ bazel
 44.0 KiB │  ├─ gni
 36.0 KiB │  ├─ DEPS
 24.0 KiB │  ├─ PRESUBMIT.py
 16.0 KiB │  ├─ build_overrides
 16.0 KiB │  ├─ AUTHORS
 12.0 KiB │  ├─ custom_deps
  8.0 KiB │  ├─ .ycm_extra_conf.py
  8.0 KiB │  ├─ WATCHLISTS
  8.0 KiB │  ├─ .git-blame-ignore-revs
  8.0 KiB │  ├─ docs
  4.0 KiB │  ├─ .vpython3
  4.0 KiB │  ├─ LICENSE.strongtalk
  4.0 KiB │  ├─ .bazelrc
  4.0 KiB │  ├─ PPC_OWNERS
  4.0 KiB │  ├─ .mailmap
  4.0 KiB │  ├─ MODULE.bazel
  4.0 KiB │  ├─ S390_OWNERS
  4.0 KiB │  ├─ OWNERS
  4.0 KiB │  ├─ .gitattributes
  4.0 KiB │  ├─ .clang-format
  4.0 KiB │  ├─ LOONG_OWNERS
  4.0 KiB │  ├─ .github
  4.0 KiB │  ├─ .style.yapf
  4.0 KiB │  ├─ LICENSE.v8
  4.0 KiB │  ├─ .gitignore
  4.0 KiB │  ├─ LICENSE.fdlibm
  4.0 KiB │  ├─ INTL_OWNERS
  4.0 KiB │  ├─ README.md
  4.0 KiB │  ├─ codereview.settings
  4.0 KiB │  ├─ .editorconfig
  4.0 KiB │  ├─ .clang-tidy
  4.0 KiB │  ├─ ENG_REVIEW_OWNERS
  4.0 KiB │  ├─ MIPS_OWNERS
  4.0 KiB │  ├─ .gn
  4.0 KiB │  ├─ COMMON_OWNERS
  4.0 KiB │  ├─ INFRA_OWNERS
  4.0 KiB │  ├─ LICENSE
  4.0 KiB │  ├─ pyrightconfig.json
  4.0 KiB │  ├─ RISCV_OWNERS
  4.0 KiB │  ├─ .flake8
  4.0 KiB │  ├─ DIR_METADATA
  4.0 KiB │  └─ CODE_OF_CONDUCT.md
 36.0 KiB ├─ .cipd
 32.0 KiB │  ├─ pkgs
  4.0 KiB │  └─ tagcache.db
  8.0 KiB ├─ .gclient_entries
  4.0 KiB ├─ .gclient_previous_sync_commits
  4.0 KiB ├─ .gcs_entries
  4.0 KiB └─ .gclient

次にthird_party reposの更新とpre-compile hooksの実行も行っときます。

$ cd v8 && gclient sync
Syncing projects: 100% (46/46), done.
Running hooks: 100% (23/23), done.

GNを使用してビルド

https://v8.dev/docs/build-gn

GNとは?
GN は “Generate Ninja” の略で、Google が Chromium プロジェクト向けに開発した メタビルドシステム です — 独自 DSL で記述された BUILD.gn ファイルを読み取り、実際のビルダーである Ninja 用の build.ninja を高速生成することに特化しています。

  1. ビルドファイルを生成する

一般的な設定のビルドファイルをより簡単に生成するための便利なスクリプトの v8/tools/dev/v8gen.py を使用します。

以下で利用可能な構成を一覧表示する事ができます。

# v8ディレクトリ配下で作業
$ ./tools/dev/v8gen.py list
android.arm.debug
android.arm.optdebug
android.arm.release
arm.debug
arm.optdebug
arm.release
arm64.debug
arm64.optdebug
arm64.release
arm64.release.sample
ia32.debug
ia32.optdebug
ia32.release
mips64el.debug
mips64el.optdebug
mips64el.release
ppc64.debug
ppc64.debug.sim
ppc64.optdebug
ppc64.optdebug.sim
ppc64.release
ppc64.release.sim
riscv64.debug
riscv64.debug.sim
riscv64.optdebug
riscv64.optdebug.sim
riscv64.release
riscv64.release.sim
s390x.debug
s390x.debug.sim
s390x.optdebug
s390x.optdebug.sim
s390x.release
s390x.release.sim
x64.debug
x64.optdebug
x64.release
x64.release.sample

実際にMacのApple silicon向けのビルドファイルを生成してみます。

./tools/dev/v8gen.py arm64.release

実行すると out.gn/arm64.release が作成されます。

  1. ビルドする

準備ができたので d8 をビルドしていきます。

ninja -C out.gn/arm64.release d8

無事ビルドが終わると out.gn/arm64.release/d8 が作成されます。

d8単体で実行することができます。

out.gn/arm64.release/d8
V8 version 13.9.0 (candidate)
d8> console.log('hello');
hello
undefined
d8> const array = [1,2,3,4].map((x) => x * 2);
undefined
d8> console.log(array);
2,4,6,8
undefined

d8に関して

d8のソースコード自体は src/d8 にあります。d8が setTimeout 等のWeb APIをどこまで擬似的に対応しているのかチェックする test.js を作成して試してみます。

function testWebAPICompatibility() {
    print("Web API Compatibility in d8:");
    
    const webAPIs = [
        // Console API
        { name: 'console', test: () => typeof console !== 'undefined' && typeof console.log === 'function' },
        { name: 'console.error', test: () => typeof console.error === 'function' },
        { name: 'console.warn', test: () => typeof console.warn === 'function' },
        { name: 'console.info', test: () => typeof console.info === 'function' },
        { name: 'console.debug', test: () => typeof console.debug === 'function' },
        { name: 'console.trace', test: () => typeof console.trace === 'function' },
        { name: 'console.assert', test: () => typeof console.assert === 'function' },

        // Timers (usually not available in d8)
        { name: 'setTimeout', test: () => typeof setTimeout === 'function' },
        { name: 'setInterval', test: () => typeof setInterval === 'function' },
        { name: 'clearTimeout', test: () => typeof clearTimeout === 'function' },
        { name: 'clearInterval', test: () => typeof clearInterval === 'function' },
        { name: 'setImmediate', test: () => typeof setImmediate === 'function' },

        // URL API
        { name: 'URL', test: () => typeof URL === 'function' },
        { name: 'URLSearchParams', test: () => typeof URLSearchParams === 'function' },

        // Encoding API
        { name: 'TextEncoder', test: () => typeof TextEncoder === 'function' },
        { name: 'TextDecoder', test: () => typeof TextDecoder === 'function' },
        { name: 'btoa', test: () => typeof btoa === 'function' },
        { name: 'atob', test: () => typeof atob === 'function' },

        // Crypto API
        { name: 'crypto', test: () => typeof crypto !== 'undefined' },
        { name: 'crypto.getRandomValues', test: () => typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function' },
        { name: 'crypto.randomUUID', test: () => typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' },
        { name: 'crypto.subtle', test: () => typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined' },

        // Performance API
        { name: 'performance', test: () => typeof performance !== 'undefined' },
        { name: 'performance.now', test: () => typeof performance !== 'undefined' && typeof performance.now === 'function' },
        { name: 'performance.mark', test: () => typeof performance !== 'undefined' && typeof performance.mark === 'function' },
        { name: 'performance.measure', test: () => typeof performance !== 'undefined' && typeof performance.measure === 'function' },

        // Fetch API
        { name: 'fetch', test: () => typeof fetch === 'function' },
        { name: 'Request', test: () => typeof Request === 'function' },
        { name: 'Response', test: () => typeof Response === 'function' },
        { name: 'Headers', test: () => typeof Headers === 'function' },

        // Streams API
        { name: 'ReadableStream', test: () => typeof ReadableStream === 'function' },
        { name: 'WritableStream', test: () => typeof WritableStream === 'function' },
        { name: 'TransformStream', test: () => typeof TransformStream === 'function' },

        // File API
        { name: 'File', test: () => typeof File === 'function' },
        { name: 'Blob', test: () => typeof Blob === 'function' },
        { name: 'FileReader', test: () => typeof FileReader === 'function' },

        // Storage API
        { name: 'localStorage', test: () => typeof localStorage !== 'undefined' },
        { name: 'sessionStorage', test: () => typeof sessionStorage !== 'undefined' },

        // IndexedDB
        { name: 'indexedDB', test: () => typeof indexedDB !== 'undefined' },
        { name: 'IDBDatabase', test: () => typeof IDBDatabase === 'function' },

        // WebSocket
        { name: 'WebSocket', test: () => typeof WebSocket === 'function' },

        // Event API
        { name: 'Event', test: () => typeof Event === 'function' },
        { name: 'CustomEvent', test: () => typeof CustomEvent === 'function' },
        { name: 'EventTarget', test: () => typeof EventTarget === 'function' },
        { name: 'AbortController', test: () => typeof AbortController === 'function' },
        { name: 'AbortSignal', test: () => typeof AbortSignal === 'function' },

        // DOM-like APIs (usually not in d8)
        { name: 'document', test: () => typeof document !== 'undefined' },
        { name: 'window', test: () => typeof window !== 'undefined' },
        { name: 'global', test: () => typeof global !== 'undefined' },
        { name: 'globalThis', test: () => typeof globalThis !== 'undefined' },

        // Worker APIs
        { name: 'Worker', test: () => typeof Worker === 'function' },
        { name: 'SharedWorker', test: () => typeof SharedWorker === 'function' },
        { name: 'MessageChannel', test: () => typeof MessageChannel === 'function' },
        { name: 'MessagePort', test: () => typeof MessagePort === 'function' },

        // Intl API (usually available)
        { name: 'Intl', test: () => typeof Intl !== 'undefined' },
        { name: 'Intl.DateTimeFormat', test: () => typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat === 'function' },
        { name: 'Intl.NumberFormat', test: () => typeof Intl !== 'undefined' && typeof Intl.NumberFormat === 'function' },
        { name: 'Intl.Collator', test: () => typeof Intl !== 'undefined' && typeof Intl.Collator === 'function' },

        // Other utility APIs
        { name: 'structuredClone', test: () => typeof structuredClone === 'function' },
        { name: 'queueMicrotask', test: () => typeof queueMicrotask === 'function' },
    ];

    let supported = 0;
    let total = webAPIs.length;

    webAPIs.forEach(api => {
        try {
            const isSupported = api.test();
            print(`  ${api.name}: ${isSupported ? '✓ SUPPORTED' : '✗ NOT SUPPORTED'}`);
            if (isSupported) supported++;
        } catch (error) {
            print(`  ${api.name}: ✗ ERROR - ${error.message}`);
        }
    });

    print(`\nWeb API Support Summary: ${supported}/${total} (${Math.round(supported/total*100)}%)`);
}
testWebAPICompatibility();

実際に試してみると以下の様な結果になりました。

Web API Compatibility in d8:
  console: ✓ SUPPORTED
  console.error: ✓ SUPPORTED
  console.warn: ✓ SUPPORTED
  console.info: ✓ SUPPORTED
  console.debug: ✓ SUPPORTED
  console.trace: ✓ SUPPORTED
  console.assert: ✓ SUPPORTED
  setTimeout: ✓ SUPPORTED
  setInterval: ✗ NOT SUPPORTED
  clearTimeout: ✗ NOT SUPPORTED
  clearInterval: ✗ NOT SUPPORTED
  setImmediate: ✗ NOT SUPPORTED
  URL: ✗ NOT SUPPORTED
  URLSearchParams: ✗ NOT SUPPORTED
  TextEncoder: ✗ NOT SUPPORTED
  TextDecoder: ✗ NOT SUPPORTED
  btoa: ✗ NOT SUPPORTED
  atob: ✗ NOT SUPPORTED
  crypto: ✗ NOT SUPPORTED
  crypto.getRandomValues: ✗ NOT SUPPORTED
  crypto.randomUUID: ✗ NOT SUPPORTED
  crypto.subtle: ✗ NOT SUPPORTED
  performance: ✓ SUPPORTED
  performance.now: ✓ SUPPORTED
  performance.mark: ✓ SUPPORTED
  performance.measure: ✓ SUPPORTED
  fetch: ✗ NOT SUPPORTED
  Request: ✗ NOT SUPPORTED
  Response: ✗ NOT SUPPORTED
  Headers: ✗ NOT SUPPORTED
  ReadableStream: ✗ NOT SUPPORTED
  WritableStream: ✗ NOT SUPPORTED
  TransformStream: ✗ NOT SUPPORTED
  File: ✗ NOT SUPPORTED
  Blob: ✗ NOT SUPPORTED
  FileReader: ✗ NOT SUPPORTED
  localStorage: ✗ NOT SUPPORTED
  sessionStorage: ✗ NOT SUPPORTED
  indexedDB: ✗ NOT SUPPORTED
  IDBDatabase: ✗ NOT SUPPORTED
  WebSocket: ✗ NOT SUPPORTED
  Event: ✗ NOT SUPPORTED
  CustomEvent: ✗ NOT SUPPORTED
  EventTarget: ✗ NOT SUPPORTED
  AbortController: ✗ NOT SUPPORTED
  AbortSignal: ✗ NOT SUPPORTED
  document: ✗ NOT SUPPORTED
  window: ✗ NOT SUPPORTED
  global: ✗ NOT SUPPORTED
  globalThis: ✓ SUPPORTED
  Worker: ✓ SUPPORTED
  SharedWorker: ✗ NOT SUPPORTED
  MessageChannel: ✗ NOT SUPPORTED
  MessagePort: ✗ NOT SUPPORTED
  Intl: ✓ SUPPORTED
  Intl.DateTimeFormat: ✓ SUPPORTED
  Intl.NumberFormat: ✓ SUPPORTED
  Intl.Collator: ✓ SUPPORTED
  structuredClone: ✗ NOT SUPPORTED
  queueMicrotask: ✗ NOT SUPPORTED

Web API Support Summary: 18/60 (30%)

使えるものは以下の様になっている様です。

  - Console API(すべて)
  - setTimeout(部分的 - setIntervalとclear系は未対応)
  - Performance API(すべて)
  - globalThis
  - Worker(基本のみ、SharedWorkerは未対応)
  - Intl API(主要な機能)

Console API 以外を軽く試してみます。

$ out.gn/arm64.release/d8
V8 version 13.9.0 (candidate)
d8> setTimeout(() => console.log("hello"), 5000)
hello
0
d8> const start = performance.now();
undefined
d8> const end = performance.now();
undefined
d8> console.log(`${(end - start).toFixed(3)}ms precision`)
4354.649ms precision
undefined
d8> Object.keys(globalThis)
["version", "print", "printErr", "write", "writeFile", "read", "readbuffer", "readline", "load", "setTimeout", "quit", "Realm", "performance", "Worker", "os", "d8", "arguments"]
d8> const formatter = new Intl.DateTimeFormat("en-US")
undefined
d8> formatter.format(new Date())
"6/25/2025"
d8> quit()

setTimeout に関しては 5000ms 待たずに即時実行されていました。

バッドノウハウ

fetch v8 で以下エラーが発生する

error: RPC failed; HTTP 500 curl 22 The requested URL returned error: 500

大容量なのでHTTPS で取得中に帯域制限があったりすると 500 を返すことがあるらしい。

なので --no-history (--depth=1 相当) を付与して実行する

$ fetch --no-history v8

Discussion