
-
SwiftとJavaの相互運用性の詳細
SwiftとJavaを単一のコードベースで組み合わせる方法を学びましょう。SwiftとJavaをそれぞれ他方のプログラムで使用するための、Swift/Java相互運用性に関するプロジェクトを紹介します。このプロジェクトが提供するツールやライブラリを使用して、これら2つのランタイム間で相互運用性のある安全で高パフォーマンスなコードを記述する方法を解説します。
関連する章
- 0:00 - イントロダクションとアジェンダ
- 2:41 - ランタイムの相違点
- 3:31 - Javaネイティブメソッド
- 6:29 - SwiftJava
- 10:13 - SwiftからのJavaの呼び出し
- 14:01 - JavaからのSwiftの呼び出し
- 20:47 - まとめ
リソース
関連ビデオ
WWDC25
WWDC23
-
このビデオを検索
こんにちは Konradです Swift言語チームのエンジニアです 本日は 相互運用性に関する 新たな取り組みを紹介します 今年初めに始まった SwiftとJavaの相互運用性です 相互運用性は Swiftを活用できるアプリの 幅が広がるという点で重要です 例えば 他の言語で書かれた 既存のコードベースに Swiftを段階的に 導入できるようになります わざわざコードを書き直す必要はありません 既存のコードベースの その他の部分はそのままで 新しい機能を追加したり 既存の機能をSwiftに置き換えたりできます また 他の言語で実装されている ライブラリも再利用できます Swift言語のライブラリを 他言語で利用することもできます さらに 今回の相互運用性は 言語面だけにとどまりません 各エコシステムに最適な ビルドツールも統合しています CやC++のCMake JavaのGradleなどです C言語との相互運用性は 当初からSwiftの重要な機能でした これにより Appleの デベロッパエコシステムと シームレスに統合できたからこそ SwiftはAppleプラットフォームの 開発における プライマリ言語となっています 2年前には C++との 相互運用性も実現しました C++とSwiftのライブラリが 1つの コードベースで併用可能になったのです その結果 Swiftをさらに幅広く 活用できるようになりました この点に関して詳しくは WWDC 2023のセッション 「Mixed Swift and C++」 または今年のセッション「Safely Mixed C, C++ and Swift」をご覧ください さて 相互運用性には 2つの方向性があります 1つは Swiftで記述された アプリのコードから Javaのコードを呼び出すという方向 もう1つは JavaからSwiftのコードを 呼び出すという方向です 今回の相互運用性は この両方向に対応しています 使用するツールやテクニックは 方向よって多少異なりますが 1つのプロジェクトで 両方を併用する必要が生じるかもしれません 具体例を紹介する前に JavaとSwiftの ランタイムの違いを復習しましょう それが済んだら Javaとの 相互運用性について 実際の具体例を3つ見ていきます まず Javaの既存の ネイティブメソッドの使い方 続いて Swiftから1つの Javaライブラリの全体を 利用できるようにする方法 最後が反対方向 つまり SwiftライブラリをJavaプロジェクトで 簡単に利用できるようにする方法です では 2つのランタイムの 比較から始めましょう Javaランタイムは 既にSwiftとの相互運用が可能な 他のネイティブ言語とは大きく異なります ところが やや大きな視点で見ると SwiftはJavaに 非常によく似たネイティブ言語だと 言うこともできます どちらもクラスが利用でき 継承モデルもよく似ています また メモリ管理は どちらも自動で ほぼ透過的です ジェネリクスがあるのも同じで しかもよく似ています ただし ランタイムでの 表れ方はそれぞれ異なります そして Javaの例外と同様 Swiftのエラーもスローが可能です もっとも ランタイムでのエラーや例外に 含まれる情報の量は若干の差があります ここで重要なのは 各種機能の相互運用の あり方を慎重に検討すれば ほとんどのAPIは どちらの言語でも表現できるという点です さて JavaとSwiftの 違いや共通点を 確認したので ここからは 既存のJavaアプリを 拡張する方法を見ていきましょう 話を単純にするために まずはSwiftで 関数を1つ実装してみましょう そのためには Java言語の ある機能を使用します ネイティブメソッドです Javaのネイティブメソッドは Java Native Interface APIに含まれています JNIとも言います JNIは iPod発売前の1997年から Javaの一部となっています Javaからネイティブコードを 呼び出せるように設計されており Javaヒープの負荷を軽減して パフォーマンス目標を達成したり Javaにはない ネイティブライブラリを使用したりする 目的でよく利用されます JNIは 今なおJavaとネイティブコードの 相互運用を確保する手段の主流で これまで JNI APIには 大きな変更がなされていません そこで まずはJNIを 見ていくことにしましょう 今回は JNIに多少なりとも 慣れることができるように ライブラリやツールを使わずに 従来の手順に従って JNIを使ってみましょう Javaアプリ側でネイティブ関数を定義し ネイティブコードを使って実装します JNIがネイティブコードに オブジェクトを渡す方法を説明するために この関数では 引数と戻り値のどちらも 大文字のInteger型を使用します プリミティブのInt値とは異なり Javaオブジェクトなので そのように扱う必要があります 次にJavaコンパイラを使用しますが ファイルにネイティブメソッドがあるので -hフラグを付けておきます これにより Cの関数宣言が入った Cヘッダーファイルが生成されます Javaネイティブメソッドが呼び出された時に JVMが呼び出すファイルです 宣言の名前はかなり長く Java_com_example_JNIExample_computeです これは ネイティブメソッドのパッケージ、 クラス、メソッド名を表しています これが受け入れるのは JNI環境、 このメソッドを呼び出す Javaオブジェクトをthis参照した jobjectパラメータ、 元のJava関数宣言の すべてのパラメータです 最後に この関数を ネイティブ言語で実装します 今回はSwiftですね
さて コードが大量ですね ボイラープレート部分で ロジックの実装が埋もれていますが こちらにあります 他の部分もノイズではありません 小さな問題が発生することは 多々あり そのどれもが クラッシュにつながりうるからです まとめると JNIは JVMからネイティブ コードを呼び出すための優れた方法ですが 適切に使用するのが 非常に難しいという問題もあります メソッドのパフォーマンスを高めるには キャッシュなどのテクニックが 必要ですが そうすると コードがさらに複雑になります また 追加のビルド手順や Cヘッダーにも注意が必要です さらに メソッドのシグネチャや マジック文字列を 目視で照合するのは間違いの元です また 作成したり受け取ったりした Javaオブジェクトの有効期間も 慎重に管理する必要があります このため JNIだけで作業を進めるのは 可能ですが 良いやり方ではありません そこでSwiftJavaです これは SwiftとJavaの相互運用の 新たな基盤となるものです SwiftJavaは SwiftとJavaの相互運用に 柔軟性、安全性、高パフォーマンスの すべてを実現するために生まれました
いくつかの要素で構成され 1つずつ導入することも 全部同時に導入することも可能です まず Swiftパッケージです JavaKitの ライブラリとマクロが含まれており 前の例で示したようなJNIコードを 安全に処理することができます 次に SwiftKitと呼ばれる Javaライブラリです これは JavaアプリでSwiftオブジェクトを 処理するのに役立ちます 最後は swift-javaコマンドラインツール および各種ビルドツールの統合です 例えば SwiftPMプラグインのほか Gradleなど 人気のJavaビルドツールとの 統合を進めています さて 先ほどの例を もう一度見てみましょう 今回は SwiftJavaのツールを使用します Javaコンパイラに代わり ここでは swift-javaコマンドラインツールを使って 必要なブリッジングを生成します 生成されたソースと追加構成の 書き込み先となる モジュールの名前を指定する必要があります もっと複雑なプロジェクトなら SwiftPMのビルドプラグインなど 別のビルドシステムを使うこともあります 生成されたSwiftファイルを見ると インポートされたJavaクラスの 記述があります Javaの型にメンバーメソッドがある場合は それも対応するSwift型で表示されるので そのJava関数に Swiftからコールバックできます 最後が 生成された JNIExampleNativeMethodsプロトコルです これには この型で実装できる ネイティブメソッドがすべて含まれています 以前に使用した Cヘッダーの代わりとなります 実際にネイティブ関数を実装するには 生成されたJNIExampleクラスに 拡張を追加して JNIExampleNativeMethodsプロトコルに 準拠させる必要があります また JavaKitにある JavaImplementationマクロを使って 注釈を付ける必要もあります 必要な関数シグネチャは コンパイラが正しく実装してくれます ただし 注釈を付ける必要があるのを 忘れないようにしましょう 使うのは JNIの細かな処理を 実行するJavaMethodマクロです SwiftJavaのおかげで メソッドの 実装が非常にシンプルになりました これならビジネスロジックに 集中できますね さらに良いのは Swiftを使っているので 好きなSwiftライブラリを使えることです 例えば ここで暗号化アルゴリズムの ネイティブ実装を 使いたいとしましょう Swiftのエコシステムには そのためのライブラリがあります Cryptoモジュールをインポートして 渡されたデータの SHA256ハッシュを計算してみましょう 先ほど JNIの実装で発生した問題を 覚えているでしょうか そのほとんどは JNIが原因ではありません JNIを正しく使うのが 難しいことが原因です SwiftJavaなら JNIの ボイラープレートの多くを省略できるので 保守や保存が非常に簡単な コードができあがります Cヘッダーを使う必要も まったくありません 代わりに 適切な型で生成された 関数シグネチャを使用できます ミスが原因のデバッグに 時間がかかる事態を防げます そして 2つの言語をつなぐコードを 手書きした場合に比べて オブジェクトの有効期間の 管理も大きく改善できました この点は 複数言語にまたがるコードの メモリ安全性を確保する上で重要です SwiftJavaを使えば このように JNI関連の処理のすべてを 安全かつ便利に進められます SwiftJavaには 他にも多くの機能があります そこで 今度はSwiftから Javaライブラリを使用する 状況を見てみましょう 今回もswift-javaツールを使います ただし 今度は 1つの型だけでなく 既存のJavaライブラリ全体を インポートします 例えば Swiftから 人気のJavaライブラリ Apache Commons CSVを使いたいとします この場合 ライブラリ自体だけでなく その下流の依存関係も すべて探す必要があります Javaライブラリには 推移的依存関係が多く さらにそれ自体も依存関係を抱えているので 依存関係の解決は すぐに複雑化してしまいます 幸い SwiftJavaならこれに対応できます 必要なのは 使いたいライブラリの アーティファクトコーディネートを 用意することだけです この情報は通常 オンライン検索で 簡単に見つかります チームにいる友人の Javaデベロッパに尋ねても良いでしょう 依存関係には3つの要素があります アーティファクトを特定する アーティファクトID ライブラリを公開している 組織を示すグループID そして バージョン番号です 次に SwiftJavaのGradle統合を使用します これは Javaエコシステムで 人気のビルドツール兼 依存関係管理ツールです 今回のこの依存関係を Gradle向けに表現するため この3つの値それぞれの間に コロンを置きます これで Gradleが理解できる形式の 依存関係が完成しました これで 依存関係の コーディネートがわかりました これをJavaApacheCommonsCSVに ダウンロードして ラップするには2つの方法があります 1つは SwiftJavaビルドツール プラグインを使用する方法です ターゲットのswift-java.configファイルに 解決したいルート依存関係を すべて列挙したセクションを追加します これで良いでしょう これで プロジェクトをビルドする際に プラグインが自動でGradleを呼び出し 依存関係を解決してくれるようになりました ただし SwiftPMではセキュリティ サンドボックスが強制されます そのためプラグインが任意のファイルや 場所にアクセスすることはできません そこで このアプローチを使用する場合は プロジェクトのビルド時にセキュリティ サンドボックスを無効にしておきます ただし どんな環境でも 実行できる方法ではありません そこで もう1つのアプローチを 見ていきましょう swift-javaコマンドラインツールの resolveコマンドを使用する方法です 設定ファイルが入った モジュールの名前をツールに渡すと 依存関係が解決され ファイルにクラスパスが 書き込まれます この処理はSwiftPMのビルドの 外部で実行されるため サンドボックスを無効にせずに済みます ただし こちらの方法では プロジェクトのビルド時などに 依存関係の解決を 手動で起動する必要があります この点はトレードオフなので ワークフローに応じて 好きな方を選びましょう
これでSwiftから Javaライブラリを使えるようになりました 次は JavaKitと JavaApacheCommonsCSVのインポートです 続いて Javaのコードを実行するために SwiftプロセスでJVMを起動します これで Swiftアプリから Javaを使う準備ができました ここでは JDKのFileReaderを使用して 先ほどインポートした CSVライブラリに渡しています さらに 返されたJavaコレクションで SwiftのforEachループを 直接使うこともできます
ここまでで SwiftJavaがGradleと SwiftPMをどのように使って 優れた体験を実現しているかを確認しました Javaソースを一切変更することなく Javaライブラリ全体を インポートできるようになりました ソースで生成されたSwiftコードでは JavaKitのJDKラッパー型を使って ユーザー定義型をシームレスに処理します また Javaオブジェクトの参照を 必要に応じてグローバル参照にすることで JavaKitは Javaオブジェクトの 有効期間の管理をシンプルにしています さて 今日最後に取り上げるのが Swiftライブラリ全体をJavaアプリで 利用できるようにする方法です これが非常に重要なのは 重要なビジネスロジックを Swiftで実装した上で 各種のアプリやサービスで 利用できるようになるからです その際 Swiftを 採用しているかどうかは問いません
先ほど 双方向の相互運用性が 必要であるという話をしました JavaからSwiftへの方向性で 優れた体験を届けられれば 多くのプロジェクトでSwiftの利用が進み Swiftが便利だと 感じてもらうことができます これこそ コードに新しい言語を導入する 重要な社会的側面ではないでしょうか さて Swiftライブラリ全体を 公開するにあたり 先ほどの方法を使用すると Java側で多数のラッパー関数を 書かなければなりません 関数が少数ならそれで良いのですが ライブラリ全体なら 別のアプローチを 検討した方が良いでしょう JavaライブラリをSwiftで 利用する場合と同じように JavaからSwiftの呼び出しも できるだけシームレスかつ簡単にしましょう それには Swiftライブラリの型を全部 Javaクラスでラップした上で Javaライブラリにまとめて配布します 今回 JNIは一切使用しません 代わりに使うのは 新しい Foreign Function and Memory APIです このAPIは昨年3月に安定し Java 22以降で利用できるようになりました このAPIでは ネイティブメモリの制御と ネイティブ呼び出しが改善します 場合によっては JNIの代わりに 使われることもあります この新しいAPIを使うと JavaとSwiftのランタイムとメモリ管理に 非常に密な統合を実現できます このレベルの統合は 他の方法では不可能です その結果 Javaからの ネイティブ呼び出しの 安全性とパフォーマンスが高まります 今回の例では Swiftのstruct型を使います これは これからJavaで使えるようにしたい ビジネスオブジェクトです 値型なので 安定した オブジェクトIDはなく Javaオブジェクトでは表現できません そのため このオブジェクトを Javaで取り扱う際には注意が必要です このオブジェクトには 他に publicプロパティとイニシャライザ さらに いくつかのメソッドがあります この型をJavaで使えるようにするには やはりswift-javaツールを使用します ただし 今回は モードを少し変えて使用します ツールにSwiftの入力パスと 生成後のSwiftとJavaのソースの 出力ディレクトリを指定します すると ツールでは入力パスにある ソースをすべて取得し Swiftの型と関数の アクセサとして機能する Javaクラスを生成します また Swiftのヘルパーコードも 必要なものが生成されます 最後に 動的ライブラリとしてビルドされた Swiftコードなどのすべてが コンパイルされ Javaライブラリとして パッケージ化されます 生成されるJavaクラスは このようになります 元はstructだったので Swiftの値の インターフェイスが実装されています selfというメモリセグメントがあります これはネイティブメモリ内の インスタンスへのポインタのようなものです また publicのイニシャライザや プロパティや関数も 同等のJavaシグネチャで表現されています これらの内部には Foreign Function APIを使って ネイティブ呼び出しをするための 効率的なコードが入っています このJavaアプリでは 生成されたJavaソースを使用できます ネイティブのSwift値の 作成と管理に必要なのが SwiftArenaです これは Swiftオブジェクトの メモリ割り当てと有効期間の管理を担います アリーナが準備できたら 通常のJavaクラスのように Swift値のコンストラクタを 呼び出すだけです せっかくなので もう少し話を続けましょう ネイティブとJavaの メモリリソースの管理についてです まず Javaヒープに新しいJavaラッパー オブジェクトが割り当てられます このオブジェクトは JVMのガベージ コレクタによって管理されます 次に 生成されたコンストラクタが 渡されたSwiftArenaを使って ネイティブヒープ上の Swift値型の インスタンスの割り当てと 初期化を実行します 今回のSwiftyBusiness構造体のような 値型は 通常なら スタックに割り当てられますが ここでは安定した メモリアドレスが必要なので ヒープに割り当てています これで Javaのラッパーオブジェクトから メモリのこのアドレスを安全に参照できます その結果 Javaラッパーは 使われなくなります ラッパーは ガベージコレクタが適宜 収集のうえ破棄します このとき Swift側のネイティブ インスタンスも破棄されます そのため 安全なメモリ管理が実現します もっとも Swiftとは違い このように オブジェクトのファイナライズに依存すると 追加のトラッキングが必要になるため GCに大きな負担がかかります また Swiftのネイティブ値の デイニシャライズのタイミングも 予測できなくなります このパターンは 最初に試すには 簡単で良かったのですが もっと良い方法で ネイティブメモリを管理しましょう 今回は Javaの try-with-resourcesを使用します アリーナのタイプには AutoではなくConfinedを使用します オブジェクトの割り当ての 展開は先ほどと同じですが try-with-resourcesを使用した場合は オブジェクトの破棄方法が変わります 具体的には tryのスコープの終わりに アリーナが閉鎖されます その結果 Javaラッパーオブジェクトの 破棄が開始されます さらにそれが引き金となって Swift値の ネイティブヒープの破棄が開始されます とてもよくなりました オブジェクトのファイナライズで GCに負担がかからないので オブジェクトが大量でも 問題なくなりました また Swiftの多くのプログラムに 共通する 入念に定義され 秩序立ったオブジェクトの デシリアライズも戻ってきました 可能な限り スコープ付きの アリーナを使用してください GCに頼らずに済むので アプリに 最善の動作とパフォーマンスを実現できます このセッションのまとめです swift-javaコマンドラインツールを 1回呼び出すだけで Swiftライブラリ全体をラップできました それをJavaライブラリとして ラップしてビルドのうえ公開し Javaプロジェクトで簡単に 利用できるようにしました これで チームがいっそう Swiftを導入しやすくなります また Foreign Function and Memory APIを 使用すれば Swiftの値型であっても オブジェクトの割り当てと有効期間を 細かく制御できることを確認しました 本日は SwiftとJavaの併用に関する 様々な手法を紹介しました 別々に使っても 一緒に使ってもかまいません プロジェクトのニーズに応じて 適宜活用してみてください SwiftJavaはまだ生まれたばかりで 改善が必要な点も多々残っていますが SwiftとJavaの相互運用に 大いに貢献しています SwiftKitとJavaKitの2つの サポートライブラリを使えば 一方の言語からもう一方の言語を 安全かつ効率的に使えるコードが書けます また JavaKitマクロや swift-javaコマンドラインツールを使えば ボイラープレートを自動で生成できます 加えて その保守も簡単です
最後に このプロジェクトの開発に 参加くださる方を募集しています 開発はSwiftlangのGithubリポジトリで 完全にオープンソースで進めています 対応が必要な課題やアイデアが 多数残っていますので ぜひご協力ください コントリビュートはできないものの SwiftとJavaについて理解を深めたい方や アイデアやフィードバックをくださる方は ぜひSwiftフォーラムにご参加ください ご視聴ありがとうございました 私はこれから コーヒーでも飲もうと思います
-
-
9:05 - Implement JNI native methods in Swift
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() } }
-
12:30 - Resolve Java dependencies with swift-java
swift-java resolve --module-name JavaApacheCommonsCSV
-
13:05 - Use a Java library from Swift
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.")
-
16:22 - Wrap Swift types for Java
swift-java --input-swift Sources/SwiftyBusiness \ --java-package com.example.business \ --output-swift .build/.../outputs/SwiftyBusiness \ --output-java .build/.../outputs/Java ...
-
18:55 - Create Swift objects from Java
try (var arena = SwiftArena.ofConfined()) { var business = new SwiftyBusiness(..., arena); }
-