Skip to content

Commit 5aac152

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 5cd11e5 commit 5aac152

16 files changed

+235
-113
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+
}

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: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,19 @@ 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+
35+
if expectedFileName.contains("MultiplePublicTypes") {
36+
log.error("MultiplePublicTypes MUST NOT BE EMPTY!") // FIXME: xxx
37+
}
2738

2839
var printer = CodePrinter()
2940
printer.print("// Empty file generated on purpose")
@@ -46,32 +57,49 @@ extension FFMSwift2JavaGenerator {
4657
outputDirectory: self.swiftOutputDirectory,
4758
javaPackagePath: nil,
4859
filename: moduleFilename) {
49-
print("[swift-java] Generated: \(moduleFilenameBase.bold).swift (at \(outputFile))")
60+
log.info("Generated: \(moduleFilenameBase.bold).swift (at \(outputFile.absoluteString))")
5061
self.expectedOutputSwiftFiles.remove(moduleFilename)
5162
}
5263
} catch {
5364
log.warning("Failed to write to Swift thunks: \(moduleFilename)")
5465
}
5566

5667
// === 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)")
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+
log.warning("Writing types in file group: \(group.key): \(group.value.map(\.key))")
72+
73+
let importedTypesForThisFile = group.value
74+
.map(\.value)
75+
.sorted(by: { $0.qualifiedName < $1.qualifiedName })
76+
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+
log.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)")
82+
printer.printSeparator("Thunks for \(ty.qualifiedName)")
83+
84+
do {
85+
try printSwiftThunkSources(&printer, ty: ty)
86+
} catch {
87+
log.warning("Failed to print to Swift thunks for type'\(ty.qualifiedName)' to '\(filename)', error: \(error)")
88+
}
89+
90+
}
6291

92+
log.warning("Write Swift thunks file: \(filename.bold)")
6393
do {
64-
try printSwiftThunkSources(&printer, ty: ty)
65-
6694
if let outputFile = try printer.writeContents(
6795
outputDirectory: self.swiftOutputDirectory,
6896
javaPackagePath: nil,
6997
filename: filename) {
70-
print("[swift-java] Generated: \(fileNameBase.bold).swift (at \(outputFile))")
98+
log.info("Done writing Swift thunks to: \(outputFile.absoluteString)")
7199
self.expectedOutputSwiftFiles.remove(filename)
72100
}
73101
} catch {
74-
log.warning("Failed to write to Swift thunks: \(filename)")
102+
log.warning("Failed to write to Swift thunks: \(filename), error: \(error)")
75103
}
76104
}
77105
}

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ 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

@@ -77,16 +77,12 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator {
7777

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

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

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-
}
85+
try writeSwiftExpectedEmptySources()
9086
}
9187
}
9288

@@ -134,7 +130,7 @@ extension FFMSwift2JavaGenerator {
134130
javaPackagePath: javaPackagePath,
135131
filename: filename
136132
) {
137-
print("[swift-java] Generated: \(ty.swiftNominal.name.bold).java (at \(outputFile))")
133+
log.info("Generated: \((ty.swiftNominal.name.bold + ".java").bold) (at \(outputFile.absoluteString))")
138134
}
139135
}
140136

@@ -148,7 +144,7 @@ extension FFMSwift2JavaGenerator {
148144
javaPackagePath: javaPackagePath,
149145
filename: filename)
150146
{
151-
print("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))")
147+
log.info("Generated: \((self.swiftModuleName + ".java").bold) (at \(outputFile.absoluteString))")
152148
}
153149
}
154150
}

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.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ 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

Sources/JExtractSwiftLib/Swift2JavaTranslator.swift

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,7 @@ public final class Swift2JavaTranslator {
3232

3333
// ==== Input
3434

35-
struct Input {
36-
let filePath: String
37-
let syntax: SourceFileSyntax
38-
}
39-
40-
var inputs: [Input] = []
35+
var inputs: [SwiftJavaInputFile] = []
4136

4237
/// A list of used Swift class names that live in dependencies, e.g. `JavaInteger`
4338
package var dependenciesClasses: [String] = []
@@ -85,15 +80,12 @@ extension Swift2JavaTranslator {
8580
package func add(filePath: String, text: String) {
8681
log.trace("Adding: \(filePath)")
8782
let sourceFileSyntax = Parser.parse(source: text)
88-
self.inputs.append(Input(filePath: filePath, syntax: sourceFileSyntax))
83+
self.inputs.append(SwiftJavaInputFile(syntax: sourceFileSyntax, path: filePath))
8984
}
9085

9186
/// Convenient method for analyzing single file.
92-
package func analyze(
93-
file: String,
94-
text: String
95-
) throws {
96-
self.add(filePath: file, text: text)
87+
package func analyze(filePath: String, text: String) throws {
88+
self.add(filePath: filePath, text: text)
9789
try self.analyze()
9890
}
9991

@@ -104,16 +96,16 @@ extension Swift2JavaTranslator {
10496
let visitor = Swift2JavaVisitor(translator: self)
10597

10698
for input in self.inputs {
107-
log.trace("Analyzing \(input.filePath)")
108-
visitor.visit(sourceFile: input.syntax)
99+
log.trace("Analyzing \(input.path)")
100+
visitor.visit(inputFile: input)
109101
}
110102

111103
// If any API uses 'Foundation.Data', import 'Data' as if it's declared in
112104
// this module.
113105
if let dataDecl = self.symbolTable[.data] {
114106
let dataProtocolDecl = self.symbolTable[.dataProtocol]!
115107
if self.isUsing(where: { $0 == dataDecl || $0 == dataProtocolDecl }) {
116-
visitor.visit(nominalDecl: dataDecl.syntax!.asNominal!, in: nil)
108+
visitor.visit(nominalDecl: dataDecl.syntax!.asNominal!, in: nil, sourceFilePath: "Foundation/FAKE_FOUNDATION_DATA.swift")
117109
}
118110
}
119111
}
@@ -123,7 +115,7 @@ extension Swift2JavaTranslator {
123115

124116
let symbolTable = SwiftSymbolTable.setup(
125117
moduleName: self.swiftModuleName,
126-
inputs.map({ $0.syntax }) + [dependenciesSource],
118+
inputs + [dependenciesSource],
127119
log: self.log
128120
)
129121
self.lookupContext = SwiftTypeLookupContext(symbolTable: symbolTable)
@@ -184,13 +176,14 @@ extension Swift2JavaTranslator {
184176
}
185177

186178
/// Returns a source file that contains all the available dependency classes.
187-
private func buildDependencyClassesSourceFile() -> SourceFileSyntax {
179+
private func buildDependencyClassesSourceFile() -> SwiftJavaInputFile {
188180
let contents = self.dependenciesClasses.map {
189181
"public class \($0) {}"
190182
}
191183
.joined(separator: "\n")
192184

193-
return SourceFileSyntax(stringLiteral: contents)
185+
let syntax = SourceFileSyntax(stringLiteral: contents)
186+
return SwiftJavaInputFile(syntax: syntax, path: "FakeDependencyClassesSourceFile.swift")
194187
}
195188
}
196189

0 commit comments

Comments
 (0)