この記事の要点
- Google Summer of Code(GSoC)2025 の Swift プロジェクトの成果を紹介するシリーズの第 2 弾で、Mads Odgaard さんが取り組んだ「Swift/Java 相互運用の拡張」を取り上げています。
- swift-java の
swift-java jextractツールに、これまでの FFM(Foreign Function and Memory API)モードに加えて新しい JNI(Java Native Interface)モードが追加されました。 - FFM モードは JDK 22 以上を必要とし Android などでは使えませんでしたが、JNI モードはより広いプラットフォームで動作します。当初目標だった FFM モードとの機能同等にとどまらず、
enumやプロトコルといった追加の Swift 言語機能にも対応しました。
プログラムの目的
GSoC は、メンターと組みながらオープンソースプロジェクトへコントリビューションすることで、新しい開発者をオープンソース開発に導くことを目的とした長期のメンターシッププログラムです。2025 年も Swift プロジェクトが参加し、複数のプロジェクトが実施されました。この記事はそのシリーズの第 2 弾で、Mads Odgaard さんが Konrad Malawski さんのメンターのもとで取り組んだ、Swift/Java 相互運用の拡張を紹介しています。
swift-java は Swift と Java の相互運用を担うライブラリで、その swift-java jextract ツールは、Java から Swift のコードを呼び出すための Java ソースを自動生成します。このツールはこれまで FFM(Foreign Function and Memory API)のみに対応しており、FFM は JDK 22 以上を必要とするため Android などのプラットフォームでは利用できませんでした。このプロジェクトの目標は、FFM の代わりに JNI を使う Java ソースも生成できるよう jextract を拡張し、より多くのプラットフォームで Swift/Java 相互運用を使えるようにすることでした。
成果
JNI モードの追加
このプロジェクトにより、swift-java jextract に JNI モードが追加されました。次のように --mode jni を指定すると、Swift ライブラリに対する Java のラッパーが JNI で生成され、Android などのプラットフォームでも利用できるようになります。
swift-java jextract --swift-module MySwiftLibrary \
--mode jni \
--input-swift Sources/MySwiftLibrary \
--output-java out/java \
--output-swift out/swift
当初の目標は FFM モードとの機能同等でしたが、最終的には enum やプロトコルといった追加の Swift 言語機能にも対応しています。なお、この JNI モードは Swift SDK for Android の発表記事でも実際に使われています。
生成されるコードの仕組み
各 Swift の class / struct は 1 つの Java の class として抽出されます。関数や変数は Java のメソッドとして生成され、その内部で @_cdecl を使って Swift 側に実装された native メソッドを呼び出します。たとえば、次のような Swift のコードがあるとします。
public class MySwiftClass {
public let x: Int64
public init(x: Int64) {
self.x = x
}
public func printMe() {
print("\(self.x)")
}
}
これは、おおよそ次のような Java の class に変換されます。プロパティやメソッドが Java のメソッドになり、それぞれが native メソッドを介して Swift の実装を呼び出します。
public final class MySwiftClass implements JNISwiftInstance {
public static MySwiftClass init(long x, SwiftArena swiftArena$) {
return MySwiftClass.wrapMemoryAddressUnsafe(MySwiftClass.$init(x), swiftArena$);
}
public long getX() {
return MySwiftClass.$getX(this.$memoryAddress());
}
public void printMe() {
MySwiftClass.$printMe(this.$memoryAddress());
}
private static native long $init(long x);
private static native long $getX(long self);
private static native void $printMe(long self);
}
加えて、これらの native メソッドを実装し、対応する Swift のメソッドを実際に呼び出すための Swift の thunk も生成されます。
JVM と Swift のメモリ管理
相互運用ライブラリで興味深いのは、JVM 側と Swift 側のメモリ管理です。FFM モードは MemorySegment まわりの FFM API でネイティブメモリを確保・管理します。一方 JNI には同等の仕組みがないため、このプロジェクトではメモリ確保の責務を Swift 側に移しました。これにより、FFM モードで必要だった witness table の複雑さを扱わずに済むという利点が得られました。
たとえば Swift の class をラップする際は、生成された native メソッド $init を呼び出します。これは Swift 側のメモリ空間にあるインスタンスへのポインタを long として返し、その値を wrapMemoryAddressUnsafe に渡してラッパーのフィールドに保持しつつ SwiftArena に登録します。
SwiftArena は、Java のラッパーが不要になったときに最終的にメモリを解放するための型で、次の 2 種類があります。
SwiftArena.ofConfined(): try-with-resources と組み合わせて使い、スコープの終わりですべてのインスタンスを解放する confined なアリーナを返します。SwiftArena.ofAuto(): ガベージコレクタが判断したタイミングでインスタンスを解放するアリーナを返します。
Swift 側の $init の実装では、MySwiftClass 1 個分のメモリを確保し、新しいインスタンスで初期化したうえで、そのポインタのメモリアドレスを返します。struct でも同じ方式が使われます。
// Generated code, not something you would write
@_cdecl("Java_com_example_swift_MySwiftClass__00024init__J")
func Java_com_example_swift_MySwiftClass__00024init__J(environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass, x: jlong) -> jlong {
let result$ = UnsafeMutablePointer<MySwiftClass>.allocate(capacity: 1)
result$.initialize(to: MySwiftClass.init(x: Int64(fromJNI: x, in: environment!)))
let resultBits$ = Int64(Int(bitPattern: result$))
return resultBits$.getJNIValue(in: environment!)
}
学び・今後
Mads さんは、GSoC を通じてお気に入りの言語で実用的なライブラリに取り組めたことを振り返り、オープンソースコミュニティと関わり、スキルを伸ばし、優秀な人々と働く良い機会だったとして GSoC を勧めています。あわせて、一見すると恥ずかしく思えるような質問でも臆せず尋ねることが、有益な議論や解決につながるとアドバイスしています。
この JNI モードはすでに Swift SDK for Android でも活用されており、Swift/Java 相互運用を Android を含むより広いプラットフォームへ広げる土台となっています。