diff --git a/.github/actions/prepare_env/action.yml b/.github/actions/prepare_env/action.yml index 8fc986e0..53bb3fd7 100644 --- a/.github/actions/prepare_env/action.yml +++ b/.github/actions/prepare_env/action.yml @@ -26,9 +26,9 @@ runs: elif [[ -n "$JAVA_HOME_24_ARM64" ]]; then echo "JAVA_HOME_24=$JAVA_HOME_24_ARM64" >> $GITHUB_ENV fi - - name: Check Java environment - shell: bash - run: ./gradlew -q javaToolchains + # - name: Check Java environment + # shell: bash + # run: ./gradlew -q javaToolchains - name: Cache local SwiftPM repository if: matrix.os_version == 'jammy' uses: actions/cache@v4 diff --git a/.github/scripts/install_swiftly.sh b/.github/scripts/install_swiftly.sh index 78fa3f6b..e0792894 100755 --- a/.github/scripts/install_swiftly.sh +++ b/.github/scripts/install_swiftly.sh @@ -41,6 +41,7 @@ else curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg && pkgutil --check-signature swiftly.pkg && pkgutil --verbose --expand swiftly.pkg "${SWIFTLY_HOME_DIR}" && tar -C "${SWIFTLY_HOME_DIR}" -xvf "${SWIFTLY_HOME_DIR}"/swiftly-*/Payload && "$SWIFTLY_HOME_DIR/bin/swiftly" init -y --skip-install + chmod +x "$SWIFTLY_HOME_DIR/env.sh" # shellcheck disable=SC1091 . "$SWIFTLY_HOME_DIR/env.sh" fi @@ -84,5 +85,8 @@ echo "Displaying swift version" swiftly run "${runSelector[@]}" swift --version if [[ "$(uname -s)" == "Linux" ]]; then - CC=clang swiftly run "${runSelector[@]}" "$(dirname "$0")/install-libarchive.sh" + if [[ -f "$(dirname "$0")/install-libarchive.sh" ]]; then + CC=clang swiftly run "${runSelector[@]}" "$(dirname "$0")/install-libarchive.sh" + fi fi + diff --git a/.github/scripts/validate_docs.sh b/.github/scripts/validate_docs.sh new file mode 100755 index 00000000..8ee777b0 --- /dev/null +++ b/.github/scripts/validate_docs.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e +set -x + +cat <> Package.swift + +package.dependencies.append( + .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0") +) +EOF + +swift package --disable-sandbox plugin generate-documentation --target "SwiftJavaDocumentation" --warnings-as-errors --analyze diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 9a0b59b8..b2338ed2 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -13,6 +13,31 @@ jobs: # FIXME: Something is off with the format task and it gets "stuck", need to investigate format_check_enabled: false license_header_check_project_name: Swift.org + # Since we need JAVA_HOME to be set up for building the project and depending on a different workflow won't + # give us that, we disable the checking and make a separate job that performs docs validation + docs_check_enabled: false + + # This replicates 'docs-check' from https://github.com/swiftlang/github-workflows/blob/main/.github/workflows/soundness.yml + # because we need to set up environment so we can build the SwiftJava project (including Java runtime/dependencies). + soundness-docs: + name: Documentation check + runs-on: ubuntu-latest + container: + image: 'swift:6.1-noble' + strategy: + fail-fast: true + matrix: + swift_version: ['6.1.2'] + os_version: ['jammy'] + jdk_vendor: ['corretto'] + steps: + - uses: actions/checkout@v4 + - name: Prepare CI Environment + uses: ./.github/actions/prepare_env + - name: Swift Build + run: swift build + - name: Run documentation check + run: ./.github/scripts/validate_docs.sh test-java: name: Test (Java) (${{ matrix.os_version }} swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}}) diff --git a/.gitignore b/.gitignore index f77390a7..28dfbae2 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ Package.resolved */**/*.o */**/*.swiftdeps */**/*.swiftdeps~ +*/**/.docc-build/ diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 00000000..58345f25 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,8 @@ +version: 1 +builder: + configs: + - documentation_targets: [ + SwiftJavaDocumentation, + JavaKit, + SwiftKitSwift + ] diff --git a/Package.swift b/Package.swift index 81d53b1d..ef529496 100644 --- a/Package.swift +++ b/Package.swift @@ -139,6 +139,12 @@ let package = Package( name: "swift-java", targets: ["SwiftJavaTool"] ), + + + .library( + name: "SwiftJavaDocumentation", + targets: ["SwiftJavaDocumentation"] + ), // ==== Plugin for building Java code .plugin( @@ -198,6 +204,14 @@ let package = Package( .package(url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.4.0")), ], targets: [ + .target( + name: "SwiftJavaDocumentation", + dependencies: [ + "JavaKit", + "SwiftKitSwift", + ] + ), + .macro( name: "JavaKitMacros", dependencies: [ diff --git a/USER_GUIDE.md b/Sources/JavaKit/Documentation.docc/JavaKit.md similarity index 62% rename from USER_GUIDE.md rename to Sources/JavaKit/Documentation.docc/JavaKit.md index 351a3d5c..fe0f50ba 100644 --- a/USER_GUIDE.md +++ b/Sources/JavaKit/Documentation.docc/JavaKit.md @@ -2,11 +2,7 @@ Library and tools to make it easy to use Java libraries from Swift using the Java Native Interface (JNI). -## Getting started - -Before using this package, set the `JAVA_HOME` environment variable to point at your Java installation. Failing to do so will produce errors when processing the package manifest. Alternatively, you can put the path to your Java installation in the file `~/.java_home`. - -### Using Java libraries from Swift +## JavaKit: Using Java libraries from Swift Existing Java libraries can be wrapped for use in Swift with the `swift-java` tool. In a Swift program, the most direct way to access a Java API is to use the SwiftPM plugin to provide Swift wrappers for the Java classes. To do so, add a configuration file `swift-java.config` into the source directory for the Swift target. This is a JSON file that specifies Java classes and the Swift type name that should be generated to wrap them. For example, the following file maps `java.math.BigInteger` to a Swift type named `BigInteger`: @@ -282,7 +278,7 @@ Java native methods that throw any checked exception should be marked as `throws The Swift implementations of Java `native` constructors and static methods require an additional Swift parameter `environment: JNIEnvironment? = nil`, which will receive the JNI environment in which the function is being executed. In case of nil, the `JavaVirtualMachine.shared().environment()` value will be used. -## Using Java libraries from Swift +## JavaKit: Using Java libraries from Swift This section describes how Java libraries and mapped into Swift and their use from Swift. @@ -420,7 +416,7 @@ extension JavaClass { } ``` -### Interfaces +### Java Interfaces Java interfaces are similar to classes, and are projected into Swift in much the same way, but with the macro `JavaInterface`. The `JavaInterface` macro takes the Java interface name as well as any Java interfaces that this interface extends. As an example, here is the Swift projection of the [`java.util.Enumeration`](https://docs.oracle.com/javase/8/docs/api/java/util/Enumeration.html) generic interface: @@ -437,247 +433,3 @@ public struct Enumeration { public func nextElement() -> JavaObject! } ``` - -## Translating Java classes with `swift-java` - -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 -single Java class. The `swift-java` can be executed like this: - -``` -swift-java -``` - -to produce help output like the following: - -``` -OVERVIEW: Generate sources and configuration for Swift and Java interoperability. - -USAGE: swift-java - -OPTIONS: - --depends-on - A Java2Swift configuration file for a given Swift module name on which this module depends, e.g., JavaKitJar=Sources/JavaKitJar/Java2Swift.config. There should be one of these options for each Swift module that this - module depends on (transitively) that contains wrapped Java sources. - --jar Specifies that the input is a Jar file whose public classes will be loaded. The output of Java2Swift will be a configuration file (Java2Swift.config) that can be used as input to a subsequent Java2Swift invocation to - generate wrappers for those public classes. - --fetch Fetch dependencies from given target (containing swift-java configuration) or dependency string - --swift-native-implementation - The names of Java classes whose declared native methods will be implemented in Swift. - --output-swift - The directory where generated Swift files should be written. Generally used with jextract mode. - --output-java - The directory where generated Java files should be written. Generally used with jextract mode. - --java-package - The Java package the generated Java code should be emitted into. - -c, --cache-directory - Directory where to write cached values (e.g. swift-java.classpath files) - -o, --output-directory - The directory in which to output the generated Swift files or the SwiftJava configuration file. - --input-swift - 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. - -l, --log-level - Configure the level of logs that should be printed (values: trace, debug, info, notice, warning, error, critical; default: log level) - --cp, --classpath Class search path of directories and zip/jar files from which Java classes can be loaded. - -f, --filter-java-package - While scanning a classpath, inspect only types included in this package - -h, --help Show help information. - -SUBCOMMANDS: - configure Configure and emit a swift-java.config file based on an input dependency or jar file - resolve Resolve dependencies and write the resulting swift-java.classpath file - - See 'swift-java help ' for detailed help. -``` - -For example, the `JavaKitJar` library is generated with this command line: - -```swift -swift run swift-java --module-name JavaKitJar --depends-on JavaKit=Sources/JavaKit/swift-java.config -o Sources/JavaKitJar/generated Sources/JavaKitJar/swift-java.config -``` - -The `--swift-module JavaKitJar` parameter describes the name of the Swift module in which the code will be generated. - -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 `=`, 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 -`JavaKitNetwork`'s configuration file as a dependency here. - -The `-o` option specifies the output directory. Typically, this will be `Sources//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. - -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 -`java.util.zip.ZipOutputStream` and `java.io.OutputStream`: - -``` -warning: Unable to translate 'java.util.jar.JarOutputStream' superclass: Java class 'java.util.zip.ZipOutputStream' has not been translated into Swift -warning: Unable to translate 'java.util.jar.JarOutputStream' constructor: Java class 'java.io.OutputStream' has not been translated into Swift -warning: Unable to translate 'java.util.jar.JarInputStream' method 'transferTo': Java class 'java.io.OutputStream' has not been translated into Swift -``` - -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. - -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. - -### Under construction: Create a Java class to wrap the Swift library - -**NOTE**: the instructions here work, but we are still smoothing out the interoperability story. - -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`: - - -```java -package org.swift.javakit; - -public class HelloSwiftMain { - static { - System.loadLibrary("HelloSwift"); - } - - public native static void main(String[] args); -} -``` - -Compile this into a `.class` file with `javac` before we build the Swift half, e.g.,: - -``` -javac Java/src/org/swift/javakit/JavaClassTranslator.java -``` - -### Create a Swift library - -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., - -```swift - products: [ - .library( - name: "HelloSwift", - type: .dynamic, - targets: ["HelloSwift"] - ), - ] -``` - -with an associated target that depends on `JavaKit`: - -```swift - .target( - name: "HelloSwift", - dependencies: [ - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "JavaKit", package: "JavaKit") - ]) -``` - -### Implement the `native` Java method in Swift -Now, in the `HelloSwift` Swift library, define a `struct` that provides the `main` method for the Java class we already defined: - -```swift -import JavaKit - -@JavaImplementation("org.swift.javakit.HelloSwiftMain") -struct HelloSwiftMain { - @JavaStaticMethod - static func main(arguments: [String], environment: JNIEnvironment? = nil) { - print("Command line arguments are: \(arguments)") - } -} -``` - -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. - -### Putting it all together! - -Finally, run this program on the command line like this: - -``` -java -cp Java/src -Djava.library.path=$(PATH_CONTAINING_HELLO_SWIFT)/ org.swift.javakit.HelloSwiftMain -v argument -``` - -This will prints the command-line arguments `-v` and `argument` as seen by Swift. - -### Bonus: Swift argument parser - -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: - -```swift -import ArgumentParser -import JavaKit - -@JavaClass("org.swift.javakit.HelloSwiftMain") -struct HelloSwiftMain: ParsableCommand { - @Option(name: .shortAndLong, help: "Enable verbose output") - var verbose: Bool = false - - @JavaImplementation - static func main(arguments: [String], environment: JNIEnvironment? = nil) { - let command = Self.parseOrExit(arguments) - command.run(environment: environment) - } - - func run(environment: JNIEnvironment? = nil) { - print("Verbose = \(verbose)") - } -} -``` - -# `jextract-swift` - -The project is still very early days, however the general outline of using this approach is as follows: - -- **No code changes** need to be made to Swift libraries that are to be exposed to Java using jextract-swift. -- Swift sources are compiled to `.swiftinterface` files -- These `.swiftinterface` files are imported by jextract-swift which generates `*.java` files -- The generated Java files contain generated code for efficient native invocations. - -You can then use Swift libraries in Java just by calling the appropriate methods and initializers. - -## `jextract-swift`: Generating Java bridging files - -This repository also includes the `jextract-swift` tool which is similar to the JDK's [`jextract`](https://github.com/openjdk/jextract/). - -This approach is using Java's most recent (stable in JDK22) Foreign function and Memory APIs, collectively known as "Project Panama". You can read more about it here: https://openjdk.org/projects/panama/ It promises much higher performance than traditional approaches using JNI, and is primarily aimed for calling native code from a Java application. - -:warning: This feature requires JDK 22. The recommended way to install/manage JDKs is using [sdkman](https://sdkman.io): - -``` -curl -s "https://get.sdkman.io" | bash -sdk install java 22-open - -export JAVA_HOME=$(sdk home java 22-open) -``` - -`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. - -## JExtract: Swift <-> Java Type mapping - -TODO: these are not implemented yet. - -### Closures and Callbacks - -A Swift function may accept a closure which is used as a callback: - -```swift -func callMe(maybe: () -> ()) {} -``` - -Minimal support for c-compatible closures is implemented, more documentation soon. - - -## `jextract-swift` importer behavior - -Only `public` functions, properties and types are imported. - -Global Swift functions become static functions on on a class with the same name as the Swift module in Java, - -```swift -// Swift (Sources/SomeModule/Example.swift) - -public func globalFunction() -``` - -becomes: - -```java -// Java (SomeModule.java) - -public final class SomeModule ... { - public static void globalFunction() { ... } -} -``` diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md new file mode 100644 index 00000000..dc2c7b0e --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -0,0 +1,94 @@ +# Supported Features + +Summary of features supported by the swift-java interoperability libraries and tools. + +## Overview + +JavaKit supports both directions of interoperability, using Swift macros and source generation +(via the `swift-java wrap-java` command). + +### Java -> Swift + +It is possible to use JavaKit macros and the `wrap-java` command to simplify implementing +Java `native` functions. JavaKit simplifies the type conversions + +> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' +> around the [7-minute mark](https://youtu.be/QSHO-GUGidA?si=vUXxphTeO-CHVZ3L&t=448). + +| Feature | Macro support | +|--------------------------------------------------|-------------------------| +| Java `static native` method implemented by Swift | ✅ `@JavaImplementation` | +| **This list is very work in progress** | | + +### Swift -> Java + + +> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' +> around the [10-minute mark](https://youtu.be/QSHO-GUGidA?si=QyYP5-p2FL_BH7aD&t=616). + +| Java Feature | Macro support | +|----------------------------------------|---------------| +| Java `class` | ✅ | +| Java class inheritance | ✅ | +| Java `abstract class` | TODO | +| Java `enum` | ❌ | +| Java methods: `static`, member | ✅ `@JavaMethod` | +| **This list is very work in progress** | | + + +### JExtract: Java -> Swift + +SwiftJava's `swift-java jextract` tool automates generating Java bindings from Swift sources. + +> tip: This direction of interoperability is covered in the WWDC2025 session 'Explore Swift and Java interoperability' +> around the [14-minute mark](https://youtu.be/QSHO-GUGidA?si=b9YUwAWDWFGzhRXN&t=842). + + +| Swift Feature | FFM | JNI | +|--------------------------------------------------------------------------------------| -------- |-----| +| Initializers: `class`, `struct` | ✅ | ✅ | +| Optional Initializers / Throwing Initializers | ❌ | ❌ | +| Deinitializers: `class`, `struct` | ✅ | ✅ | +| `enum`, `actor` | ❌ | ❌ | +| Global Swift `func` | ✅ | ✅ | +| Class/struct member `func` | ✅ | ✅ | +| Throwing functions: `func x() throws` | ❌ | ✅ | +| Typed throws: `func x() throws(E)` | ❌ | ❌ | +| Stored properties: `var`, `let` (with `willSet`, `didSet`) | ✅ | ✅ | +| Computed properties: `var` (incl. `throws`) | ✅ / TODO | ✅ | +| Async functions `func async` and properties: `var { get async {} }` | ❌ | ❌ | +| Arrays: `[UInt8]`, `[T]` | ❌ | ❌ | +| Dictionaries: `[String: Int]`, `[K:V]` | ❌ | ❌ | +| Generic functions | ❌ | ❌ | +| `Foundation.Data`, `any Foundation.DataProtocol` | ✅ | ❌ | +| Tuples: `(Int, String)`, `(A, B, C)` | ❌ | ❌ | +| Protocols: `protocol`, existential parameters `any Collection` | ❌ | ❌ | +| Optional types: `Int?`, `AnyObject?` | ❌ | ❌ | +| Primitive types: `Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Float`, `Double` | ✅ | ✅ | +| Unsigned primitive types: `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` | ❌ | ❌ | +| String (with copying data) | ✅ | ✅ | +| Variadic parameters: `T...` | ❌ | ❌ | +| Parametrer packs / Variadic generics | ❌ | ❌ | +| Ownership modifiers: `inout`, `borrowing`, `consuming` | ❌ | ❌ | +| Default parameter values: `func p(name: String = "")` | ❌ | ❌ | +| Operators: `+`, `-`, user defined | ❌ | ❌ | +| Subscripts: `subscript()` | ❌ | ❌ | +| Equatable | ❌ | ❌ | +| Pointers: `UnsafeRawPointer`, UnsafeBufferPointer (?) | 🟡 | ❌ | +| Nested types: `struct Hello { struct World {} }` | ❌ | ❌ | +| Inheritance: `class Caplin: Capybara` | ❌ | ❌ | +| Non-escaping `Void` closures: `func callMe(maybe: () -> ())` | ✅ | ✅ | +| Non-escaping closures with primitive arguments/results: `func callMe(maybe: (Int) -> (Double))` | ✅ | ✅ | +| Non-escaping closures with object arguments/results: `func callMe(maybe: (JavaObj) -> (JavaObj))` | ❌ | ❌ | +| `@escaping` closures: `func callMe(_: @escaping () -> ())` | ❌ | ❌ | +| Swift type extensions: `extension String { func uppercased() }` | 🟡 | 🟡 | +| Swift macros (maybe) | ❌ | ❌ | +| Result builders | ❌ | ❌ | +| Automatic Reference Counting of class types / lifetime safety | ✅ | ✅ | +| Value semantic types (e.g. struct copying) | ❌ | ❌ | +| Opaque types: `func get() -> some Builder`, func take(worker: some Worker) | ❌ | ❌ | +| Swift concurrency: `func async`, `actor`, `distribued actor` | ❌ | ❌ | +| | | | +| | | | + +> tip: The list of features may be incomplete, please file an issue if something is unclear or should be clarified in this table. diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md new file mode 100644 index 00000000..f83458d4 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftJavaCommandLineTool.md @@ -0,0 +1,256 @@ +# swift-java command line tool + +The `swift-java` command line tool offers multiple ways to interact your Java interoperability enabled projects. + +## Overview + +The `swift-java` command line tool offers multiple modes which you can use to prepare your Swift and Java code to interact with eachother. + +The following sections will explain the modes in depth. When in doubt, you can always use the command line `--help` to get additional +guidance about the tool and available options: + +```bash +> swift-java --help + +USAGE: swift-java + +OPTIONS: + -h, --help Show help information. + +SUBCOMMANDS: + configure Configure and emit a swift-java.config file based on an input dependency or jar file + resolve Resolve dependencies and write the resulting swift-java.classpath file + wrap-java Wrap Java classes with corresponding Swift bindings. + jextract Wrap Swift functions and types with Java bindings, making them available to be called from Java + + See 'swift-java help ' for detailed help. +``` + +### Expose Java classes to Swift: swift-java wrap-java + +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 +single Java class. The `swift-java` can be executed like this: + +``` +swift-java help wrap-java +``` + +to produce help output like the following: + +``` +USAGE: swift-java wrap-java [--output-directory ] [--input-swift ] [--log-level ] [--cp ...] [--filter-java-package ] --swift-module [--depends-on ...] [--swift-native-implementation ...] [--cache-directory ] [--swift-match-package-directory-structure ] + +ARGUMENTS: + Path to .jar file whose Java classes should be wrapped using Swift bindings + +OPTIONS: + -o, --output-directory + The directory in which to output generated SwiftJava configuration files. + --input-swift + 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. + -l, --log-level + Configure the level of logs that should be printed (values: trace, debug, info, notice, warning, error, critical; default: log level) + --cp, --classpath Class search path of directories and zip/jar files from which Java classes can be loaded. + -f, --filter-java-package + While scanning a classpath, inspect only types included in this package + --swift-module + The name of the Swift module into which the resulting Swift types will be generated. + --depends-on + A swift-java configuration file for a given Swift module name on which this module depends, + e.g., JavaKitJar=Sources/JavaKitJar/Java2Swift.config. There should be one of these options + for each Swift module that this module depends on (transitively) that contains wrapped Java sources. + --swift-native-implementation + The names of Java classes whose declared native methods will be implemented in Swift. + --cache-directory + Cache directory for intermediate results and other outputs between runs + --swift-match-package-directory-structure + Match java package directory structure with generated Swift files (default: false) + -h, --help Show help information. + +``` + +For example, the `JavaKitJar` library is generated with this command line: + +```swift +swift-java wrap-java --swift-module JavaKitJar --depends-on JavaKit=Sources/JavaKit/swift-java.config -o Sources/JavaKitJar/generated Sources/JavaKitJar/swift-java.config +``` + +The `--swift-module JavaKitJar` parameter describes the name of the Swift module in which the code will be generated. + +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 `=`, 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 +`JavaKitNetwork`'s configuration file as a dependency here. + +The `-o` option specifies the output directory. Typically, this will be `Sources//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. + +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 +`java.util.zip.ZipOutputStream` and `java.io.OutputStream`: + +``` +warning: Unable to translate 'java.util.jar.JarOutputStream' superclass: Java class 'java.util.zip.ZipOutputStream' has not been translated into Swift +warning: Unable to translate 'java.util.jar.JarOutputStream' constructor: Java class 'java.io.OutputStream' has not been translated into Swift +warning: Unable to translate 'java.util.jar.JarInputStream' method 'transferTo': Java class 'java.io.OutputStream' has not been translated into Swift +``` + +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. + +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. + +### Under construction: Create a Java class to wrap the Swift library + +**NOTE**: the instructions here work, but we are still smoothing out the interoperability story. + +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`: + + +```java +package org.swift.javakit; + +public class HelloSwiftMain { + static { + System.loadLibrary("HelloSwift"); + } + + public native static void main(String[] args); +} +``` + +Compile this into a `.class` file with `javac` before we build the Swift half, e.g.,: + +``` +javac Java/src/org/swift/javakit/JavaClassTranslator.java +``` + +### Create a Swift library + +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., + +```swift + products: [ + .library( + name: "HelloSwift", + type: .dynamic, + targets: ["HelloSwift"] + ), + ] +``` + +with an associated target that depends on `JavaKit`: + +```swift + .target( + name: "HelloSwift", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "JavaKit", package: "JavaKit") + ]) +``` + +### Implement the `native` Java method in Swift +Now, in the `HelloSwift` Swift library, define a `struct` that provides the `main` method for the Java class we already defined: + +```swift +import JavaKit + +@JavaImplementation("org.swift.javakit.HelloSwiftMain") +struct HelloSwiftMain { + @JavaStaticMethod + static func main(arguments: [String], environment: JNIEnvironment? = nil) { + print("Command line arguments are: \(arguments)") + } +} +``` + +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. + +### Putting it all together! + +Finally, run this program on the command line like this: + +``` +java -cp Java/src -Djava.library.path=$(PATH_CONTAINING_HELLO_SWIFT)/ org.swift.javakit.HelloSwiftMain -v argument +``` + +This will prints the command-line arguments `-v` and `argument` as seen by Swift. + +### Bonus: Swift argument parser + +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: + +```swift +import ArgumentParser +import JavaKit + +@JavaClass("org.swift.javakit.HelloSwiftMain") +struct HelloSwiftMain: ParsableCommand { + @Option(name: .shortAndLong, help: "Enable verbose output") + var verbose: Bool = false + + @JavaImplementation + static func main(arguments: [String], environment: JNIEnvironment? = nil) { + let command = Self.parseOrExit(arguments) + command.run(environment: environment) + } + + func run(environment: JNIEnvironment? = nil) { + print("Verbose = \(verbose)") + } +} +``` + +### Download Java dependencies in Swift builds: swift-java resolve + +> TIP: See the `Samples/DependencySampleApp` for a fully functional showcase of this mode. + +TODO: documentation on this feature + +### Expose Swift code to Java: swift-java jextract + +The project is still very early days, however the general outline of using this approach is as follows: + +- **No code changes** need to be made to Swift libraries that are to be exposed to Java using jextract-swift. +- Swift sources are compiled to `.swiftinterface` files +- These `.swiftinterface` files are imported by jextract-swift which generates `*.java` files +- The generated Java files contain generated code for efficient native invocations. + +You can then use Swift libraries in Java just by calling the appropriate methods and initializers. + +### Generating Java bindings for Swift libraries + +This repository also includes the `jextract-swift` tool which is similar to the JDK's [`jextract`](https://github.com/openjdk/jextract/). + +This approach offers two modes of operation: + +- 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. + +> 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): +> +> ``` +> curl -s "https://get.sdkman.io" | bash +> sdk install java 22-open +> +> export JAVA_HOME=$(sdk home java 22-open) +> ``` + +`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. + +### Default jextract behaviors + +Only `public` functions, properties and types are imported. + +Global Swift functions become static functions on on a class with the same name as the Swift module in Java, + +```swift +// Swift (Sources/SomeModule/Example.swift) + +public func globalFunction() +``` + +becomes: + +```java +// Java (SomeModule.java) + +public final class SomeModule ... { + public static void globalFunction() { ... } +} +``` diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md new file mode 100644 index 00000000..d10ab387 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SwiftPMPlugin.md @@ -0,0 +1,126 @@ +# SwiftJava SwiftPM Plugin + +The `SwiftJavaPlugin` automates `swift-java` command line tool invocations during the build process. + +## Overview + +### Installing the plugin + +To install the SwiftPM plugin in your target of choice include the `swift-java` package dependency: + +```swift +import Foundation + +let javaHome = findJavaHome() + +let javaIncludePath = "\(javaHome)/include" +#if os(Linux) + let javaPlatformIncludePath = "\(javaIncludePath)/linux" +#elseif os(macOS) + let javaPlatformIncludePath = "\(javaIncludePath)/darwin" +#elseif os(Windows) + let javaPlatformIncludePath = "\(javaIncludePath)/win32" +#endif + +let package = Package( + name: "MyProject", + + products: [ + .library( + name: "JavaKitExample", + type: .dynamic, + targets: ["JavaKitExample"] + ), + ], + + dependencies: [ + .package(url: "https://github.com/apple/swift-java", from: "..."), + ], + + targets: [ + .target( + name: "MyProject", + dependencies: [ + // ... + ], + swiftSettings: [ + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) + ], + plugins: [ + .plugin(name: "JavaCompilerPlugin", package: "swift-java"), + .plugin(name: "JExtractSwiftPlugin", package: "swift-java"), + .plugin(name: "SwiftJavaPlugin", package: "swift-java"), + ] + ), + ] +) +``` + +```swift + +// Note: the JAVA_HOME environment variable must be set to point to where +// Java is installed, e.g., +// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. +func findJavaHome() -> String { + if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { + print("JAVA_HOME = \(home)") + return home + } + + // This is a workaround for envs (some IDEs) which have trouble with + // picking up env variables during the build process + let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" + if let home = try? String(contentsOfFile: path, encoding: .utf8) { + if let lastChar = home.last, lastChar.isNewline { + return String(home.dropLast()) + } + + return home + } + + if let home = getJavaHomeFromLibexecJavaHome(), + !home.isEmpty { + return home + } + + fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") +} + +/// On MacOS we can use the java_home tool as a fallback if we can't find JAVA_HOME environment variable. +func getJavaHomeFromLibexecJavaHome() -> String? { + let task = Process() + task.executableURL = URL(fileURLWithPath: "/usr/libexec/java_home") + + // Check if the executable exists before trying to run it + guard FileManager.default.fileExists(atPath: task.executableURL!.path) else { + print("/usr/libexec/java_home does not exist") + return nil + } + + let pipe = Pipe() + task.standardOutput = pipe + task.standardError = pipe // Redirect standard error to the same pipe for simplicity + + do { + try task.run() + task.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) + + if task.terminationStatus == 0 { + return output + } else { + print("java_home terminated with status: \(task.terminationStatus)") + // Optionally, log the error output for debugging + if let errorOutput = String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) { + print("Error output: \(errorOutput)") + } + return nil + } + } catch { + print("Error running java_home: \(error)") + return nil + } +} +``` \ No newline at end of file diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md new file mode 100644 index 00000000..7c695a71 --- /dev/null +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md @@ -0,0 +1,32 @@ +# ``SwiftJavaDocumentation`` + +The SwiftJava project enables interoperability between Swift and Java. + +## Overview + +This project contains a number of support packages, java libraries, tools and plugins that provide a complete +Swift and Java interoperability story. + +Please refer to articles about the specific direction of interoperability you are interested in. + +### Getting started + +TODO: Some general intro + +If you prefer a video introduction, you may want to this +[Explore Swift and Java interoperability](https://www.youtube.com/watch?v=QSHO-GUGidA) +WWDC 2025 session, +which is a quick overview of all the features and approaches offered by SwiftJava. + +## Topics + +### Supported Features + +- + + +### Source generation + +- +- + diff --git a/Sources/SwiftJavaDocumentation/empty.swift b/Sources/SwiftJavaDocumentation/empty.swift new file mode 100644 index 00000000..9c28861c --- /dev/null +++ b/Sources/SwiftJavaDocumentation/empty.swift @@ -0,0 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Empty file, this target is a docs-only target. diff --git a/WIP.md b/WIP.md index e1f58612..88671233 100644 --- a/WIP.md +++ b/WIP.md @@ -6,9 +6,6 @@ Here is a long yet still very incomplete list of things we would like to do or improve: - Expressivity gaps: - - [x] Translate Java exceptions into Swift and vice versa - - [ ] Expose Swift types to Java - - [x] Figure out when to promote from the local to the global heap - [ ] Automatically turn get/set method pairs into Swift properties? - [ ] Implement a global registry that lets us find the Swift type corresponding to a canonical Java class name (e.g., `java.net.URL` -> `JavaKitNetwork.URL`) - [ ] Introduce overloads of `is` and `as` on the Swift projections so that conversion to any implemented interface or extended superclass returns non-optional. @@ -16,14 +13,8 @@ improve: - [ ] Recognize Java's enum classes and map them into Swift well - [ ] Translate Java constants into Swift constants - [ ] Support nested classes - - [x] Streamline the definition of "main" code in Swift - [ ] Figure out how to subclass a Java class from Swift - Tooling - - [ ] Extract documentation comments from Java and put them in the Swift projections - - [ ] [SwiftPM build plugin](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/Plugins.md) to generate the Swift projections from Java as part of the build - - [x] Figure out how to launch the Java runtime from Swift code so we don't need to always start via `java` - - [x] Figure out how to unit-test this framework using Swift Testing - - [x] Add a "Jar mode" to `Java2Swift` that translates all classes in the given Jar file. - [ ] Generate Swift projections for more common Java types into JavaKit libraries to make it easier to get started - [ ] Teach `Java2Swift` when to create extensions of already-translated types that pick up any members that couldn't be translated because of missing types. See, for example, how `JavaKitReflection` adds extensions to `JavaClass` based on types like `Method` and `Parameter` - Performance: