How It Works
swift4j bridges Swift and the JVM through JNI (Java Native Interface) — the standard mechanism for calling native code from Java and Kotlin. swift4j automates the two things that make JNI painful: writing the native glue code on the Swift side, and writing the proxy classes on the JVM side.
The two sides
Section titled “The two sides”Swift side — @jvm macro
When you apply @jvm to a Swift type, the macro expands at compile time and adds JNI registration code directly to your type. This code runs once when the library is loaded and tells the JVM runtime where to find each exported method. No separate bridging file is needed — the registration is embedded in your Swift module.
JVM side — generated proxy classes
The generate-java-bridging SPM plugin reads your Swift source files and generates a Kotlin (or Java) proxy class for each @jvm-annotated type. Each proxy holds an opaque pointer to the corresponding Swift object and delegates every method call to the native stubs registered by the macro.
Object lifecycle
Section titled “Object lifecycle”Swift objects are reference-counted by ARC as usual. On the JVM side, each proxy holds a SwiftPtr — a JNI global reference that keeps the Swift object alive. When the JVM garbage-collects the proxy, the finalizer releases the SwiftPtr, which decrements the ARC retain count and allows the Swift object to be deallocated normally.
This means Swift destructors (deinit) run as expected, but at JVM GC time rather than at Swift scope exit.
Build output
Section titled “Build output”Your Swift package compiles to a dynamic library (.so on Linux/Android, .dylib on macOS). This library is what the JVM loads with System.loadLibrary(). The generated proxy classes are plain Kotlin/Java source files that you include in your JVM or Android project — they contain no native code themselves.