ハイライト
SwiftJava は、JavaKit マクロ、SwiftKit ライブラリ、および swift-java コマンドラインツールを通じて、Swift による JNI native メソッドの実装、Swift からの Java ライブラリのインポート、Swift ライブラリの Java ライブラリとしてのラッピングという 3 方向の相互運用を実現します。
主要内容
多くのチームのコアビジネスロジックは Java 上で動作しています。こうしたコードベースに Swift を導入するには、これまでは手書きの JNI しかありませんでした。JNI は 1997 年から存在しており、iPod よりも古い API で、ほとんど変わっていません。開発者は Java 側で native メソッドを宣言し、javac -h で C ヘッダーファイルを生成してから、Java_com_example_JNIExample_compute のような長い名前の関数を手動で実装し、JNIEnv や jobject、オブジェクトのライフサイクルを管理する必要がありました。Konrad は Session 内で「使えるが、正しく使うのは難しい。シグネチャを間違えるとクラッシュする」と率直に述べています。
WWDC25 で Apple は SwiftJava を公式の interoperability プロジェクトとして紹介しました。これは 3 つの構成要素から成ります。Swift 側の JavaKit(ライブラリ + マクロ、JNI 呼び出しを簡素化)、Java 側の SwiftKit(Java アプリケーションが Swift オブジェクトを利用するための補助ライブラリ)、および swift-java コマンドラインツールと SwiftPM プラグインです。全体の設計目標は、Swift が Java から呼び出せること、そしてその逆も可能であること、さらに両方向の呼び出しを同一プロジェクト内で共存させることです。
SwiftJava は 3 つの典型的なシーンを解決します。1 つ目は、Java アプリケーション内の native メソッドを Swift で実装し、JavaKit マクロで C ヘッダーファイルを代替するケースです。2 つ目は、Swift から既存の Java ライブラリを一括インポートし、swift-java ツールが Gradle を呼び出して依存関係を解決するケースです。3 つ目は、Swift ライブラリ全体を Java ライブラリとしてパッケージングし、Java 22 から stable となった Foreign Function and Memory API を利用して JNI を迂回するケースです。3 つのシーンはそれぞれ異なるツールの組み合わせに対応しており、独立して使用することも、混在させて使用することも可能です。
詳細
Swift による JNI native メソッドの実装
最初のシーンは、Java 側の native メソッドから始まります。Konrad は、Swift と Java はランタイムレベルで実は非常に似ていると指摘しています。クラス継承モデル、自動メモリ管理(ARC vs GC)、ジェネリックシステム、エラー送出(Swift Error vs Java Exception)がすべて対応しています。そのため、Swift で Java の native メソッドを実装するのは自然な入り口となります。
ツールチェーンのフローは次の通りです。swift-java コマンドラインツールを javac -h の代替として使用し、ツールが Java クラスを読み込んで Swift 側のブリッジコードを生成します。これには JNIExampleNativeMethods プロトコルが含まれます。これは元の C ヘッダーファイルに代わるものです。開発者は extension を書き、このプロトコルに conform させ、@JavaImplementation と @JavaMethod マクロを付与するだけです(09:05)。
import JavaKit
import JavaRuntime
import Crypto
@JavaImplementation("com.example.JNIExample")
extension JNIExample: JNIExampleNativeMethods {
@JavaMethod
func compute(_ a: JavaInteger?, _ b: JavaInteger?) -> [UInt8] {
guard let a else { fatalError("Expected non-null parameter 'a'") }
guard let a else { fatalError("Expected non-null parameter 'b'") }
let digest = SHA256Digest([a.intValue(), b.intValue()]) // convenience init defined elsewhere
return digest.toArray()
}
}
ポイント:
@JavaImplementation("com.example.JNIExample"):マクロが現在の extension が Java 側のcom.example.JNIExampleクラスの native メソッドを実装することを示し、ツールはこれに基づいて JNI エントリシンボルを生成します。extension JNIExample: JNIExampleNativeMethods:JNIExampleとJNIExampleNativeMethodsプロトコルは両方とも swift-java ツールによって生成され、プロトコルは Swift で実装する必要のあるすべての native メソッドを列挙しています。@JavaMethod:マクロが JNI の詳細(JNIEnv の抽出、パラメータの unbox、戻り値の box)を処理します。JavaInteger?:Java 側のIntegerはオブジェクトであり null の可能性があるため、Swift の Optional 型にマッピングされます。intValue()でプリミティブな int を取り出します。import Crypto:Swift の実装内では任意の Swift ライブラリを直接 import できます。ここでは swift-crypto を使用して SHA256 を計算しており、Java 側でネイティブ暗号ライブラリを呼び出す手間を省いています。
Swift からの Java ライブラリのインポート
2 つ目のシーンは方向が逆です。Swift プロジェクトが既存の Java ライブラリ(例:Apache Commons CSV)を使用したい場合です。Java ライブラリには多くの推移的依存関係があり、手動で classpath を管理するのは現実的ではありません。SwiftJava は Gradle に依存関係の解決を委譲し、その後 Swift ブリッジコードを生成します(12:30)。
依存関係の座標は Gradle の慣習に従って groupId:artifactId:version の 3 要素で記述し、swift-java.config に配置します。解決には 2 つのパスがあります。SwiftPM build plugin を使用して自動的にトリガーする方法(ただし SwiftPM のセーフティサンドボックスを無効にする必要があります)、または手動で swift-java resolve コマンドを実行して classpath をファイルに書き出す方法です。
swift-java resolve --module-name JavaApacheCommonsCSV
ポイント:
swift-java resolve:依存関係解決のサブコマンドをトリガーし、サンドボックス外で実行して SwiftPM の制限を回避します。--module-name:swift-java.config を含む対象モジュールを指定します。- 解決結果は classpath ファイルに書き出され、後続の Swift モジュール構築時に自動的に読み込まれます。
解決後、Swift 側ではまるでネイティブモジュールのように Java ライブラリを import できます(13:05)。
import JavaKit
import JavaKitIO
import JavaApacheCommonsCSV
let jvm = try JavaVirtualMachine.shared()
let reader = FileReader("sample.csv") // java.io.StringReader
for record in try JavaClass<CSVFormat>().RFC4180.parse(reader)!.getRecords()! {
for field in record.toList()! { // Field: hello
print("Field: \(field)") // Field: example
} // Field: csv
}
print("Done.")
ポイント:
JavaVirtualMachine.shared():Swift プロセス内で JVM インスタンスを起動し、Java コードは同一プロセス内で実行されます。FileReader("sample.csv"):JDK 標準のjava.io.FileReaderを直接呼び出しており、JavaKitIO によってラップされています。JavaClass<CSVFormat>().RFC4180:Java クラスの静的フィールドへのアクセスにはJavaClass<T>()が必要で、ここではCSVFormat.RFC4180を取得しています。for record in ...:返される Java コレクションは Swift の for-each で直接反復可能であり、JavaKit が Iterable プロトコルをアダプトしています。- 言語間のオブジェクトライフサイクルは JavaKit が自動管理します。必要に応じてローカル参照をグローバル参照に昇格させ、GC による誤った回収を防ぎます。
Swift ライブラリを Java から使用可能にする
3 つ目のシーンは最も複雑です。Swift ライブラリ全体を Java ライブラリとして配布することです。今回は JNI を使用せず、Java 22 から stable となった Foreign Function and Memory API を使用します(16:22)。
swift-java --input-swift Sources/SwiftyBusiness \
--java-package com.example.business \
--output-swift .build/.../outputs/SwiftyBusiness \
--output-java .build/.../outputs/Java ...
ポイント:
--input-swift:公開する Swift ソースコードのディレクトリを指定し、ツールは公開型、メソッド、プロパティを解析します。--java-package:生成された Java クラスを配置するパッケージを指定します。--output-swift/--output-java:それぞれ Swift 補助コードと Java wrapper クラスを出力します。- 最終成果物:Swift は動的ライブラリとしてコンパイルされ、生成された Java クラスとともに Java ライブラリとしてパッケージングされ、Maven リポジトリに直接公開できます。
生成された Java クラスは selfMemorySegment ポインタを保持しており、これは native ヒープ上の Swift インスタンスを指しています。Java 側は Foreign Function API を通じて Swift 関数を直接呼び出し、JNI 変換を経由しません。
しかし、Swift の struct は値型であり、安定したオブジェクトアイデンティティを持たないため、Java 側のポインタから参照できるようにするにはヒープ上に配置する必要があります。これによりメモリ管理の問題が生じます。いつ解放するのか?Session では 2 つの SwiftArena 方式が提示されています。Auto Arena は GC finalizer に依存し、シンプルですが GC 負荷が高く、解放タイミングが不確定です。Confined Arena は try-with-resources と組み合わせ、スコープ終了時に即座に解放します(18:55)。
try (var arena = SwiftArena.ofConfined()) {
var business = new SwiftyBusiness(..., arena);
}
ポイント:
SwiftArena.ofConfined():現在のスレッドにバインドされた、スコープ限定のメモリアリーナを作成します。try (...):Java 7 からの try-with-resources 構文で、スコープ終了時に自動的にclose()を呼び出します。new SwiftyBusiness(..., arena):コンストラクタが追加で arena を受け取り、Swift インスタンスは arena 管理下の native メモリに割り当てられます。- ブロック終了 → arena.close() → Java wrapper の破棄 → native 上の Swift 値の破棄、という順序が確定します。
- 利点:GC finalizer に依存せず、大量のオブジェクト時に GC パフォーマンスが崩壊するのを防ぎ、Swift プログラムが依存する決定論的なデストラクションセマンティクスを保持します。
重要ポイント
1. SwiftJava を使用して手書き JNI を代替する
なぜ価値があるか:手書き JNI が本当に難しいのは、シグネチャの綴り、オブジェクトライフサイクル管理、キャッシュ最適化などの詳細です。シグネチャを間違えると即座にクラッシュします。JavaKit マクロはこれらをすべてコンパイル時の検査に収め、クラッシュリスクを大幅に低減します。
どう始めるか:既存の Java プロジェクトから、パフォーマンスに敏感でロジックがシンプルな native メソッド(例:ハッシュ、圧縮)を選び、swift-java ツールでブリッジコードを生成し、extension を書いてそのメソッドを実装します。元の実装と benchmark で対照検証します。
2. swift-java resolve + Gradle を使用して Java エコシステムのライブラリを導入する
なぜ価値があるか:多くのドメイン(CSV 解析、Apache シリーズ、Hadoop、エンタープライズミドルウェア)には Swift に対応するライブラリがありません。SwiftJava を通じて Java ライブラリを直接インポートすることで、再実装や代替品探しのコストを削減できます。
どう始めるか:単一で明確な依存関係から始めます(例:Apache Commons CSV)。swift-java.config に依存関係の 3 要素を記述し、swift-java resolve コマンドを実行して classpath を生成し、Swift コードで import JavaXxx として API を試用します。
3. コアビジネスロジックを Swift で実装し、Java ライブラリとしてラッピングする
なぜ価値があるか:チーム内の Java プロジェクト、Android プロジェクト、サーバーサイドの Java マイクロサービスがすべて同じコアルールを必要としています。Swift で一度実装し、swift-java ツールで Java wrapper を生成すれば、すべての Java プロジェクトが利用できます。同時に、より多くのモジュールを後に Swift に移行するためのインターフェースを確保します。
どう始めるか:境界が明確なドメインモデル(請求計算、リスク管理ルール、プロトコルエンコード・デコード)を選び、Swift で struct を使用して表現し、swift-java コマンドで Java パッケージを生成し、内部 Maven リポジトリに公開して他のチームに提供します。
4. Java から Swift への呼び出しは常に Confined Arena + try-with-resources を使用する
なぜ価値があるか:Auto Arena は GC finalizer に依存し、オブジェクト数が増えると GC を圧倒し、解放タイミングが予測不能になります。Confined Arena はライフサイクルを開発者に返還し、Swift の決定論的なデストラクションセマンティクスに対応します。
どう始めるか:すべての Swift オブジェクトの作成を try (var arena = SwiftArena.ofConfined()) ブロック内に配置し、スコープ終了時に自動解放します。負荷テスト時に GC ログとレイテンシ分布を観察し、Auto Arena モードと比較します。
5. SwiftJava プロジェクト自体に注目する
なぜ価値があるか:プロジェクトは初期段階にあり、Gradle 統合はまだ磨きがかかっており、コマンドライン体験も反復改善中です。初期の参加者は API 設計に影響を与えることができ、ドキュメントやサンプルへの貢献も容易です。
どう始めるか:github.com/swiftlang/swift-java を clone し、公式の example を実行して、遭遇した問題を Swift フォーラムや GitHub で issue として報告します。
関連セッション
- Embracing Swift concurrency — 言語間の相互運用時も、Swift 側は actor 分離と並行安全性を遵守する必要があります
- Improve memory usage and performance with Swift — Swift のメモリモデルは SwiftJava の native ヒープ割り当てと密接に関連しています
- What’s new in Swift — SwiftJava は Swift のクロスプラットフォーム戦略の一部です
- Optimize SwiftUI performance with Instruments — Swift ライブラリが Java から呼び出されて UI チェーンに参加する場合、Instruments は依然としてパフォーマンス分析の主な入り口です
コメント
GitHub Issues · utterances