Skip to content

Commit afc0c5a

Browse files
committed
jextract: generate one output swift file per input file
Rather than generating an output file per TYPE which we did before. The per-type mode cannot work correctly because SwiftPM expects to know all the output Swift files. So we'd have to parse sources and determine up front what the outputs will be -- doable, but problematic -- instead, we now generate files based on input files, and map which type goes into which output file. This makes it also easier to find where thunks are -- they are in the same named file as the original type or func was declared in. This may have some edge case problems still. Resolves #365
1 parent d7e1959 commit afc0c5a

22 files changed

+320
-145
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+
}
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+
}

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
}

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,33 +60,27 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator {
6060
// If we are forced to write empty files, construct the expected outputs
6161
if translator.config.writeEmptyFiles ?? false {
6262
self.expectedOutputSwiftFiles = Set(translator.inputs.compactMap { (input) -> String? in
63-
guard let filePathPart = input.filePath.split(separator: "/\(translator.swiftModuleName)/").last else {
63+
guard let filePathPart = input.path.split(separator: "/\(translator.swiftModuleName)/").last else {
6464
return nil
6565
}
6666

6767
return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift"))
6868
})
6969
self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)Module+SwiftJava.swift")
70-
71-
// FIXME: Can we avoid this?
72-
self.expectedOutputSwiftFiles.insert("Data+SwiftJava.swift")
70+
self.expectedOutputSwiftFiles.insert("Foundation+SwiftJava.swift")
7371
} else {
7472
self.expectedOutputSwiftFiles = []
7573
}
7674
}
7775

7876
func generate() throws {
7977
try writeSwiftThunkSources()
80-
print("[swift-java] Generated Swift sources (module: '\(self.swiftModuleName)') in: \(swiftOutputDirectory)/")
78+
log.info("Generated Swift sources (module: '\(self.swiftModuleName)') in: \(swiftOutputDirectory)/")
8179

8280
try writeExportedJavaSources()
83-
print("[swift-java] Generated Java sources (package: '\(javaPackage)') in: \(javaOutputDirectory)/")
81+
log.info("Generated Java sources (package: '\(javaPackage)') in: \(javaOutputDirectory)/")
8482

85-
let pendingFileCount = self.expectedOutputSwiftFiles.count
86-
if pendingFileCount > 0 {
87-
print("[swift-java] Write empty [\(pendingFileCount)] 'expected' files in: \(swiftOutputDirectory)/")
88-
try writeSwiftExpectedEmptySources()
89-
}
83+
try writeSwiftExpectedEmptySources()
9084
}
9185
}
9286

@@ -134,7 +128,7 @@ extension FFMSwift2JavaGenerator {
134128
javaPackagePath: javaPackagePath,
135129
filename: filename
136130
) {
137-
print("[swift-java] Generated: \(ty.swiftNominal.name.bold).java (at \(outputFile))")
131+
log.info("Generated: \((ty.swiftNominal.name.bold + ".java").bold) (at \(outputFile.absoluteString))")
138132
}
139133
}
140134

@@ -148,7 +142,7 @@ extension FFMSwift2JavaGenerator {
148142
javaPackagePath: javaPackagePath,
149143
filename: filename)
150144
{
151-
print("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))")
145+
log.info("Generated: \((self.swiftModuleName + ".java").bold) (at \(outputFile.absoluteString))")
152146
}
153147
}
154148
}

Sources/JExtractSwiftLib/ImportedDecls.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,15 @@ package enum SwiftAPIKind {
2727

2828
/// Describes a Swift nominal type (e.g., a class, struct, enum) that has been
2929
/// imported and is being translated into Java.
30-
package class ImportedNominalType: ImportedDecl {
30+
package final class ImportedNominalType: ImportedDecl {
3131
let swiftNominal: SwiftNominalTypeDeclaration
3232

33+
// The short path from module root to the file in which this nominal was originally declared.
34+
// E.g. for `Sources/Example/My/Types.swift` it would be `My/Types.swift`.
35+
package var sourceFilePath: String {
36+
self.swiftNominal.sourceFilePath
37+
}
38+
3339
package var initializers: [ImportedFunc] = []
3440
package var methods: [ImportedFunc] = []
3541
package var variables: [ImportedFunc] = []

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,16 @@ extension JNISwift2JavaGenerator {
2626
}
2727

2828
package func writeSwiftExpectedEmptySources() throws {
29+
let pendingFileCount = self.expectedOutputSwiftFiles.count
30+
guard pendingFileCount > 0 else {
31+
return // no need to write any empty files, yay
32+
}
33+
34+
print("[swift-java] Write empty [\(self.expectedOutputSwiftFiles.count)] 'expected' files in: \(swiftOutputDirectory)/")
35+
2936
for expectedFileName in self.expectedOutputSwiftFiles {
30-
logger.trace("Write empty file: \(expectedFileName) ...")
37+
logger.debug("Write SwiftPM-'expected' empty file: \(expectedFileName.bold)")
38+
3139

3240
var printer = CodePrinter()
3341
printer.print("// Empty file generated on purpose")
@@ -52,27 +60,46 @@ extension JNISwift2JavaGenerator {
5260
javaPackagePath: nil,
5361
filename: moduleFilename
5462
) {
55-
print("[swift-java] Generated: \(moduleFilenameBase.bold).swift (at \(outputFile))")
63+
logger.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))")
5664
self.expectedOutputSwiftFiles.remove(moduleFilename)
5765
}
5866

59-
for (_, ty) in self.analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) {
60-
let fileNameBase = "\(ty.swiftNominal.qualifiedName)+SwiftJava"
61-
let filename = "\(fileNameBase).swift"
62-
logger.debug("Printing contents: \(filename)")
67+
// === All types
68+
// We have to write all types to their corresponding output file that matches the file they were declared in,
69+
// because otherwise SwiftPM plugins will not pick up files apropriately -- we expect 1 output +SwiftJava.swift file for every input.
70+
for group: (key: String, value: [Dictionary<String, ImportedNominalType>.Element]) in Dictionary(grouping: self.analysis.importedTypes, by: { $0.value.sourceFilePath }) {
71+
logger.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))")
6372

64-
do {
65-
try printNominalTypeThunks(&printer, ty)
73+
let importedTypesForThisFile = group.value
74+
.map(\.value)
75+
.sorted(by: { $0.qualifiedName < $1.qualifiedName })
6676

77+
let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift"
78+
let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift")
79+
80+
for ty in importedTypesForThisFile {
81+
logger.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)")
82+
printer.printSeparator("Thunks for \(ty.qualifiedName)")
83+
84+
do {
85+
try printNominalTypeThunks(&printer, ty)
86+
} catch {
87+
logger.warning("Failed to print to Swift thunks for type'\(ty.qualifiedName)' to '\(filename)', error: \(error)")
88+
}
89+
90+
}
91+
92+
logger.warning("Write Swift thunks file: \(filename.bold)")
93+
do {
6794
if let outputFile = try printer.writeContents(
6895
outputDirectory: self.swiftOutputDirectory,
6996
javaPackagePath: nil,
7097
filename: filename) {
71-
print("[swift-java] Generated: \(fileNameBase.bold).swift (at \(outputFile))")
98+
logger.info("Done writing Swift thunks to: \(outputFile.absoluteString)")
7299
self.expectedOutputSwiftFiles.remove(filename)
73100
}
74101
} catch {
75-
logger.warning("Failed to write to Swift thunks: \(filename)")
102+
logger.warning("Failed to write to Swift thunks: \(filename), error: \(error)")
76103
}
77104
}
78105
} catch {

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,14 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator {
6767
// If we are forced to write empty files, construct the expected outputs
6868
if translator.config.writeEmptyFiles ?? false {
6969
self.expectedOutputSwiftFiles = Set(translator.inputs.compactMap { (input) -> String? in
70-
guard let filePathPart = input.filePath.split(separator: "/\(translator.swiftModuleName)/").last else {
70+
guard let filePathPart = input.path.split(separator: "/\(translator.swiftModuleName)/").last else {
7171
return nil
7272
}
7373

7474
return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift"))
7575
})
7676
self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)Module+SwiftJava.swift")
77-
78-
// FIXME: Can we avoid this?
79-
self.expectedOutputSwiftFiles.insert("Data+SwiftJava.swift")
77+
self.expectedOutputSwiftFiles.insert("Foundation+SwiftJava.swift")
8078
} else {
8179
self.expectedOutputSwiftFiles = []
8280
}

0 commit comments

Comments
 (0)