こんにちは、虎の穴ラボのH.Kです。
JavaOne 2026にてOracleから超野心的な新プロジェクト「Project Detroit」が発表されました!
このプロジェクト、なんとJavaの中にJavaScriptエンジン「V8」やPythonランタイム「CPython」を直接組み込んでしまうという代物です。AIやデータ領域が盛り上がる中、他言語エンジンたちをJavaから透過的に扱えるようにするアプローチとして大きな注目を集めています。
個人的に「GraalVMのPolyglotがあるのになぜ?」「一体どうやってV8とJavaを繋いでいるの?」と技術的にめちゃくちゃ気になったので、さっそく公開されたコードベースを読み解いてみました!
本記事では、特に JavaScript(V8)とJavaの相互運用 に焦点を当て、その裏側で動いている3層のブリッジ構造や、最新JDKにおけるJNIの扱いについてわかったことをまとめていきます。
1. JavaScriptからJavaクラスを呼び出せる仕組み
JavaScriptからJavaの世界へアクセスするための仕組みは、大きく分けて3層のブリッジ構造になっています。
第1層:JNIによる物理的な接着剤(Java ↔ C++)
最下層では、伝統的なJNI(Java Native Interface)が使われています。
プロジェクト内の src/native/ にある jvmv8_jni.cpp や jvmv8_java_classes.cpp が、JavaとV8エンジン(C++で実装)を物理的につなぐ役割を果たします。Javaの native メソッドとして宣言された関数はJNIを通じてC++に委譲され、「Java → C++ → V8」および「V8 → C++ → Java」の双方向アクセスを実現しています。
第2層:JS側のAPI(Java.type() と JavaImporter)
JavaScript側からは、非常に直感的にJavaのクラスを扱えるAPIが提供されています。
これらは V8GlobalFunctions.java で定義され、V8起動時にグローバルスコープに注入されます。
// 1. Javaの型をJSの変数として取得 var Runnable = Java.type("java.lang.Runnable"); var Thread = Java.type("java.lang.Thread"); // 2. JSのオブジェクト記法でJavaインターフェースを実装 var r = new Runnable({ run: function() { print("I am a runnable " + Thread.currentThread()); } }); r.run(); // 3. JavaImporterでパッケージ単位のインポート with (new JavaImporter(java.io, java.net, java.lang.StringBuilder)) { var buf = new StringBuilder(); var u = new URL("https://dev.java/"); } // 4. ドット記法での直接アクセス var console = java.lang.System.console();
第3層:リフレクションとコード生成による透過的なラップ
コアとなるのが V8ClassGenerator.java と AccessibleMembersLookup.java です。JS側で Java.type() が呼ばれると、内部で以下の処理が走ります。
- メタ情報の収集: Javaリフレクションで対象クラスのメソッドやフィールドを動的に読み取る。
- テンプレート生成: そのクラスに対応するV8のオブジェクトテンプレート(JSの「型」)をC++レベルで生成する。
- コールバック登録: 各メソッドに対応するコールバック関数をV8に登録し、JSからの呼び出しをJNI経由でJavaへ繋ぐ。
💡 逆方向のアクセス(JS → Java) 逆にJSのオブジェクトをJavaのインターフェースとして扱う場合は、
V8InterfaceImplementor.javaが動的プロキシ(java.lang.reflect.Proxy)を利用してラッパーオブジェクトを生成します。
【まとめ:データの流れ】
JavaScript (V8) ↕ V8 Object Template / Callback C++ ブリッジ層 (jvmv8_*.cpp) ↕ JNI (Java Native Interface) Java (JVM) ↕ Reflection / Dynamic Proxy Javaクラス・インターフェース
GraalVMのPolyglot APIに近い発想ですが、純粋なV8ベースでこれを実現しているのがdetroit-jsの大きな特徴です。
余談:GraalVMの「Polyglot API」との違い
Javaから他言語を呼び出す仕組みといえば、すでに実績のあるGraalVMのPolyglot APIを思い浮かべる方も多いでしょう。一見すると似たような機能に見えますが、内部のアプローチは「真逆」と言っていいほど異なります。
GraalVMのアプローチ:JVM上で世界を統一する(Truffleフレームワーク)
GraalVM(例えばGraalJSやGraalPy)は、JavaScriptやPythonの実行エンジンをJava(Truffleフレームワーク)で再実装しています。 つまり、JSもPythonも最終的にはJavaのバイトコードとしてJVM上で動き、メモリ管理(GC)もJVMがまとめて面倒を見ます。言語の壁を越えても「すべてが同じJVMの上にある」ため、相互アクセスのオーバーヘッドが極めて低く、シームレスな連携ができるのが強みです。
Project Detroitのアプローチ:本物のネイティブエンジンを横付けする(JNIブリッジ)
一方、今回のProject DetroitはJSエンジンをJavaで再実装しません。Google Chromeなどで使われているC++製の「本物のV8エンジン」をそのまま持ってきて、JNIを使ってJVMの横にドカンと横付けしています。 JVMとV8はそれぞれ独立したメモリ空間とGCを持っているため、先ほど解説したような「3層の複雑なブリッジ(通訳)」が必要になります。
なぜわざわざ「横付け」するのか?
GraalVMの「すべてをJavaで再実装する」アプローチはアーキテクチャとして美しい反面、絶望的な保守コストを伴います。 猛スピードで進化するJSやPythonの言語仕様に追従し続け、さらにNumPyのような「CPythonのC APIにべったり依存したライブラリ」をエミュレートし続けるのは至難の業です(かつてのNashornが力尽きたのもこれが原因です)。
Project Detroitの根底にあるのは、「言語エンジンの保守はGoogle(V8)やPythonコミュニティに任せよう」という現実主義です。ブリッジのオーバーヘッドを許容してでも、オリジナルをそのまま丸ごと使うことで、「100%完全な互換性」と「最新仕様への保守コストの大幅削減」を同時に実現するという、極めて実用的でパワープレイな設計思想に基づいているのです。
2. JDK 24以降のJNI警告問題への対応
近年、Javaプラットフォームは "integrity by default"(デフォルトでの完全性・堅牢性)を推し進めています。その一環として JDK 24で導入された JEP 472 により、JNIの使用には厳しい制限が掛かるようになりました。
ネイティブライブラリの読み込み(System.loadLibrary())や、native メソッドの宣言を行うと、デフォルトで実行時に警告が出ます。
detroit-jsのスマートな対応策
detroit-jsの起動スクリプト(jjs.sh)を見ると、この仕様変更に対して適切に対応されています。
java \ --module-path $lib \ --add-modules org.openjdk.engine.javascript \ --enable-native-access=org.openjdk.engine.javascript \ # ← ここがポイント org.openjdk.engine.javascript.Main "$@"
| 項目 | 内容 |
|---|---|
| JNI警告の仕様 | JDK 24 (JEP 472) 以降、--enable-native-access オプションなしでJNIを利用すると警告が発生する。 |
| 将来の方針 | 警告から例外(Exception)のスローへと段階的に強化される予定。 |
| detroit-jsの対応 | 起動時に --enable-native-access=org.openjdk.engine.javascript を明示的に指定し、警告を抑制。 |
| 設計の優れた点 | JPMS(モジュールシステム)を活用し、ALL-UNNAMED ではなく対象モジュールのみに最小権限を与えている。 |
3. なぜ「Panama(FFM API)」ではなく「JNI」なのか?
現代のJavaでネイティブ連携といえば、Project Panamaによる FFM API (java.lang.foreign) がトレンドです。しかし、detroit-jsのコードベースを見ると、Panamaのクラスは一切使われておらず、正統派のJNIが採用されています。
技術的理由を推測していきます。
- 複雑な双方向コールバックへの適性
FFM APIはC言語的なフラットな関数呼び出し(ダウンコール/アップコール)には強力ですが、V8との連携では「C++側からJavaメソッドをコールバックとして頻繁に呼ぶ」複雑なやり取りが発生します。
JNIEnv*を通じてJavaオブジェクトを直接操作できるJNIの方が、V8のC++クラスやテンプレートを用いた高度なAPIと自然にマッピングできるのです。
余談:前世代エンジン「Nashorn」からのパラダイムシフト
かつてJavaには、JDK 8から標準搭載され、JDK 15で削除されたJSエンジン「Nashorn」がありました。
Nashornは今回のdetroit-jsとは異なり、100%ピュアJavaで実装されたJSエンジンでした。しかし、ピュアJavaで独自のJSエンジンをゼロから開発・維持するアプローチは、ECMAScriptの爆発的な進化スピードに追いつくためのメンテナンスコストが膨大になり、結果として非推奨の道を辿りました。
Project Detroitが「V8 + JNI」という全く違うアプローチをとった理由はここにあります。
Javaで一からJSエンジンを作るのではなく、世界中で最も使われ、進化し続けているC++製の最強エンジン「V8」をそのままJavaの世界に持ち込む戦略に切り替えたのです。そして、この巨大で複雑なC++の塊(V8)とJavaを柔軟かつ確実に結びつける「接着剤」として、新しいFFM APIよりも、C++との親和性が高く実績のあるJNIが選ばれた、というのが歴史的・技術的な実態と言えるでしょう。
おわりに
コードを読んでみてわかったのは、Project Detroitが単なる「おまけのスクリプト機能」ではなく、V8という巨大で強力な他言語エコシステムを、JNIとリフレクションを駆使して強引かつエレガントにJavaの世界へ引きずり込んでいるということです。
かつてのNashornのように「JavaでJSエンジンを再実装する」という茨の道を捨て、素直に「世界中で鍛え上げられたC++の最強エンジンに頼る」という現実的で強力な選択をした点に、現代のJavaのパラダイムシフトを感じますね。
現状はJNIによる連携が採用され、最新のJNI制限に対しても最小権限のモジュール許可でスマートに対応していますが、将来的にはこのブリッジがProject Panama(FFM API)ベースへと進化していくのかどうかも大きな見どころです。
AI時代に向けて他言語との相互運用性を急加速させるJava。今後もProject Detroitの動向を楽しくウォッチしていきたいと思います!
[NEW]続編としてPython側の実装の記事も書きました。