Skip to content

Commit 3be8df2

Browse files
madsodgaardktoso
andauthored
jextract: add support for implementing Swift protocols in Java (#449)
Co-authored-by: Konrad `ktoso` Malawski <konrad_malawski@apple.com>
1 parent 67c20f7 commit 3be8df2

File tree

47 files changed

+1871
-283
lines changed

Some content is hidden

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

47 files changed

+1871
-283
lines changed

Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift

Lines changed: 195 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
2525

2626
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
2727
let toolURL = try context.tool(named: "SwiftJavaTool").url
28-
28+
29+
var commands: [Command] = []
30+
2931
guard let sourceModule = target.sourceModule else { return [] }
3032

33+
3134
// Note: Target doesn't have a directoryURL counterpart to directory,
3235
// so we cannot eliminate this deprecation warning.
3336
for dependency in target.dependencies {
@@ -80,7 +83,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
8083
let (moduleName, configFile) = moduleAndConfigFile
8184
return [
8285
"--depends-on",
83-
"\(configFile.path(percentEncoded: false))"
86+
"\(moduleName)=\(configFile.path(percentEncoded: false))"
8487
]
8588
}
8689
arguments += dependentConfigFilesArguments
@@ -123,15 +126,165 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
123126

124127
print("[swift-java-plugin] Output swift files:\n - \(outputSwiftFiles.map({$0.absoluteString}).joined(separator: "\n - "))")
125128

126-
return [
129+
var jextractOutputFiles = outputSwiftFiles
130+
131+
// If the developer has enabled java callbacks in the configuration (default is false)
132+
// and we are running in JNI mode, we will run additional phases in this build plugin
133+
// to generate Swift wrappers using wrap-java that can be used to callback to Java.
134+
let shouldRunJavaCallbacksPhases =
135+
if let configuration,
136+
configuration.enableJavaCallbacks == true,
137+
configuration.effectiveMode == .jni {
138+
true
139+
} else {
140+
false
141+
}
142+
143+
// Extract list of all sources
144+
let javaSourcesListFileName = "jextract-generated-sources.txt"
145+
let javaSourcesFile = outputJavaDirectory.appending(path: javaSourcesListFileName)
146+
if shouldRunJavaCallbacksPhases {
147+
arguments += [
148+
"--generated-java-sources-list-file-output", javaSourcesListFileName
149+
]
150+
jextractOutputFiles += [javaSourcesFile]
151+
}
152+
153+
commands += [
127154
.buildCommand(
128155
displayName: "Generate Java wrappers for Swift types",
129156
executable: toolURL,
130157
arguments: arguments,
131158
inputFiles: [ configFile ] + swiftFiles,
132-
outputFiles: outputSwiftFiles
159+
outputFiles: jextractOutputFiles
160+
)
161+
]
162+
163+
// If we do not need Java callbacks, we can skip the remaining steps.
164+
guard shouldRunJavaCallbacksPhases else {
165+
return commands
166+
}
167+
168+
// The URL of the compiled Java sources
169+
let javaCompiledClassesURL = context.pluginWorkDirectoryURL
170+
.appending(path: "compiled-java-output")
171+
172+
// Build SwiftKitCore and get the classpath
173+
// as the jextracted sources will depend on that
174+
175+
guard let swiftJavaDirectory = findSwiftJavaDirectory(for: target) else {
176+
fatalError("Unable to find the path to the swift-java sources, please file an issue.")
177+
}
178+
log("Found swift-java at \(swiftJavaDirectory)")
179+
180+
let swiftKitCoreClassPath = swiftJavaDirectory.appending(path: "SwiftKitCore/build/classes/java/main")
181+
182+
// We need to use a different gradle home, because
183+
// this plugin might be run from inside another gradle task
184+
// and that would cause conflicts.
185+
let gradleUserHome = context.pluginWorkDirectoryURL.appending(path: "gradle-user-home")
186+
187+
let GradleUserHome = "GRADLE_USER_HOME"
188+
let gradleUserHomePath = gradleUserHome.path(percentEncoded: false)
189+
log("Prepare command: :SwiftKitCore:build in \(GradleUserHome)=\(gradleUserHomePath)")
190+
var gradlewEnvironment = ProcessInfo.processInfo.environment
191+
gradlewEnvironment[GradleUserHome] = gradleUserHomePath
192+
log("Forward environment: \(gradlewEnvironment)")
193+
194+
let gradleExecutable = findExecutable(name: "gradle") ?? // try using installed 'gradle' if available in PATH
195+
swiftJavaDirectory.appending(path: "gradlew") // fallback to calling ./gradlew if gradle is not installed
196+
log("Detected 'gradle' executable (or gradlew fallback): \(gradleExecutable)")
197+
198+
commands += [
199+
.buildCommand(
200+
displayName: "Build SwiftKitCore using Gradle (Java)",
201+
executable: gradleExecutable,
202+
arguments: [
203+
":SwiftKitCore:build",
204+
"--project-dir", swiftJavaDirectory.path(percentEncoded: false),
205+
"--gradle-user-home", gradleUserHomePath,
206+
"--configure-on-demand",
207+
"--no-daemon"
208+
],
209+
environment: gradlewEnvironment,
210+
inputFiles: [swiftJavaDirectory],
211+
outputFiles: [swiftKitCoreClassPath]
212+
)
213+
]
214+
215+
// Compile the jextracted sources
216+
let javaHome = URL(filePath: findJavaHome())
217+
218+
commands += [
219+
.buildCommand(
220+
displayName: "Build extracted Java sources",
221+
executable: javaHome
222+
.appending(path: "bin")
223+
.appending(path: self.javacName),
224+
arguments: [
225+
"@\(javaSourcesFile.path(percentEncoded: false))",
226+
"-d", javaCompiledClassesURL.path(percentEncoded: false),
227+
"-parameters",
228+
"-classpath", swiftKitCoreClassPath.path(percentEncoded: false)
229+
],
230+
inputFiles: [javaSourcesFile, swiftKitCoreClassPath],
231+
outputFiles: [javaCompiledClassesURL]
232+
)
233+
]
234+
235+
// Run `configure` to extract a swift-java config to use for wrap-java
236+
let swiftJavaConfigURL = context.pluginWorkDirectoryURL.appending(path: "swift-java.config")
237+
238+
commands += [
239+
.buildCommand(
240+
displayName: "Output swift-java.config that contains all extracted Java sources",
241+
executable: toolURL,
242+
arguments: [
243+
"configure",
244+
"--output-directory", context.pluginWorkDirectoryURL.path(percentEncoded: false),
245+
"--cp", javaCompiledClassesURL.path(percentEncoded: false),
246+
"--swift-module", sourceModule.name,
247+
"--swift-type-prefix", "Java"
248+
],
249+
inputFiles: [javaCompiledClassesURL],
250+
outputFiles: [swiftJavaConfigURL]
133251
)
134252
]
253+
254+
let singleSwiftFileOutputName = "WrapJavaGenerated.swift"
255+
256+
// In the end we can run wrap-java on the previous inputs
257+
var wrapJavaArguments = [
258+
"wrap-java",
259+
"--swift-module", sourceModule.name,
260+
"--output-directory", outputSwiftDirectory.path(percentEncoded: false),
261+
"--config", swiftJavaConfigURL.path(percentEncoded: false),
262+
"--cp", swiftKitCoreClassPath.path(percentEncoded: false),
263+
"--single-swift-file-output", singleSwiftFileOutputName
264+
]
265+
266+
// Add any dependent config files as arguments
267+
wrapJavaArguments += dependentConfigFilesArguments
268+
269+
commands += [
270+
.buildCommand(
271+
displayName: "Wrap compiled Java sources using wrap-java",
272+
executable: toolURL,
273+
arguments: wrapJavaArguments,
274+
inputFiles: [swiftJavaConfigURL, swiftKitCoreClassPath],
275+
outputFiles: [outputSwiftDirectory.appending(path: singleSwiftFileOutputName)]
276+
)
277+
]
278+
279+
return commands
280+
}
281+
282+
var javacName: String {
283+
#if os(Windows)
284+
"javac.exe"
285+
#else
286+
"javac"
287+
#endif
135288
}
136289

137290
/// Find the manifest files from other swift-java executions in any targets
@@ -181,5 +334,43 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
181334

182335
return dependentConfigFiles
183336
}
337+
338+
private func findSwiftJavaDirectory(for target: any Target) -> URL? {
339+
for dependency in target.dependencies {
340+
switch dependency {
341+
case .target(let target):
342+
continue
343+
344+
case .product(let product):
345+
guard let swiftJava = product.sourceModules.first(where: { $0.name == "SwiftJava" }) else {
346+
return nil
347+
}
348+
349+
// We are inside Sources/SwiftJava
350+
return swiftJava.directoryURL.deletingLastPathComponent().deletingLastPathComponent()
351+
352+
@unknown default:
353+
continue
354+
}
355+
}
356+
357+
return nil
358+
}
184359
}
185360

361+
func findExecutable(name: String) -> URL? {
362+
let fileManager = FileManager.default
363+
364+
guard let path = ProcessInfo.processInfo.environment["PATH"] else {
365+
return nil
366+
}
367+
368+
for path in path.split(separator: ":") {
369+
let fullURL = URL(fileURLWithPath: String(path)).appendingPathComponent(name)
370+
if fileManager.isExecutableFile(atPath: fullURL.path) {
371+
return fullURL
372+
}
373+
}
374+
375+
return nil
376+
}

Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,6 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
171171
arguments += javaStdlibModules.flatMap { ["--depends-on", $0] }
172172

173173
if !outputSwiftFiles.isEmpty {
174-
arguments += [ configFile.path(percentEncoded: false) ]
175-
176174
let displayName = "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'"
177175
log("Prepared: \(displayName)")
178176
commands += [
@@ -266,4 +264,4 @@ func getExtractedJavaStdlibModules() -> [String] {
266264
}
267265
return url.lastPathComponent
268266
}.sorted()
269-
}
267+
}

Samples/JavaDependencySampleApp/ci-validate.sh

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@
33
set -e
44
set -x
55

6+
# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418
7+
if [ "$(uname)" = "Darwin" ]; then
8+
DISABLE_EXPERIMENTAL_PREBUILTS=''
9+
else
10+
DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts'
11+
fi
12+
613
# invoke resolve as part of a build run
714
swift build \
8-
--disable-experimental-prebuilts \
15+
$DISABLE_EXPERIMENTAL_PREBUILTS \
916
--disable-sandbox
1017

1118
# explicitly invoke resolve without explicit path or dependency
1219
# the dependencies should be uses from the --swift-module
1320

1421
# FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418
1522
swift run \
16-
--disable-experimental-prebuilts \
23+
$DISABLE_EXPERIMENTAL_PREBUILTS \
1724
swift-java resolve \
1825
Sources/JavaCommonsCSV/swift-java.config \
1926
--swift-module JavaCommonsCSV \

Samples/JavaKitSampleApp/ci-validate.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
set -e
44
set -x
55

6-
swift build \
7-
--disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418
6+
# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418
7+
if [ "$(uname)" = "Darwin" ]; then
8+
DISABLE_EXPERIMENTAL_PREBUILTS=''
9+
else
10+
DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts'
11+
fi
12+
13+
swift build $DISABLE_EXPERIMENTAL_PREBUILTS
814

915
"$JAVA_HOME/bin/java" \
1016
-cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java \

Samples/JavaProbablyPrime/ci-validate.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
set -e
44
set -x
55

6-
# FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418
6+
# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418
7+
if [ "$(uname)" = "Darwin" ]; then
8+
DISABLE_EXPERIMENTAL_PREBUILTS=''
9+
else
10+
DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts'
11+
fi
12+
713
swift run \
8-
--disable-experimental-prebuilts \
14+
$DISABLE_EXPERIMENTAL_PREBUILTS \
915
JavaProbablyPrime 1337
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 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 SwiftJava
16+
17+
public protocol CallbackProtocol {
18+
func withBool(_ input: Bool) -> Bool
19+
func withInt8(_ input: Int8) -> Int8
20+
func withUInt16(_ input: UInt16) -> UInt16
21+
func withInt16(_ input: Int16) -> Int16
22+
func withInt32(_ input: Int32) -> Int32
23+
func withInt64(_ input: Int64) -> Int64
24+
func withFloat(_ input: Float) -> Float
25+
func withDouble(_ input: Double) -> Double
26+
func withString(_ input: String) -> String
27+
func withVoid()
28+
func withObject(_ input: MySwiftClass) -> MySwiftClass
29+
func withOptionalInt64(_ input: Int64?) -> Int64?
30+
func withOptionalObject(_ input: MySwiftClass?) -> Optional<MySwiftClass>
31+
}
32+
33+
public struct CallbackOutput {
34+
public let bool: Bool
35+
public let int8: Int8
36+
public let uint16: UInt16
37+
public let int16: Int16
38+
public let int32: Int32
39+
public let int64: Int64
40+
public let _float: Float
41+
public let _double: Double
42+
public let string: String
43+
public let object: MySwiftClass
44+
public let optionalInt64: Int64?
45+
public let optionalObject: MySwiftClass?
46+
}
47+
48+
public func outputCallbacks(
49+
_ callbacks: some CallbackProtocol,
50+
bool: Bool,
51+
int8: Int8,
52+
uint16: UInt16,
53+
int16: Int16,
54+
int32: Int32,
55+
int64: Int64,
56+
_float: Float,
57+
_double: Double,
58+
string: String,
59+
object: MySwiftClass,
60+
optionalInt64: Int64?,
61+
optionalObject: MySwiftClass?
62+
) -> CallbackOutput {
63+
return CallbackOutput(
64+
bool: callbacks.withBool(bool),
65+
int8: callbacks.withInt8(int8),
66+
uint16: callbacks.withUInt16(uint16),
67+
int16: callbacks.withInt16(int16),
68+
int32: callbacks.withInt32(int32),
69+
int64: callbacks.withInt64(int64),
70+
_float: callbacks.withFloat(_float),
71+
_double: callbacks.withDouble(_double),
72+
string: callbacks.withString(string),
73+
object: callbacks.withObject(object),
74+
optionalInt64: callbacks.withOptionalInt64(optionalInt64),
75+
optionalObject: callbacks.withOptionalObject(optionalObject)
76+
)
77+
}

0 commit comments

Comments
 (0)