Skip to content

Commit 0dbc1e9

Browse files
committed
Redesign jextract-swift: plugins and avoid custom swift features
We now heavily rely on Swift thunk generation and call into those C calling convention compatible APIs from Java. They translate into member calls on classes -- this way we'll be able to also do structs and other things. This also introduces gradle to "drive" the `swift package jextract` plugin. This allows us for samples to be a plain `./gradlew run`, and no more manual running of any make or other swift interface generation steps etc. This is a big milestone towards getting "just works" builds with the jextract route. Follow ups will handle more java library path handling and more types of calls we can make, like variables, and importing structs etc.
1 parent fff9483 commit 0dbc1e9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1759
-492
lines changed

.github/workflows/pull_request.yml

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ jobs:
1919
strategy:
2020
fail-fast: true
2121
matrix:
22-
swift_version: ['nightly-main']
22+
# swift_version: ['nightly-main']
23+
swift_version: ['6.0.2']
2324
os_version: ['jammy']
2425
jdk_vendor: ['Corretto']
2526
container:
@@ -43,8 +44,8 @@ jobs:
4344
if: steps.cache-jdk.outputs.cache-hit != 'true'
4445
run: "bash -xc 'JDK_VENDOR=${{ matrix.jdk_vendor }} ./docker/install_jdk.sh'"
4546
# TODO: not using setup-java since incompatible with the swiftlang/swift base image
46-
- name: Install Untested Nightly Swift
47-
run: "bash -xc './docker/install_untested_nightly_swift.sh'"
47+
# - name: Install Untested Nightly Swift
48+
# run: "bash -xc './docker/install_untested_nightly_swift.sh'"
4849
- name: Cache local Gradle repository
4950
uses: actions/cache@v4
5051
continue-on-error: true
@@ -79,7 +80,8 @@ jobs:
7980
strategy:
8081
fail-fast: false
8182
matrix:
82-
swift_version: ['nightly-main']
83+
# swift_version: ['nightly-main']
84+
swift_version: ['6.0.2']
8385
os_version: ['jammy']
8486
jdk_vendor: ['Corretto']
8587
container:
@@ -103,8 +105,8 @@ jobs:
103105
if: steps.cache-jdk.outputs.cache-hit != 'true'
104106
run: "bash -xc 'JDK_VENDOR=${{ matrix.jdk_vendor }} ./docker/install_jdk.sh'"
105107
# TODO: not using setup-java since incompatible with the swiftlang/swift base image
106-
- name: Install Untested Nightly Swift
107-
run: "bash -xc './docker/install_untested_nightly_swift.sh'"
108+
# - name: Install Untested Nightly Swift
109+
# run: "bash -xc './docker/install_untested_nightly_swift.sh'"
108110
- name: Cache local Gradle repository
109111
uses: actions/cache@v4
110112
continue-on-error: true
@@ -125,9 +127,9 @@ jobs:
125127
${{ runner.os }}-swiftpm-cache
126128
${{ runner.os }}-swiftpm-
127129
# run the actual build
128-
- name: Generate sources (make) (Temporary)
129-
# TODO: this should be triggered by the respective builds
130-
run: "make jextract-generate"
130+
# - name: Generate sources (make) (Temporary)
131+
# # TODO: this should be triggered by the respective builds
132+
# run: "make jextract-generate"
131133
- name: Test Swift
132134
run: "swift test"
133135
- name: Build (Swift) Sample Apps

.licenseignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ Makefile
3737
gradle/wrapper/gradle-wrapper.properties
3838
gradlew
3939
gradlew.bat
40+
**/gradlew
41+
**/gradlew.bat

Makefile

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ all:
5757
@echo "Welcome to swift-java! There are several makefile targets to choose from:"
5858
@echo " javakit-run: Run the JavaKit example program that uses Java libraries from Swift."
5959
@echo " javakit-generate: Regenerate the Swift wrapper code for the various JavaKit libraries from Java. This only has to be done when changing the Java2Swift tool."
60-
@echo " jextract-run: Run the Java example code that uses the wrapped Swift library. NOTE: this requires development toolchain described in the README."
61-
@echo " jextract-generate: Generate Java wrapper code for the example Swift library allowing Swift to be called from Java. NOTE: this requires development toolchain described in the README."
6260

6361
$(BUILD_DIR)/debug/libJavaKit.$(LIB_SUFFIX) $(BUILD_DIR)/debug/Java2Swift:
6462
swift build
@@ -105,42 +103,5 @@ format:
105103
### "SwiftKit" is the "call swift from java" ###
106104
#################################################
107105

108-
JEXTRACT_BUILD_DIR="$(BUILD_DIR)/jextract"
109-
110-
define make_swiftinterface
111-
$(eval $@_MODULE = $(1))
112-
$(eval $@_FILENAME = $(2))
113-
eval ${SWIFTC} \
114-
-emit-module-interface-path ${JEXTRACT_BUILD_DIR}/${$@_MODULE}/${$@_FILENAME}.swiftinterface \
115-
-emit-module-path ${JEXTRACT_BUILD_DIR}/${$@_MODULE}/${$@_FILENAME}.swiftmodule \
116-
-enable-library-evolution \
117-
-Xfrontend -abi-comments-in-module-interface \
118-
-module-name ${$@_MODULE} \
119-
-Xfrontend -abi-comments-in-module-interface \
120-
Sources/${$@_MODULE}/${$@_FILENAME}.swift
121-
echo "Generated: ${JEXTRACT_BUILD_DIR}/${$@_MODULE}/${$@_FILENAME}.swiftinterface"
122-
endef
123-
124-
jextract-swift: generate-JExtract-interface-files
125-
swift build
126-
127-
generate-JExtract-interface-files: $(BUILD_DIR)/debug/libJavaKit.$(LIB_SUFFIX)
128-
@echo "Generate .swiftinterface files..."
129-
@$(call make_swiftinterface, "ExampleSwiftLibrary", "MySwiftLibrary")
130-
@$(call make_swiftinterface, "SwiftKitSwift", "SwiftKit")
131-
132-
jextract-generate: jextract-swift generate-JExtract-interface-files
133-
swift run jextract-swift \
134-
--package-name com.example.swift.generated \
135-
--swift-module ExampleSwiftLibrary \
136-
--output-directory ${SAMPLES_DIR}/SwiftKitSampleApp/build/generated/sources/jextract/main \
137-
$(BUILD_DIR)/jextract/ExampleSwiftLibrary/MySwiftLibrary.swiftinterface; \
138-
swift run jextract-swift \
139-
--package-name org.swift.swiftkit.generated \
140-
--swift-module SwiftKitSwift \
141-
--output-directory ${SAMPLES_DIR}/SwiftKitSampleApp/build/generated/sources/jextract/main \
142-
$(BUILD_DIR)/jextract/SwiftKitSwift/SwiftKit.swiftinterface
143-
144-
145106
jextract-run: jextract-generate
146107
./gradlew Samples:SwiftKitSampleApp:run

Package.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ let package = Package(
126126
targets: ["JExtractSwift"]
127127
),
128128

129+
// ==== Plugin for wrapping Java classes in Swift
130+
.plugin(
131+
name: "JExtractSwiftPlugin",
132+
targets: [
133+
"JExtractSwiftPlugin"
134+
]
135+
),
136+
129137
// ==== Examples
130138

131139
.library(
@@ -229,6 +237,7 @@ let package = Package(
229237
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
230238
]
231239
),
240+
232241
.plugin(
233242
name: "JavaCompilerPlugin",
234243
capability: .buildTool()
@@ -331,6 +340,14 @@ let package = Package(
331340
]
332341
),
333342

343+
.plugin(
344+
name: "JExtractSwiftPlugin",
345+
capability: .buildTool(),
346+
dependencies: [
347+
"JExtractSwiftTool"
348+
]
349+
),
350+
334351
.testTarget(
335352
name: "JavaKitTests",
336353
dependencies: ["JavaKit", "JavaKitNetwork"],
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
17+
/// Configuration for the JExtractSwift translation tool, provided on a per-target
18+
/// basis.
19+
struct Configuration: Codable {
20+
var javaPackage: String
21+
}
22+
23+
func readConfiguration(sourceDir: String) throws -> Configuration {
24+
let configFile = URL(filePath: sourceDir).appending(path: "JExtractSwift.config")
25+
do {
26+
let configData = try Data(contentsOf: configFile)
27+
return try JSONDecoder().decode(Configuration.self, from: configData)
28+
} catch {
29+
throw ConfigurationError(message: "Failed to parse JExtractSwift configuration at '\(configFile)!'", error: error)
30+
}
31+
}
32+
33+
struct ConfigurationError: Error {
34+
let message: String
35+
let error: any Error
36+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
import PackagePlugin
17+
18+
@main
19+
struct JExtractSwiftBuildToolPlugin: BuildToolPlugin {
20+
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
21+
guard let sourceModule = target.sourceModule else { return [] }
22+
23+
// Note: Target doesn't have a directoryURL counterpart to directory,
24+
// so we cannot eliminate this deprecation warning.
25+
let sourceDir = target.directory.string
26+
27+
let configuration = try readConfiguration(sourceDir: "\(sourceDir)")
28+
29+
// We use the the usual maven-style structure of "src/[generated|main|test]/java/..."
30+
// that is common in JVM ecosystem
31+
let outputDirectoryJava = context.pluginWorkDirectoryURL
32+
.appending(path: "src")
33+
.appending(path: "generated")
34+
.appending(path: "java")
35+
let outputDirectorySwift = context.pluginWorkDirectoryURL
36+
.appending(path: "src")
37+
.appending(path: "generated")
38+
.appending(path: "Sources")
39+
40+
var arguments: [String] = [
41+
"--swift-module", sourceModule.name,
42+
"--package-name", configuration.javaPackage,
43+
"--output-directory-java", outputDirectoryJava.path(percentEncoded: false),
44+
"--output-directory-swift", outputDirectorySwift.path(percentEncoded: false),
45+
// TODO: "--build-cache-directory", ...
46+
// Since plugins cannot depend on libraries we cannot detect what the output files will be,
47+
// as it depends on the contents of the input files. Therefore we have to implement this as a prebuild plugin.
48+
// We'll have to make up some caching inside the tool so we don't re-parse files which have not changed etc.
49+
]
50+
arguments.append(sourceDir)
51+
52+
return [
53+
.prebuildCommand(
54+
displayName: "Generate Java wrappers for Swift types",
55+
executable: try context.tool(named: "JExtractSwiftTool").url,
56+
arguments: arguments,
57+
// inputFiles: [ configFile ] + swiftFiles,
58+
// outputFiles: outputJavaFiles
59+
outputFilesDirectory: outputDirectorySwift
60+
)
61+
]
62+
}
63+
}
64+
65+
// Note: the JAVA_HOME environment variable must be set to point to where
66+
// Java is installed, e.g.,
67+
// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home.
68+
func findJavaHome() -> String {
69+
if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] {
70+
return home
71+
}
72+
73+
// This is a workaround for envs (some IDEs) which have trouble with
74+
// picking up env variables during the build process
75+
let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home"
76+
if let home = try? String(contentsOfFile: path, encoding: .utf8) {
77+
if let lastChar = home.last, lastChar.isNewline {
78+
return String(home.dropLast())
79+
}
80+
81+
return home
82+
}
83+
84+
fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.")
85+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// swift-tools-version: 6.0
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
import class Foundation.FileManager
7+
import class Foundation.ProcessInfo
8+
9+
// Note: the JAVA_HOME environment variable must be set to point to where
10+
// Java is installed, e.g.,
11+
// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home.
12+
func findJavaHome() -> String {
13+
if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] {
14+
return home
15+
}
16+
17+
// This is a workaround for envs (some IDEs) which have trouble with
18+
// picking up env variables during the build process
19+
let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home"
20+
if let home = try? String(contentsOfFile: path, encoding: .utf8) {
21+
if let lastChar = home.last, lastChar.isNewline {
22+
return String(home.dropLast())
23+
}
24+
25+
return home
26+
}
27+
28+
fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.")
29+
}
30+
let javaHome = findJavaHome()
31+
32+
let javaIncludePath = "\(javaHome)/include"
33+
#if os(Linux)
34+
let javaPlatformIncludePath = "\(javaIncludePath)/linux"
35+
#elseif os(macOS)
36+
let javaPlatformIncludePath = "\(javaIncludePath)/darwin"
37+
#else
38+
// TODO: Handle windows as well
39+
#error("Currently only macOS and Linux platforms are supported, this may change in the future.")
40+
#endif
41+
42+
let package = Package(
43+
name: "JExtractPluginSampleApp",
44+
platforms: [
45+
.macOS(.v10_15),
46+
],
47+
products: [
48+
.library(
49+
name: "JExtractPluginSampleLib",
50+
type: .dynamic,
51+
targets: ["JExtractPluginSampleLib"]
52+
),
53+
],
54+
dependencies: [
55+
.package(name: "swift-java", path: "../../"),
56+
],
57+
targets: [
58+
.target(
59+
name: "JExtractPluginSampleLib",
60+
dependencies: [
61+
],
62+
swiftSettings: [
63+
.unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"])
64+
],
65+
plugins: [
66+
.plugin(name: "JExtractSwiftPlugin", package: "swift-java"),
67+
]
68+
),
69+
]
70+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"javaPackage": "com.example.swift"
3+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
public class MyCoolSwiftClass {
16+
var number: Int
17+
public init(number: Int) {
18+
print("[swift] init(number: \(number))")
19+
self.number = number
20+
}
21+
22+
public func exposedToJava() {
23+
print("[swift] exposedToJava()")
24+
print("[swift] number = \(number)")
25+
}
26+
}

0 commit comments

Comments
 (0)