V8 JavaScript Engineをローカルでソースコードからビルドして動かす
概要
ChromeやNode.jsなどで使用されているV8 JavaScript engineを手元のPCでソースコードからビルドしてみた際の記事になります。V8 JavaScript engine単体では動かせないですが、d8というスタンドアロンで動かせるものが用意されています。今回はそのd8を実際に手元で動かせる所までを試してみました。
V8
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をソースコードからビルドしてみる
まずは手元でビルドするにあたってソースコードのダウンロードを行います。
ソースコードのダウンロード
MacOSの場合 Git
と depot_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を使用してビルド
GNとは?
GN は “Generate Ninja” の略で、Google が Chromium プロジェクト向けに開発した メタビルドシステム です — 独自 DSL で記述されたBUILD.gn
ファイルを読み取り、実際のビルダーである Ninja 用のbuild.ninja
を高速生成することに特化しています。
- ビルドファイルを生成する
一般的な設定のビルドファイルをより簡単に生成するための便利なスクリプトの 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
が作成されます。
- ビルドする
準備ができたので 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