Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin {
$0.pathExtension == "swift"
}

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

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

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

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

return [
.buildCommand(
displayName: "Generate Java wrappers for Swift types",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

// This file exists to exercise the swiftpm plugin generating separate output Java files
// for the public types; because Java public types must be in a file with the same name as the type.

public struct PublicTypeOne {
public init() {}
public func test() {}
}

public struct PublicTypeTwo {
public init() {}
public func test() {}
}
2 changes: 2 additions & 0 deletions Samples/SwiftJavaExtractFFMSampleApp/ci-validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
set -x
set -e

swift build # as a workaround for building swift build from within gradle having issues on CI sometimes

./gradlew run
./gradlew test
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

package com.example.swift;

import org.junit.jupiter.api.Test;
import org.swift.swiftkit.core.*;
import org.swift.swiftkit.ffm.*;

import java.lang.foreign.Arena;
import java.util.Optional;
import java.util.OptionalInt;

import static org.junit.jupiter.api.Assertions.*;

public class MultipleTypesFromSingleFileTest {

@Test
void bothTypesMustHaveBeenGenerated() {
try (var arena = AllocatingSwiftArena.ofConfined()) {
PublicTypeOne.init(arena);
PublicTypeTwo.init(arena);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

// This file exists to exercise the swiftpm plugin generating separate output Java files
// for the public types; because Java public types must be in a file with the same name as the type.

public struct PublicTypeOne {
public init() {}
public func test() {}
}

public struct PublicTypeTwo {
public init() {}
public func test() {}
}
3 changes: 2 additions & 1 deletion Samples/SwiftJavaExtractJNISampleApp/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ def jextract = tasks.register("jextract", Exec) {

workingDir = layout.projectDirectory
commandLine "swift"
args("build") // since Swift targets which need to be jextract-ed have the jextract build plugin, we just need to build
// TODO: -v for debugging build issues...
args("build", "-v") // since Swift targets which need to be jextract-ed have the jextract build plugin, we just need to build
// If we wanted to execute a specific subcommand, we can like this:
// args("run",/*
// "swift-java", "jextract",
Expand Down
2 changes: 2 additions & 0 deletions Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
set -x
set -e

swift build # as a workaround for building swift build from within gradle having issues on CI sometimes

./gradlew run
./gradlew test
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift.org project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

package com.example.swift;

import org.junit.jupiter.api.Test;
import org.swift.swiftkit.core.ConfinedSwiftMemorySession;
import org.swift.swiftkit.core.SwiftArena;

import java.lang.foreign.Arena;
import java.util.Optional;
import java.util.OptionalInt;

import static org.junit.jupiter.api.Assertions.*;

public class MultipleTypesFromSingleFileTest {

@Test
void bothTypesMustHaveBeenGenerated() {
try (var arena = SwiftArena.ofConfined()) {
PublicTypeOne.init(arena);
PublicTypeTwo.init(arena);
}
}
}
34 changes: 27 additions & 7 deletions Sources/JExtractSwiftLib/CodePrinter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,28 @@ extension CodePrinter {

/// - Returns: the output path of the generated file, if any (i.e. not in accumulate in memory mode)
package mutating func writeContents(
outputDirectory: String,
outputDirectory _outputDirectory: String,
javaPackagePath: String?,
filename: String
filename _filename: String
) throws -> URL? {

// We handle 'filename' that has a path, since that simplifies passing paths from root output directory enourmously.
// This just moves the directory parts into the output directory part in order for us to create the sub-directories.
let outputDirectory: String
let filename: String
if _filename.contains(PATH_SEPARATOR) {
let parts = _filename.split(separator: PATH_SEPARATOR)
outputDirectory = _outputDirectory.appending(PATH_SEPARATOR).appending(parts.dropLast().joined(separator: PATH_SEPARATOR))
filename = "\(parts.last!)"
} else {
outputDirectory = _outputDirectory
filename = _filename
}

guard self.mode != .accumulateAll else {
// if we're accumulating everything, we don't want to finalize/flush any contents
// let's mark that this is where a write would have happened though:
print("// ^^^^ Contents of: \(outputDirectory)/\(filename)")
print("// ^^^^ Contents of: \(outputDirectory)\(PATH_SEPARATOR)\(filename)")
return nil
}

Expand All @@ -233,7 +247,7 @@ extension CodePrinter {
"// ==== ---------------------------------------------------------------------------------------------------"
)
if let javaPackagePath {
print("// \(javaPackagePath)/\(filename)")
print("// \(javaPackagePath)\(PATH_SEPARATOR)\(filename)")
} else {
print("// \(filename)")
}
Expand All @@ -242,9 +256,15 @@ extension CodePrinter {
}

let targetDirectory = [outputDirectory, javaPackagePath].compactMap { $0 }.joined(separator: PATH_SEPARATOR)
log.trace("Prepare target directory: \(targetDirectory)")
try FileManager.default.createDirectory(
atPath: targetDirectory, withIntermediateDirectories: true)
log.debug("Prepare target directory: '\(targetDirectory)' for file \(filename.bold)")
do {
try FileManager.default.createDirectory(
atPath: targetDirectory, withIntermediateDirectories: true)
} catch {
// log and throw since it can be confusing what the reason for failing the write was otherwise
log.warning("Failed to create directory: \(targetDirectory)")
throw error
}

let outputPath = Foundation.URL(fileURLWithPath: targetDirectory).appendingPathComponent(filename)
try contents.write(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ extension FFMSwift2JavaGenerator {
}

package func writeSwiftExpectedEmptySources() throws {
let pendingFileCount = self.expectedOutputSwiftFiles.count
guard pendingFileCount > 0 else {
return // no need to write any empty files, yay
}

print("[swift-java] Write empty [\(self.expectedOutputSwiftFiles.count)] 'expected' files in: \(swiftOutputDirectory)/")

for expectedFileName in self.expectedOutputSwiftFiles {
log.trace("Write empty file: \(expectedFileName) ...")
log.debug("Write SwiftPM-'expected' empty file: \(expectedFileName.bold)")


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

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

let importedTypesForThisFile = group.value
.map(\.value)
.sorted(by: { $0.qualifiedName < $1.qualifiedName })

let inputFileName = "\(group.key)".split(separator: "/").last ?? "__Unknown.swift"
let filename = "\(inputFileName)".replacing(".swift", with: "+SwiftJava.swift")

for ty in importedTypesForThisFile {
log.info("Printing Swift thunks for type: \(ty.qualifiedName.bold)")
printer.printSeparator("Thunks for \(ty.qualifiedName)")

do {
try printSwiftThunkSources(&printer, ty: ty)
} catch {
log.warning("Failed to print to Swift thunks for type'\(ty.qualifiedName)' to '\(filename)', error: \(error)")
}

}

log.warning("Write Swift thunks file: \(filename.bold)")
do {
try printSwiftThunkSources(&printer, ty: ty)

if let outputFile = try printer.writeContents(
outputDirectory: self.swiftOutputDirectory,
javaPackagePath: nil,
filename: filename) {
print("[swift-java] Generated: \(fileNameBase.bold).swift (at \(outputFile))")
log.info("Done writing Swift thunks to: \(outputFile.absoluteString)")
self.expectedOutputSwiftFiles.remove(filename)
}
} catch {
log.warning("Failed to write to Swift thunks: \(filename)")
log.warning("Failed to write to Swift thunks: \(filename), error: \(error)")
}
}
}
Expand Down
20 changes: 7 additions & 13 deletions Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,33 +60,27 @@ package class FFMSwift2JavaGenerator: Swift2JavaGenerator {
// If we are forced to write empty files, construct the expected outputs
if translator.config.writeEmptyFiles ?? false {
self.expectedOutputSwiftFiles = Set(translator.inputs.compactMap { (input) -> String? in
guard let filePathPart = input.filePath.split(separator: "/\(translator.swiftModuleName)/").last else {
guard let filePathPart = input.path.split(separator: "/\(translator.swiftModuleName)/").last else {
return nil
}

return String(filePathPart.replacing(".swift", with: "+SwiftJava.swift"))
})
self.expectedOutputSwiftFiles.insert("\(translator.swiftModuleName)Module+SwiftJava.swift")

// FIXME: Can we avoid this?
self.expectedOutputSwiftFiles.insert("Data+SwiftJava.swift")
self.expectedOutputSwiftFiles.insert("Foundation+SwiftJava.swift")
} else {
self.expectedOutputSwiftFiles = []
}
}

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

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

let pendingFileCount = self.expectedOutputSwiftFiles.count
if pendingFileCount > 0 {
print("[swift-java] Write empty [\(pendingFileCount)] 'expected' files in: \(swiftOutputDirectory)/")
try writeSwiftExpectedEmptySources()
}
try writeSwiftExpectedEmptySources()
}
}

Expand Down Expand Up @@ -134,7 +128,7 @@ extension FFMSwift2JavaGenerator {
javaPackagePath: javaPackagePath,
filename: filename
) {
print("[swift-java] Generated: \(ty.swiftNominal.name.bold).java (at \(outputFile))")
log.info("Generated: \((ty.swiftNominal.name.bold + ".java").bold) (at \(outputFile.absoluteString))")
}
}

Expand All @@ -148,7 +142,7 @@ extension FFMSwift2JavaGenerator {
javaPackagePath: javaPackagePath,
filename: filename)
{
print("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))")
log.info("Generated: \((self.swiftModuleName + ".java").bold) (at \(outputFile.absoluteString))")
}
}
}
Expand Down
Loading