Skip to content

Introduce docc documentation and "supported features" #319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/actions/prepare_env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion .github/scripts/install_swiftly.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

13 changes: 13 additions & 0 deletions .github/scripts/validate_docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

set -e
set -x

cat <<EOF >> 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
25 changes: 25 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}})
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ Package.resolved
*/**/*.o
*/**/*.swiftdeps
*/**/*.swiftdeps~
*/**/.docc-build/
8 changes: 8 additions & 0 deletions .spi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 1
builder:
configs:
- documentation_targets: [
SwiftJavaDocumentation,
JavaKit,
SwiftKitSwift
]
14 changes: 14 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ let package = Package(
name: "swift-java",
targets: ["SwiftJavaTool"]
),


.library(
name: "SwiftJavaDocumentation",
targets: ["SwiftJavaDocumentation"]
),

// ==== Plugin for building Java code
.plugin(
Expand Down Expand Up @@ -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: [
Expand Down
254 changes: 3 additions & 251 deletions USER_GUIDE.md → ...ces/JavaKit/Documentation.docc/JavaKit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -420,7 +416,7 @@ extension JavaClass<URLConnection> {
}
```

### 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:

Expand All @@ -437,247 +433,3 @@ public struct Enumeration<E: AnyJavaObject> {
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> <subcommand>

OPTIONS:
--depends-on <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 <swift-native-implementation>
The names of Java classes whose declared native methods will be implemented in Swift.
--output-swift <output-swift>
The directory where generated Swift files should be written. Generally used with jextract mode.
--output-java <output-java>
The directory where generated Java files should be written. Generally used with jextract mode.
--java-package <java-package>
The Java package the generated Java code should be emitted into.
-c, --cache-directory <cache-directory>
Directory where to write cached values (e.g. swift-java.classpath files)
-o, --output-directory <output-directory>
The directory in which to output the generated Swift files or the SwiftJava configuration file.
--input-swift <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 <log-level>
Configure the level of logs that should be printed (values: trace, debug, info, notice, warning, error, critical; default: log level)
--cp, --classpath <cp> Class search path of directories and zip/jar files from which Java classes can be loaded.
-f, --filter-java-package <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 <subcommand>' 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 `<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
`JavaKitNetwork`'s configuration file as a dependency here.

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.

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() { ... }
}
```
Loading
Loading