虎の穴ラボ技術ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

【Java】2026/3/18発表!Oracleの野心的プロジェクト「Project Detroit」のコードを読んでわかったこと

こんにちは、虎の穴ラボのH.Kです。

JavaOne 2026にてOracleから超野心的な新プロジェクト「Project Detroit」が発表されました!

openjdk.org

このプロジェクト、なんとJavaの中にJavaScriptエンジン「V8」やPythonランタイム「CPython」を直接組み込んでしまうという代物です。AIやデータ領域が盛り上がる中、他言語エンジンたちをJavaから透過的に扱えるようにするアプローチとして大きな注目を集めています。

個人的に「GraalVMのPolyglotがあるのになぜ?」「一体どうやってV8とJavaを繋いでいるの?」と技術的にめちゃくちゃ気になったので、さっそく公開されたコードベースを読み解いてみました!

本記事では、特に JavaScript(V8)とJavaの相互運用 に焦点を当て、その裏側で動いている3層のブリッジ構造や、最新JDKにおけるJNIの扱いについてわかったことをまとめていきます。

github.com

1. JavaScriptからJavaクラスを呼び出せる仕組み

JavaScriptからJavaの世界へアクセスするための仕組みは、大きく分けて3層のブリッジ構造になっています。

第1層:JNIによる物理的な接着剤(Java ↔ C++)

最下層では、伝統的なJNI(Java Native Interface)が使われています。 プロジェクト内の src/native/ にある jvmv8_jni.cppjvmv8_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.javaAccessibleMembersLookup.java です。JS側で Java.type() が呼ばれると、内部で以下の処理が走ります。

  1. メタ情報の収集: Javaリフレクションで対象クラスのメソッドやフィールドを動的に読み取る。
  2. テンプレート生成: そのクラスに対応するV8のオブジェクトテンプレート(JSの「型」)をC++レベルで生成する。
  3. コールバック登録: 各メソッドに対応するコールバック関数を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の使用には厳しい制限が掛かるようになりました。

openjdk.org

ネイティブライブラリの読み込み(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側の実装の記事も書きました。

toranoana-lab.hatenablog.com