Skip to content

Commit 89b2bcc

Browse files
authored
jextract: generate one output swift file per input file (#403)
1 parent 6168c62 commit 89b2bcc

28 files changed

+399
-147
lines changed

Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
9393
$0.pathExtension == "swift"
9494
}
9595

96+
// Output Swift files are just Java filename based converted to Swift files one-to-one
9697
var outputSwiftFiles: [URL] = swiftFiles.compactMap { sourceFileURL in
9798
guard sourceFileURL.isFileURL else {
9899
return nil as URL?
@@ -102,7 +103,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
102103
guard sourceFilePath.starts(with: sourceDir) else {
103104
fatalError("Could not get relative path for source file \(sourceFilePath)")
104105
}
105-
var outputURL = outputSwiftDirectory
106+
let outputURL = outputSwiftDirectory
106107
.appending(path: String(sourceFilePath.dropFirst(sourceDir.count).dropLast(sourceFileURL.lastPathComponent.count + 1)))
107108

108109
let inputFileName = sourceFileURL.deletingPathExtension().lastPathComponent
@@ -116,11 +117,12 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
116117

117118
// If the module uses 'Data' type, the thunk file is emitted as if 'Data' is declared
118119
// in that module. Declare the thunk file as the output.
119-
// FIXME: Make this conditional.
120120
outputSwiftFiles += [
121-
outputSwiftDirectory.appending(path: "Data+SwiftJava.swift")
121+
outputSwiftDirectory.appending(path: "Foundation+SwiftJava.swift")
122122
]
123123

124+
print("[swift-java-plugin] Output swift files:\n - \(outputSwiftFiles.map({$0.absoluteString}).joined(separator: "\n - "))")
125+
124126
return [
125127
.buildCommand(
126128
displayName: "Generate Java wrappers for Swift types",
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+
// This file exists to exercise the swiftpm plugin generating separate output Java files
16+
// for the public types; because Java public types must be in a file with the same name as the type.
17+
18+
public struct PublicTypeOne {
19+
public init() {}
20+
public func test() {}
21+
}
22+
23+
public struct PublicTypeTwo {
24+
public init() {}
25+
public func test() {}
26+
}

Samples/SwiftJavaExtractFFMSampleApp/ci-validate.sh

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

6+
swift build # as a workaround for building swift build from within gradle having issues on CI sometimes
7+
68
./gradlew run
79
./gradlew test
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) 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+
package com.example.swift;
16+
17+
import org.junit.jupiter.api.Test;
18+
import org.swift.swiftkit.core.*;
19+
import org.swift.swiftkit.ffm.*;
20+
21+
import java.lang.foreign.Arena;
22+
import java.util.Optional;
23+
import java.util.OptionalInt;
24+
25+
import static org.junit.jupiter.api.Assertions.*;
26+
27+
public class MultipleTypesFromSingleFileTest {
28+
29+
@Test
30+
void bothTypesMustHaveBeenGenerated() {
31+
try (var arena = AllocatingSwiftArena.ofConfined()) {
32+
PublicTypeOne.init(arena);
33+
PublicTypeTwo.init(arena);
34+
}
35+
}
36+
}
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+
// This file exists to exercise the swiftpm plugin generating separate output Java files
16+
// for the public types; because Java public types must be in a file with the same name as the type.
17+
18+
public struct PublicTypeOne {
19+
public init() {}
20+
public func test() {}
21+
}
22+
23+
public struct PublicTypeTwo {
24+
public init() {}
25+
public func test() {}
26+
}

Samples/SwiftJavaExtractJNISampleApp/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ def jextract = tasks.register("jextract", Exec) {
102102

103103
workingDir = layout.projectDirectory
104104
commandLine "swift"
105-
args("build") // since Swift targets which need to be jextract-ed have the jextract build plugin, we just need to build
105+
// TODO: -v for debugging build issues...
106+
args("build", "-v") // since Swift targets which need to be jextract-ed have the jextract build plugin, we just need to build
106107
// If we wanted to execute a specific subcommand, we can like this:
107108
// args("run",/*
108109
// "swift-java", "jextract",

Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh

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

6+
swift build # as a workaround for building swift build from within gradle having issues on CI sometimes
7+
68
./gradlew run
79
./gradlew test
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) 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+
package com.example.swift;
16+
17+
import org.junit.jupiter.api.Test;
18+
import org.swift.swiftkit.core.ConfinedSwiftMemorySession;
19+
import org.swift.swiftkit.core.SwiftArena;
20+
21+
import java.lang.foreign.Arena;
22+
import java.util.Optional;
23+
import java.util.OptionalInt;
24+
25+
import static org.junit.jupiter.api.Assertions.*;
26+
27+
public class MultipleTypesFromSingleFileTest {
28+
29+
@Test
30+
void bothTypesMustHaveBeenGenerated() {
31+
try (var arena = SwiftArena.ofConfined()) {
32+
PublicTypeOne.init(arena);
33+
PublicTypeTwo.init(arena);
34+
}
35+
}
36+
}

Sources/JExtractSwiftLib/CodePrinter.swift

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,14 +216,28 @@ extension CodePrinter {
216216

217217
/// - Returns: the output path of the generated file, if any (i.e. not in accumulate in memory mode)
218218
package mutating func writeContents(
219-
outputDirectory: String,
219+
outputDirectory _outputDirectory: String,
220220
javaPackagePath: String?,
221-
filename: String
221+
filename _filename: String
222222
) throws -> URL? {
223+
224+
// We handle 'filename' that has a path, since that simplifies passing paths from root output directory enourmously.
225+
// This just moves the directory parts into the output directory part in order for us to create the sub-directories.
226+
let outputDirectory: String
227+
let filename: String
228+
if _filename.contains(PATH_SEPARATOR) {
229+
let parts = _filename.split(separator: PATH_SEPARATOR)
230+
outputDirectory = _outputDirectory.appending(PATH_SEPARATOR).appending(parts.dropLast().joined(separator: PATH_SEPARATOR))
231+
filename = "\(parts.last!)"
232+
} else {
233+
outputDirectory = _outputDirectory
234+
filename = _filename
235+
}
236+
223237
guard self.mode != .accumulateAll else {
224238
// if we're accumulating everything, we don't want to finalize/flush any contents
225239
// let's mark that this is where a write would have happened though:
226-
print("// ^^^^ Contents of: \(outputDirectory)/\(filename)")
240+
print("// ^^^^ Contents of: \(outputDirectory)\(PATH_SEPARATOR)\(filename)")
227241
return nil
228242
}
229243

@@ -233,7 +247,7 @@ extension CodePrinter {
233247
"// ==== ---------------------------------------------------------------------------------------------------"
234248
)
235249
if let javaPackagePath {
236-
print("// \(javaPackagePath)/\(filename)")
250+
print("// \(javaPackagePath)\(PATH_SEPARATOR)\(filename)")
237251
} else {
238252
print("// \(filename)")
239253
}
@@ -242,9 +256,15 @@ extension CodePrinter {
242256
}
243257

244258
let targetDirectory = [outputDirectory, javaPackagePath].compactMap { $0 }.joined(separator: PATH_SEPARATOR)
245-
log.trace("Prepare target directory: \(targetDirectory)")
246-
try FileManager.default.createDirectory(
247-
atPath: targetDirectory, withIntermediateDirectories: true)
259+
log.debug("Prepare target directory: '\(targetDirectory)' for file \(filename.bold)")
260+
do {
261+
try FileManager.default.createDirectory(
262+
atPath: targetDirectory, withIntermediateDirectories: true)
263+
} catch {
264+
// log and throw since it can be confusing what the reason for failing the write was otherwise
265+
log.warning("Failed to create directory: \(targetDirectory)")
266+
throw error
267+
}
248268

249269
let outputPath = Foundation.URL(fileURLWithPath: targetDirectory).appendingPathComponent(filename)
250270
try contents.write(

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+SwiftThunkPrinting.swift

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,16 @@ extension FFMSwift2JavaGenerator {
2222
}
2323

2424
package func writeSwiftExpectedEmptySources() throws {
25+
let pendingFileCount = self.expectedOutputSwiftFiles.count
26+
guard pendingFileCount > 0 else {
27+
return // no need to write any empty files, yay
28+
}
29+
30+
print("[swift-java] Write empty [\(self.expectedOutputSwiftFiles.count)] 'expected' files in: \(swiftOutputDirectory)/")
31+
2532
for expectedFileName in self.expectedOutputSwiftFiles {
26-
log.trace("Write empty file: \(expectedFileName) ...")
33+
log.debug("Write SwiftPM-'expected' empty file: \(expectedFileName.bold)")
34+
2735

2836
var printer = CodePrinter()
2937
printer.print("// Empty file generated on purpose")
@@ -46,32 +54,49 @@ extension FFMSwift2JavaGenerator {
4654
outputDirectory: self.swiftOutputDirectory,
4755
javaPackagePath: nil,
4856
filename: moduleFilename) {
49-
print("[swift-java] Generated: \(moduleFilenameBase.bold).swift (at \(outputFile))")
57+
log.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))")
5058
self.expectedOutputSwiftFiles.remove(moduleFilename)
5159
}
5260
} catch {
5361
log.warning("Failed to write to Swift thunks: \(moduleFilename)")
5462
}
5563

5664
// === All types
57-
// FIXME: write them all into the same file they were declared from +SwiftJava
58-
for (_, ty) in self.analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) {
59-
let fileNameBase = "\(ty.swiftNominal.qualifiedName)+SwiftJava"
60-
let filename = "\(fileNameBase).swift"
61-
log.debug("Printing contents: \(filename)")
65+
// We have to write all types to their corresponding output file that matches the file they were declared in,
66+
// because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input.
67+
for group: (key: String, value: [Dictionary<String, ImportedNominalType>.Element]) in Dictionary(grouping: self.analysis.importedTypes, by: { $0.value.sourceFilePath }) {
68+
log.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))")
69+
70+
let importedTypesForThisFile = group.value
71+
.map(\.value)
72+
.sorted(by: { $0.qualifiedName < $1.qualifiedName })
73+
74+
let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift"
75+
let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift")
76+
77+
for ty in importedTypesForThisFile {
78+
log.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)")
79+
printer.printSeparator("Thunks for \(ty.qualifiedName)")
80+
81+
do {
82+
try printSwiftThunkSources(&printer, ty: ty)
83+
} catch {
84+
log.warning("Failed to print to Swift thunks for type'\(ty.qualifiedName)' to '\(filename)', error: \(error)")
85+
}
86+
87+
}
6288

89+
log.warning("Write Swift thunks file: \(filename.bold)")
6390
do {
64-
try printSwiftThunkSources(&printer, ty: ty)
65-
6691
if let outputFile = try printer.writeContents(
6792
outputDirectory: self.swiftOutputDirectory,
6893
javaPackagePath: nil,
6994
filename: filename) {
70-
print("[swift-java] Generated: \(fileNameBase.bold).swift (at \(outputFile))")
95+
log.info("Done writing Swift thunks to: \(outputFile.absoluteString)")
7196
self.expectedOutputSwiftFiles.remove(filename)
7297
}
7398
} catch {
74-
log.warning("Failed to write to Swift thunks: \(filename)")
99+
log.warning("Failed to write to Swift thunks: \(filename), error: \(error)")
75100
}
76101
}
77102
}

0 commit comments

Comments
 (0)