|
2 | 2 |
|
3 | 3 | The `swift-java` command line tool offers multiple ways to interact your Java interoperability enabled projects. |
4 | 4 |
|
5 | | -## Generating Swift bindings to Java libraries |
| 5 | +## swift-java |
6 | 6 |
|
7 | | -The `swift-java wrap-java` command. |
| 7 | +The `swift-java` command line tool offers multiple modes which you can use to prepare your Swift and Java code to interact with eachother. |
| 8 | + |
| 9 | +The following sections will explain the modes in depth. When in doubt, you can always use the command line `--help` to get additional |
| 10 | +guidance about the tool and available options: |
| 11 | + |
| 12 | +```bash |
| 13 | +> swift-java --help |
| 14 | + |
| 15 | +USAGE: swift-java <subcommand> |
| 16 | + |
| 17 | +OPTIONS: |
| 18 | + -h, --help Show help information. |
| 19 | + |
| 20 | +SUBCOMMANDS: |
| 21 | + configure Configure and emit a swift-java.config file based on an input dependency or jar file |
| 22 | + resolve Resolve dependencies and write the resulting swift-java.classpath file |
| 23 | + wrap-java Wrap Java classes with corresponding Swift bindings. |
| 24 | + jextract Wrap Swift functions and types with Java bindings, making them available to be called from Java |
| 25 | + |
| 26 | + See 'swift-java help <subcommand>' for detailed help. |
| 27 | +``` |
| 28 | + |
| 29 | +## Expose Java classes to Swift: swift-java wrap-java |
| 30 | + |
| 31 | +The `swift-java` is a Swift program that uses Java's runtime reflection facilities to translate the requested Java classes into their Swift projections. The output is a number of Swift source files, each of which corresponds to a |
| 32 | +single Java class. The `swift-java` can be executed like this: |
| 33 | + |
| 34 | +``` |
| 35 | +swift-java help wrap-java |
| 36 | +``` |
| 37 | + |
| 38 | +to produce help output like the following: |
| 39 | + |
| 40 | +``` |
| 41 | +USAGE: swift-java wrap-java [--output-directory <output-directory>] [--input-swift <input-swift>] [--log-level <log-level>] [--cp <cp> ...] [--filter-java-package <filter-java-package>] --swift-module <swift-module> [--depends-on <depends-on> ...] [--swift-native-implementation <swift-native-implementation> ...] [--cache-directory <cache-directory>] [--swift-match-package-directory-structure <swift-match-package-directory-structure>] <input> |
| 42 | +
|
| 43 | +ARGUMENTS: |
| 44 | + <input> Path to .jar file whose Java classes should be wrapped using Swift bindings |
| 45 | +
|
| 46 | +OPTIONS: |
| 47 | + -o, --output-directory <output-directory> |
| 48 | + The directory in which to output generated SwiftJava configuration files. |
| 49 | + --input-swift <input-swift> |
| 50 | + Directory containing Swift files which should be extracted into Java bindings. Also known as 'jextract' mode. Must be paired with --output-java and --output-swift. |
| 51 | + -l, --log-level <log-level> |
| 52 | + Configure the level of logs that should be printed (values: trace, debug, info, notice, warning, error, critical; default: log level) |
| 53 | + --cp, --classpath <cp> Class search path of directories and zip/jar files from which Java classes can be loaded. |
| 54 | + -f, --filter-java-package <filter-java-package> |
| 55 | + While scanning a classpath, inspect only types included in this package |
| 56 | + --swift-module <swift-module> |
| 57 | + The name of the Swift module into which the resulting Swift types will be generated. |
| 58 | + --depends-on <depends-on> |
| 59 | + A swift-java configuration file for a given Swift module name on which this module depends, |
| 60 | + e.g., JavaKitJar=Sources/JavaKitJar/Java2Swift.config. There should be one of these options |
| 61 | + for each Swift module that this module depends on (transitively) that contains wrapped Java sources. |
| 62 | + --swift-native-implementation <swift-native-implementation> |
| 63 | + The names of Java classes whose declared native methods will be implemented in Swift. |
| 64 | + --cache-directory <cache-directory> |
| 65 | + Cache directory for intermediate results and other outputs between runs |
| 66 | + --swift-match-package-directory-structure <swift-match-package-directory-structure> |
| 67 | + Match java package directory structure with generated Swift files (default: false) |
| 68 | + -h, --help Show help information. |
| 69 | +
|
| 70 | +``` |
| 71 | + |
| 72 | +For example, the `JavaKitJar` library is generated with this command line: |
| 73 | + |
| 74 | +```swift |
| 75 | +swift-java wrap-java --swift-module JavaKitJar --depends-on JavaKit=Sources/JavaKit/swift-java.config -o Sources/JavaKitJar/generated Sources/JavaKitJar/swift-java.config |
| 76 | +``` |
| 77 | + |
| 78 | +The `--swift-module JavaKitJar` parameter describes the name of the Swift module in which the code will be generated. |
| 79 | + |
| 80 | +The `--depends-on` option is followed by the swift-java configuration files for any library on which this Swift library depends. Each `--depends-on` option is of the form `<swift library name>=<swift-java.config path>`, and tells swift-java which other Java classes have already been translated to Swift. For example, if your Java class uses `java.net.URL`, then you should include |
| 81 | +`JavaKitNetwork`'s configuration file as a dependency here. |
| 82 | + |
| 83 | +The `-o` option specifies the output directory. Typically, this will be `Sources/<module name>/generated` or similar to keep the generated Swift files separate from any hand-written ones. To see the output on the terminal rather than writing files to disk, pass `-` for this option. |
| 84 | + |
| 85 | +Finally, the command line should contain the `swift-java.config` file containing the list of classes that should be translated into Swift and their corresponding Swift type names. The tool will output a single `.swift` file for each class, along with warnings for any public API that cannot be translated into Swift. The most common warnings are due to missing Swift projections for Java classes. For example, here we have not translated (or provided the translation manifests for) the Java classes |
| 86 | +`java.util.zip.ZipOutputStream` and `java.io.OutputStream`: |
| 87 | + |
| 88 | +``` |
| 89 | +warning: Unable to translate 'java.util.jar.JarOutputStream' superclass: Java class 'java.util.zip.ZipOutputStream' has not been translated into Swift |
| 90 | +warning: Unable to translate 'java.util.jar.JarOutputStream' constructor: Java class 'java.io.OutputStream' has not been translated into Swift |
| 91 | +warning: Unable to translate 'java.util.jar.JarInputStream' method 'transferTo': Java class 'java.io.OutputStream' has not been translated into Swift |
| 92 | +``` |
| 93 | + |
| 94 | +The result of such warnings is that certain information won't be statically available in Swift, e.g., the superclass won't be known (so we will assume it is `JavaObject`), or the specified constructors or methods won't be translated. If you don't need these APIs, the warnings can be safely ignored. The APIs can still be called dynamically via JNI. |
| 95 | + |
| 96 | +The `--jar` option changes the operation of `swift-java`. Instead of wrapping Java classes in Swift, it scans the given input Jar file to find all public classes and outputs a configuration file `swift-java.config` mapping all of the Java classes in the Jar file to Swift types. The `--jar` mode is expected to be used to help import a Java library into Swift wholesale, after which swift-java should invoked again given the generated configuration file. |
| 97 | + |
| 98 | +### Under construction: Create a Java class to wrap the Swift library |
| 99 | + |
| 100 | +**NOTE**: the instructions here work, but we are still smoothing out the interoperability story. |
| 101 | + |
| 102 | +All JavaKit-based applications start execution within the Java Virtual Machine. First, define your own Java class that loads your native Swift library and provides a `native` entry point to get into the Swift code. Here is a minimal Java class that has all of the program's logic written in Swift, including `main`: |
| 103 | + |
| 104 | + |
| 105 | +```java |
| 106 | +package org.swift.javakit; |
| 107 | + |
| 108 | +public class HelloSwiftMain { |
| 109 | + static { |
| 110 | + System.loadLibrary("HelloSwift"); |
| 111 | + } |
| 112 | + |
| 113 | + public native static void main(String[] args); |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +Compile this into a `.class` file with `javac` before we build the Swift half, e.g.,: |
| 118 | + |
| 119 | +``` |
| 120 | +javac Java/src/org/swift/javakit/JavaClassTranslator.java |
| 121 | +``` |
| 122 | + |
| 123 | +### Create a Swift library |
| 124 | + |
| 125 | +The Java class created above loads a native library `HelloSwift` that needs to contain a definition of the `main` method in the class `org.swift.javakit.HelloSwiftMain`. `HelloSwift` should be defined as a SwiftPM dynamic library product, e.g., |
| 126 | + |
| 127 | +```swift |
| 128 | + products: [ |
| 129 | + .library( |
| 130 | + name: "HelloSwift", |
| 131 | + type: .dynamic, |
| 132 | + targets: ["HelloSwift"] |
| 133 | + ), |
| 134 | + ] |
| 135 | +``` |
| 136 | + |
| 137 | +with an associated target that depends on `JavaKit`: |
| 138 | + |
| 139 | +```swift |
| 140 | + .target( |
| 141 | + name: "HelloSwift", |
| 142 | + dependencies: [ |
| 143 | + .product(name: "ArgumentParser", package: "swift-argument-parser"), |
| 144 | + .product(name: "JavaKit", package: "JavaKit") |
| 145 | + ]) |
| 146 | +``` |
| 147 | + |
| 148 | +### Implement the `native` Java method in Swift |
| 149 | +Now, in the `HelloSwift` Swift library, define a `struct` that provides the `main` method for the Java class we already defined: |
| 150 | + |
| 151 | +```swift |
| 152 | +import JavaKit |
| 153 | + |
| 154 | +@JavaImplementation("org.swift.javakit.HelloSwiftMain") |
| 155 | +struct HelloSwiftMain { |
| 156 | + @JavaStaticMethod |
| 157 | + static func main(arguments: [String], environment: JNIEnvironment? = nil) { |
| 158 | + print("Command line arguments are: \(arguments)") |
| 159 | + } |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +Go ahead and build this library with `swift build`, and find the path to the directory containing the resulting shared library (e.g., `HelloSwift.dylib`, `HelloSwift.so`, or `HelloSwift.dll`, depending on platform). It is often in `.build/debug/` if you ran `swift build` on the command line. |
| 164 | + |
| 165 | +### Putting it all together! |
| 166 | + |
| 167 | +Finally, run this program on the command line like this: |
| 168 | + |
| 169 | +``` |
| 170 | +java -cp Java/src -Djava.library.path=$(PATH_CONTAINING_HELLO_SWIFT)/ org.swift.javakit.HelloSwiftMain -v argument |
| 171 | +``` |
| 172 | + |
| 173 | +This will prints the command-line arguments `-v` and `argument` as seen by Swift. |
| 174 | + |
| 175 | +### Bonus: Swift argument parser |
| 176 | + |
| 177 | +The easiest way to build a command-line program in Swift is with the [Swift argument parser library](https://github.com/apple/swift-argument-parser). We can extend our `HelloSwiftMain` type to conform to `ParsableCommand` and using the Swift argument parser to process the arguments provided by Java: |
| 178 | + |
| 179 | +```swift |
| 180 | +import ArgumentParser |
| 181 | +import JavaKit |
| 182 | + |
| 183 | +@JavaClass("org.swift.javakit.HelloSwiftMain") |
| 184 | +struct HelloSwiftMain: ParsableCommand { |
| 185 | + @Option(name: .shortAndLong, help: "Enable verbose output") |
| 186 | + var verbose: Bool = false |
| 187 | + |
| 188 | + @JavaImplementation |
| 189 | + static func main(arguments: [String], environment: JNIEnvironment? = nil) { |
| 190 | + let command = Self.parseOrExit(arguments) |
| 191 | + command.run(environment: environment) |
| 192 | + } |
| 193 | + |
| 194 | + func run(environment: JNIEnvironment? = nil) { |
| 195 | + print("Verbose = \(verbose)") |
| 196 | + } |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +## Download Java dependencies in Swift builds: swift-java resolve |
| 201 | + |
| 202 | +> TIP: See the `Samples/DependencySampleApp` for a fully functional showcase of this mode. |
| 203 | +
|
| 204 | +TODO: documentation on this feature |
| 205 | + |
| 206 | +## Expose Swift code to Java: swift-java jextract |
| 207 | + |
| 208 | +The project is still very early days, however the general outline of using this approach is as follows: |
| 209 | + |
| 210 | +- **No code changes** need to be made to Swift libraries that are to be exposed to Java using jextract-swift. |
| 211 | +- Swift sources are compiled to `.swiftinterface` files |
| 212 | +- These `.swiftinterface` files are imported by jextract-swift which generates `*.java` files |
| 213 | +- The generated Java files contain generated code for efficient native invocations. |
| 214 | + |
| 215 | +You can then use Swift libraries in Java just by calling the appropriate methods and initializers. |
| 216 | + |
| 217 | +### Generating Java bindings for Swift libraries |
| 218 | + |
| 219 | +This repository also includes the `jextract-swift` tool which is similar to the JDK's [`jextract`](https://github.com/openjdk/jextract/). |
| 220 | + |
| 221 | +This approach offers two modes of operation: |
| 222 | + |
| 223 | +- the default `--mode ffm` which uses the [JEP-424 Foreign function and Memory APIs](https://openjdk.org/jeps/424) which are available since JDK **22**. It promises much higher performance than traditional approaches using JNI, and is primarily aimed for calling native code from a Java application. |
| 224 | + |
| 225 | +> Tip: In order to use the ffm mode, you need to install a recent enough JDK (at least JDK 22). The recommended, and simplest way, to install the a JDK distribution of your choice is [sdkman](https://sdkman.io): |
| 226 | +> |
| 227 | +> ``` |
| 228 | +> curl -s "https://get.sdkman.io" | bash |
| 229 | +> sdk install java 22-open |
| 230 | +> |
| 231 | +> export JAVA_HOME=$(sdk home java 22-open) |
| 232 | +> ``` |
| 233 | +
|
| 234 | +`jextract-swift` can be pointed at `*.swiftinterface` files and will generate corresponding Java files that use the (new in Java 22) Foreign Function & Memory APIs to expose efficient ways to call "down" into Swift from Java. |
| 235 | +
|
| 236 | +## Default jextract behaviors |
| 237 | +
|
| 238 | +Only `public` functions, properties and types are imported. |
| 239 | +
|
| 240 | +Global Swift functions become static functions on on a class with the same name as the Swift module in Java, |
| 241 | +
|
| 242 | +```swift |
| 243 | +// Swift (Sources/SomeModule/Example.swift) |
| 244 | + |
| 245 | +public func globalFunction() |
| 246 | +``` |
| 247 | +
|
| 248 | +becomes: |
| 249 | + |
| 250 | +```java |
| 251 | +// Java (SomeModule.java) |
| 252 | + |
| 253 | +public final class SomeModule ... { |
| 254 | + public static void globalFunction() { ... } |
| 255 | +} |
| 256 | +``` |
0 commit comments