diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index eea13444..ee29f0cc 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -19,7 +19,8 @@ jobs: strategy: fail-fast: true matrix: - swift_version: ['nightly-main'] + # swift_version: ['nightly-main'] + swift_version: ['6.0.2'] os_version: ['jammy'] jdk_vendor: ['Corretto'] container: @@ -43,8 +44,8 @@ jobs: if: steps.cache-jdk.outputs.cache-hit != 'true' run: "bash -xc 'JDK_VENDOR=${{ matrix.jdk_vendor }} ./docker/install_jdk.sh'" # TODO: not using setup-java since incompatible with the swiftlang/swift base image - - name: Install Untested Nightly Swift - run: "bash -xc './docker/install_untested_nightly_swift.sh'" + # - name: Install Untested Nightly Swift + # run: "bash -xc './docker/install_untested_nightly_swift.sh'" - name: Cache local Gradle repository uses: actions/cache@v4 continue-on-error: true @@ -79,7 +80,8 @@ jobs: strategy: fail-fast: false matrix: - swift_version: ['nightly-main'] + # swift_version: ['nightly-main'] + swift_version: ['6.0.2'] os_version: ['jammy'] jdk_vendor: ['Corretto'] container: @@ -103,8 +105,8 @@ jobs: if: steps.cache-jdk.outputs.cache-hit != 'true' run: "bash -xc 'JDK_VENDOR=${{ matrix.jdk_vendor }} ./docker/install_jdk.sh'" # TODO: not using setup-java since incompatible with the swiftlang/swift base image - - name: Install Untested Nightly Swift - run: "bash -xc './docker/install_untested_nightly_swift.sh'" + # - name: Install Untested Nightly Swift + # run: "bash -xc './docker/install_untested_nightly_swift.sh'" - name: Cache local Gradle repository uses: actions/cache@v4 continue-on-error: true diff --git a/.licenseignore b/.licenseignore index 02866b01..0825283e 100644 --- a/.licenseignore +++ b/.licenseignore @@ -37,3 +37,5 @@ Makefile gradle/wrapper/gradle-wrapper.properties gradlew gradlew.bat +**/gradlew +**/gradlew.bat diff --git a/Package.swift b/Package.swift index cd456d76..523e7c58 100644 --- a/Package.swift +++ b/Package.swift @@ -126,6 +126,14 @@ let package = Package( targets: ["JExtractSwift"] ), + // ==== Plugin for wrapping Java classes in Swift + .plugin( + name: "JExtractSwiftPlugin", + targets: [ + "JExtractSwiftPlugin" + ] + ), + // ==== Examples .library( @@ -229,6 +237,7 @@ let package = Package( .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) ] ), + .plugin( name: "JavaCompilerPlugin", capability: .buildTool() @@ -331,6 +340,14 @@ let package = Package( ] ), + .plugin( + name: "JExtractSwiftPlugin", + capability: .buildTool(), + dependencies: [ + "JExtractSwiftTool" + ] + ), + .testTarget( name: "JavaKitTests", dependencies: ["JavaKit", "JavaKitNetwork"], diff --git a/Plugins/JExtractSwiftPlugin/Configuration.swift b/Plugins/JExtractSwiftPlugin/Configuration.swift new file mode 100644 index 00000000..042c4952 --- /dev/null +++ b/Plugins/JExtractSwiftPlugin/Configuration.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// Configuration for the JExtractSwift translation tool, provided on a per-target +/// basis. +struct Configuration: Codable { + var javaPackage: String +} + +func readConfiguration(sourceDir: String) throws -> Configuration { + let configFile = URL(filePath: sourceDir).appending(path: "JExtractSwift.config") + do { + let configData = try Data(contentsOf: configFile) + return try JSONDecoder().decode(Configuration.self, from: configData) + } catch { + throw ConfigurationError(message: "Failed to parse JExtractSwift configuration at '\(configFile)!'", error: error) + } +} + +struct ConfigurationError: Error { + let message: String + let error: any Error +} diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift new file mode 100644 index 00000000..9e175fe0 --- /dev/null +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +@main +struct JExtractSwiftBuildToolPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + guard let sourceModule = target.sourceModule else { return [] } + + // Note: Target doesn't have a directoryURL counterpart to directory, + // so we cannot eliminate this deprecation warning. + let sourceDir = target.directory.string + + let configuration = try readConfiguration(sourceDir: "\(sourceDir)") + + // We use the the usual maven-style structure of "src/[generated|main|test]/java/..." + // that is common in JVM ecosystem + let outputDirectoryJava = context.pluginWorkDirectoryURL + .appending(path: "src") + .appending(path: "generated") + .appending(path: "java") + let outputDirectorySwift = context.pluginWorkDirectoryURL + .appending(path: "src") + .appending(path: "generated") + .appending(path: "Sources") + + var arguments: [String] = [ + "--swift-module", sourceModule.name, + "--package-name", configuration.javaPackage, + "--output-directory-java", outputDirectoryJava.path(percentEncoded: false), + "--output-directory-swift", outputDirectorySwift.path(percentEncoded: false), + // TODO: "--build-cache-directory", ... + // Since plugins cannot depend on libraries we cannot detect what the output files will be, + // as it depends on the contents of the input files. Therefore we have to implement this as a prebuild plugin. + // We'll have to make up some caching inside the tool so we don't re-parse files which have not changed etc. + ] + arguments.append(sourceDir) + + return [ + .prebuildCommand( + displayName: "Generate Java wrappers for Swift types", + executable: try context.tool(named: "JExtractSwiftTool").url, + arguments: arguments, + // inputFiles: [ configFile ] + swiftFiles, + // outputFiles: outputJavaFiles + outputFilesDirectory: outputDirectorySwift + ) + ] + } +} + +// Note: the JAVA_HOME environment variable must be set to point to where +// Java is installed, e.g., +// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. +func findJavaHome() -> String { + if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { + return home + } + + // This is a workaround for envs (some IDEs) which have trouble with + // picking up env variables during the build process + let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" + if let home = try? String(contentsOfFile: path, encoding: .utf8) { + if let lastChar = home.last, lastChar.isNewline { + return String(home.dropLast()) + } + + return home + } + + fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") +} diff --git a/Samples/JExtractPluginSampleApp/.gitignore b/Samples/JExtractPluginSampleApp/.gitignore new file mode 100644 index 00000000..0023a534 --- /dev/null +++ b/Samples/JExtractPluginSampleApp/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Samples/JExtractPluginSampleApp/Package.swift b/Samples/JExtractPluginSampleApp/Package.swift new file mode 100644 index 00000000..26215e55 --- /dev/null +++ b/Samples/JExtractPluginSampleApp/Package.swift @@ -0,0 +1,70 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +import class Foundation.FileManager +import class Foundation.ProcessInfo + +// Note: the JAVA_HOME environment variable must be set to point to where +// Java is installed, e.g., +// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. +func findJavaHome() -> String { + if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { + return home + } + + // This is a workaround for envs (some IDEs) which have trouble with + // picking up env variables during the build process + let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" + if let home = try? String(contentsOfFile: path, encoding: .utf8) { + if let lastChar = home.last, lastChar.isNewline { + return String(home.dropLast()) + } + + return home + } + + fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") +} +let javaHome = findJavaHome() + +let javaIncludePath = "\(javaHome)/include" +#if os(Linux) + let javaPlatformIncludePath = "\(javaIncludePath)/linux" +#elseif os(macOS) + let javaPlatformIncludePath = "\(javaIncludePath)/darwin" +#else + // TODO: Handle windows as well + #error("Currently only macOS and Linux platforms are supported, this may change in the future.") +#endif + +let package = Package( + name: "JExtractPluginSampleApp", + platforms: [ + .macOS(.v10_15), + ], + products: [ + .library( + name: "JExtractPluginSampleLib", + type: .dynamic, + targets: ["JExtractPluginSampleLib"] + ), + ], + dependencies: [ + .package(name: "swift-java", path: "../../"), + ], + targets: [ + .target( + name: "JExtractPluginSampleLib", + dependencies: [ + ], + swiftSettings: [ + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) + ], + plugins: [ + .plugin(name: "JExtractSwiftPlugin", package: "swift-java"), + ] + ), + ] +) diff --git a/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/JExtractSwift.config b/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/JExtractSwift.config new file mode 100644 index 00000000..6e5bc2af --- /dev/null +++ b/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/JExtractSwift.config @@ -0,0 +1,3 @@ +{ + "javaPackage": "com.example.swift" +} diff --git a/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/MyCoolSwiftClass.swift b/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/MyCoolSwiftClass.swift new file mode 100644 index 00000000..e87a3af4 --- /dev/null +++ b/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/MyCoolSwiftClass.swift @@ -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 +// +//===----------------------------------------------------------------------===// + +public class MyCoolSwiftClass { + var number: Int + public init(number: Int) { + print("[swift] init(number: \(number))") + self.number = number + } + + public func exposedToJava() { + print("[swift] exposedToJava()") + print("[swift] number = \(number)") + } +} diff --git a/Samples/JExtractPluginSampleApp/build.gradle b/Samples/JExtractPluginSampleApp/build.gradle new file mode 100644 index 00000000..e6febde7 --- /dev/null +++ b/Samples/JExtractPluginSampleApp/build.gradle @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import org.swift.swiftkit.gradle.BuildUtils + +import java.nio.file.Files +import java.nio.file.Paths + +plugins { + id("build-logic.java-application-conventions") +// id("me.champeau.jmh") version "0.7.2" +} + +group = "org.swift.swiftkit" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(22)) + } +} + + +// This is for development, when we edit the Swift swift-java project, the outputs of the generated sources may change. +// Thus, we also need to watch and re-build the top level project. +def buildJExtractPlugin = tasks.register("swiftBuildJExtractPlugin", Exec) { + description = "Rebuild the swift-java root project" + + inputs.file(new File(rootDir, "Package.swift")) + inputs.dir(new File(rootDir, "Sources")) + outputs.dir(new File(rootDir, ".build")) + + workingDir = rootDir + commandLine "swift" + args "build" +} + +def buildSwift = tasks.register("swiftBuildProject", Exec) { + description = "Builds swift sources, including swift-java source generation" + dependsOn buildJExtractPlugin + + // only because we depend on "live developing" the plugin while using this project to test it + inputs.file(new File(rootDir, "Package.swift")) + inputs.dir(new File(rootDir, "Sources")) + + inputs.file(layout.projectDirectory.file("Package.swift")) + inputs.dir(layout.projectDirectory.dir("Sources")) + + // TODO: we can use package describe --type json to figure out which targets depend on JExtractSwiftPlugin and will produce outputs + // Avoid adding this directory, but create the expected one specifically for all targets which WILL produce sources because they have the plugin + outputs.dir(layout.buildDirectory.dir("../.build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}")) + + File baseSwiftPluginOutputsDir = layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile + if (!baseSwiftPluginOutputsDir.exists()) { + baseSwiftPluginOutputsDir.mkdirs() + } + Files.walk(layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile.toPath()).each { + if (it.endsWith("JExtractSwiftPlugin/src/generated/java")) { + outputs.dir(it) + } + } + + workingDir = layout.projectDirectory + commandLine "swift" + args "build" +} + +// Add the java-swift generated Java sources +sourceSets { + main { + java { + srcDir(buildSwift) + } + } +} + +dependencies { + implementation(project(':SwiftKit')) + + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") + + // implementation(swiftDynamicLibrary('') +} + +tasks.named('test', Test) { + useJUnitPlatform() +} + +application { + mainClass = "com.example.swift.JExtractPluginSampleMain" + + // In order to silence: + // WARNING: A restricted method in java.lang.foreign.SymbolLookup has been called + // WARNING: java.lang.foreign.SymbolLookup::libraryLookup has been called by org.example.swift.JavaKitExample in an unnamed module + // WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module + // WARNING: Restricted methods will be blocked in a future release unless native access is enabled + // FIXME: Find out the proper solution to this + applicationDefaultJvmArgs = [ + "--enable-native-access=ALL-UNNAMED", + + // Include the library paths where our dylibs are that we want to load and call + "-Djava.library.path=" + + (BuildUtils.javaLibraryPaths(rootDir) + + BuildUtils.javaLibraryPaths(layout.projectDirectory.asFile)).join(":"), + + // Enable tracing downcalls (to Swift) + "-Djextract.trace.downcalls=true" + ] +} diff --git a/Samples/JExtractPluginSampleApp/gradlew b/Samples/JExtractPluginSampleApp/gradlew new file mode 120000 index 00000000..343e0d2c --- /dev/null +++ b/Samples/JExtractPluginSampleApp/gradlew @@ -0,0 +1 @@ +../../gradlew \ No newline at end of file diff --git a/Samples/JExtractPluginSampleApp/gradlew.bat b/Samples/JExtractPluginSampleApp/gradlew.bat new file mode 120000 index 00000000..cb5a9464 --- /dev/null +++ b/Samples/JExtractPluginSampleApp/gradlew.bat @@ -0,0 +1 @@ +../../gradlew.bat \ No newline at end of file diff --git a/Samples/JExtractPluginSampleApp/src/main/java/com/example/swift/JExtractPluginSampleMain.java b/Samples/JExtractPluginSampleApp/src/main/java/com/example/swift/JExtractPluginSampleMain.java new file mode 100644 index 00000000..d1ac8320 --- /dev/null +++ b/Samples/JExtractPluginSampleApp/src/main/java/com/example/swift/JExtractPluginSampleMain.java @@ -0,0 +1,28 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.swift.swiftkit.SwiftKit; + +public class JExtractPluginSampleMain { + public static void main(String[] args) { + System.out.println(); + System.out.println("java.library.path = " + SwiftKit.getJavaLibraryPath()); + System.out.println("jextract.trace.downcalls = " + SwiftKit.getJextractTraceDowncalls()); + + var o = new MyCoolSwiftClass(12); + o.exposedToJava(); + } +} diff --git a/Samples/JavaSieve/Sources/JavaSieve/main.swift b/Samples/JavaSieve/Sources/JavaSieve/main.swift index c075a8a1..b06cc6bb 100644 --- a/Samples/JavaSieve/Sources/JavaSieve/main.swift +++ b/Samples/JavaSieve/Sources/JavaSieve/main.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import JavaKit +import JavaMath let jvm = try JavaVirtualMachine.shared(classPath: ["QuadraticSieve-1.0.jar"]) do { @@ -20,6 +21,8 @@ do { for prime in sieveClass.findPrimes(100)! { print("Found prime: \(prime.intValue())") } + + try JavaClass().HALF_UP } catch { print("Failure: \(error)") } diff --git a/Samples/SwiftKitSampleApp/build.gradle b/Samples/SwiftKitSampleApp/build.gradle index 3b79bd72..cbc7ee3e 100644 --- a/Samples/SwiftKitSampleApp/build.gradle +++ b/Samples/SwiftKitSampleApp/build.gradle @@ -56,6 +56,10 @@ sourceSets { } } +tasks.build { + dependsOn("jextract") +} + dependencies { implementation(project(':SwiftKit')) diff --git a/Sources/JExtractSwift/CodePrinter.swift b/Sources/JExtractSwift/CodePrinter.swift index 308db69e..24fe0313 100644 --- a/Sources/JExtractSwift/CodePrinter.swift +++ b/Sources/JExtractSwift/CodePrinter.swift @@ -34,14 +34,19 @@ public struct CodePrinter { } public var indentationText: String = "" - public static func toString(_ block: (inout CodePrinter) -> ()) -> String { + public static func toString(_ block: (inout CodePrinter) throws -> ()) rethrows -> String { var printer = CodePrinter() - block(&printer) + try block(&printer) return printer.finalize() } - public init() { - + var ioMode: PrintMode + public enum PrintMode { + case accumulateAll + case flushToFileOnWrite + } + public init(io: PrintMode = .flushToFileOnWrite) { + self.ioMode = .accumulateAll } internal mutating func append(_ text: String) { @@ -91,6 +96,16 @@ public struct CodePrinter { } } + /// Print a plain newline, e.g. to separate declarations. + public mutating func println( + _ terminator: PrinterTerminator = .newLine, + function: String = #function, + file: String = #fileID, + line: UInt = #line + ) { + print("") + } + public mutating func print( _ text: Any, _ terminator: PrinterTerminator = .newLine, @@ -159,6 +174,12 @@ public struct CodePrinter { public var isEmpty: Bool { self.contents.isEmpty } + + public mutating func dump(file: String = #fileID, line: UInt = #line) { + Swift.print("// CodePrinter.dump @ \(file):\(line)") + Swift.print(contents) + } + } public enum PrinterTerminator: String { @@ -185,3 +206,49 @@ public enum PrinterTerminator: String { } } } + +extension CodePrinter { + package mutating func writeContents( + outputDirectory: String, + javaPackagePath: String?, + filename: String + ) throws { + guard self.ioMode != .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)") + return + } + + let contents = finalize() + if outputDirectory == "-" { + print( + "// ==== ---------------------------------------------------------------------------------------------------" + ) + if let javaPackagePath { + print("// \(javaPackagePath)/\(filename)") + } else { + print("// \(filename)") + } + print(contents) + return + } + + let targetDirectory = [outputDirectory, javaPackagePath].compactMap { $0 }.joined( + separator: PATH_SEPARATOR) + log.trace("Prepare target directory: \(targetDirectory)") + try FileManager.default.createDirectory( + atPath: targetDirectory, withIntermediateDirectories: true) + + let targetFilePath = [javaPackagePath, filename].compactMap { $0 }.joined( + separator: PATH_SEPARATOR) + Swift.print("Writing '\(targetFilePath)'...", terminator: "") + try contents.write( + to: Foundation.URL(fileURLWithPath: targetDirectory).appendingPathComponent(filename), + atomically: true, + encoding: .utf8 + ) + Swift.print(" done.".green) + } + +} \ No newline at end of file diff --git a/Sources/JExtractSwift/Convenience/Collection+Extensions.swift b/Sources/JExtractSwift/Convenience/Collection+Extensions.swift index 5df901d2..fcb817d7 100644 --- a/Sources/JExtractSwift/Convenience/Collection+Extensions.swift +++ b/Sources/JExtractSwift/Convenience/Collection+Extensions.swift @@ -39,3 +39,13 @@ extension Collection { } } } + +extension Collection where Element == Int { + var sum: Int { + var s = 0 + for i in self { + s += i + } + return s + } +} \ No newline at end of file diff --git a/Sources/JExtractSwift/ImportedDecls.swift b/Sources/JExtractSwift/ImportedDecls.swift index 56c3bd6b..873ec98e 100644 --- a/Sources/JExtractSwift/ImportedDecls.swift +++ b/Sources/JExtractSwift/ImportedDecls.swift @@ -28,17 +28,15 @@ public typealias JavaPackage = String public struct ImportedNominalType: ImportedDecl { public let swiftTypeName: String public let javaType: JavaType - public var swiftMangledName: String? public var kind: NominalTypeKind public var initializers: [ImportedFunc] = [] public var methods: [ImportedFunc] = [] public var variables: [ImportedVariable] = [] - public init(swiftTypeName: String, javaType: JavaType, swiftMangledName: String? = nil, kind: NominalTypeKind) { + public init(swiftTypeName: String, javaType: JavaType, kind: NominalTypeKind) { self.swiftTypeName = swiftTypeName self.javaType = javaType - self.swiftMangledName = swiftMangledName self.kind = kind } @@ -55,7 +53,7 @@ public struct ImportedNominalType: ImportedDecl { /// The Java class name without the package. public var javaClassName: String { switch javaType { - case .class(package: _, name: let name): name + case .class(package: _, let name): name default: javaType.description } } @@ -69,10 +67,10 @@ public enum NominalTypeKind { } public struct ImportedParam { - let param: FunctionParameterSyntax + let syntax: FunctionParameterSyntax var firstName: String? { - let text = param.firstName.trimmed.text + let text = syntax.firstName.trimmed.text guard text != "_" else { return nil } @@ -81,7 +79,7 @@ public struct ImportedParam { } var secondName: String? { - let text = param.secondName?.trimmed.text + let text = syntax.secondName?.trimmed.text guard text != "_" else { return nil } @@ -95,7 +93,7 @@ public struct ImportedParam { // The Swift type as-is from the swift interface var swiftType: String { - param.type.trimmed.description + syntax.type.trimmed.description } // The mapped-to Java type of the above Java type, collections and optionals may be replaced with Java ones etc. @@ -116,22 +114,33 @@ extension ImportedParam { } // TODO: this is used in different contexts and needs a cleanup +// Perhaps this is "which parameter passing style"? public enum SelfParameterVariant { + // ==== Java forwarding patterns + /// Make a method that accepts the raw memory pointer as a MemorySegment case memorySegment /// Make a method that accepts the the Java wrapper class of the type case wrapper /// Raw SWIFT_POINTER case pointer + + // ==== Swift forwarding patterns + + case swiftThunkSelf } public struct ImportedFunc: ImportedDecl, CustomStringConvertible { + + /// Swift module name (e.g. the target name where a type or function was declared) + public var module: String + /// If this function/method is member of a class/struct/protocol, /// this will contain that declaration's imported name. /// /// This is necessary when rendering accessor Java code we need the type that "self" is expecting to have. - public var parentName: TranslatedType? - public var hasParent: Bool { parentName != nil } + public var parent: TranslatedType? + public var hasParent: Bool { parent != nil } /// This is a full name such as init(cap:name:). public var identifier: String @@ -148,8 +157,8 @@ public struct ImportedFunc: ImportedDecl, CustomStringConvertible { /// A display name to use to refer to the Swift declaration with its /// enclosing type, if there is one. public var displayName: String { - if let parentName { - return "\(parentName.swiftTypeName).\(identifier)" + if let parent { + return "\(parent.swiftTypeName).\(identifier)" } return identifier @@ -158,36 +167,33 @@ public struct ImportedFunc: ImportedDecl, CustomStringConvertible { public var returnType: TranslatedType public var parameters: [ImportedParam] - public func effectiveParameters(selfVariant: SelfParameterVariant?) -> [ImportedParam] { - if let parentName { + public func effectiveParameters(paramPassingStyle: SelfParameterVariant?) -> [ImportedParam] { + if let parent { var params = parameters // Add `self: Self` for method calls on a member // // allocating initializer takes a Self.Type instead, but it's also a pointer - switch selfVariant { + switch paramPassingStyle { case nil, .wrapper: break case .pointer: let selfParam: FunctionParameterSyntax = "self$: $swift_pointer" params.append( - ImportedParam( - param: selfParam, - type: parentName - ) + ImportedParam(syntax: selfParam, type: parent) ) case .memorySegment: let selfParam: FunctionParameterSyntax = "self$: $java_lang_foreign_MemorySegment" - var parentForSelf = parentName + var parentForSelf = parent parentForSelf.javaType = .javaForeignMemorySegment params.append( - ImportedParam( - param: selfParam, - type: parentForSelf - ) + ImportedParam(syntax: selfParam, type: parentForSelf) ) + + case .swiftThunkSelf: + break } // TODO: add any metadata for generics and other things we may need to add here @@ -198,19 +204,25 @@ public struct ImportedFunc: ImportedDecl, CustomStringConvertible { } } - public var swiftMangledName: String = "" + public var swiftDecl: any DeclSyntaxProtocol - public var syntax: String? = nil + public var syntax: String? { + "\(self.swiftDecl)" + } public var isInit: Bool = false public init( - parentName: TranslatedType?, + module: String, + decl: any DeclSyntaxProtocol, + parent: TranslatedType?, identifier: String, returnType: TranslatedType, parameters: [ImportedParam] ) { - self.parentName = parentName + self.swiftDecl = decl + self.module = module + self.parent = parent self.identifier = identifier self.returnType = returnType self.parameters = parameters @@ -219,7 +231,6 @@ public struct ImportedFunc: ImportedDecl, CustomStringConvertible { public var description: String { """ ImportedFunc { - mangledName: \(swiftMangledName) identifier: \(identifier) returnType: \(returnType) parameters: \(parameters) @@ -238,6 +249,9 @@ public enum VariableAccessorKind { } public struct ImportedVariable: ImportedDecl, CustomStringConvertible { + + public var module: String + /// If this function/method is member of a class/struct/protocol, /// this will contain that declaration's imported name. /// @@ -249,7 +263,7 @@ public struct ImportedVariable: ImportedDecl, CustomStringConvertible { public var identifier: String /// Which accessors are we able to expose. - /// + /// /// Usually this will be all the accessors the variable declares, /// however if the getter is async or throwing we may not be able to import it /// (yet), and therefore would skip it from the supported set. @@ -284,51 +298,54 @@ public struct ImportedVariable: ImportedDecl, CustomStringConvertible { switch kind { case .set: - let newValueParam: FunctionParameterSyntax = "_ newValue: \(self.returnType.cCompatibleSwiftType)" + let newValueParam: FunctionParameterSyntax = + "_ newValue: \(self.returnType.cCompatibleSwiftType)" var funcDecl = ImportedFunc( - parentName: self.parentName, + module: self.module, + decl: self.syntax!, + parent: self.parentName, identifier: self.identifier, returnType: TranslatedType.void, - parameters: [.init(param: newValueParam, type: self.returnType)]) - funcDecl.swiftMangledName = self.swiftMangledName + "s" // form mangled name of the getter by adding the suffix + parameters: [.init(syntax: newValueParam, type: self.returnType)]) return funcDecl case .get: - var funcDecl = ImportedFunc( - parentName: self.parentName, + let funcDecl = ImportedFunc( + module: self.module, + decl: self.syntax!, + parent: self.parentName, identifier: self.identifier, returnType: self.returnType, parameters: []) - funcDecl.swiftMangledName = self.swiftMangledName + "g" // form mangled name of the getter by adding the suffix return funcDecl } } - public func effectiveAccessorParameters(_ kind: VariableAccessorKind, selfVariant: SelfParameterVariant?) -> [ImportedParam] { + public func effectiveAccessorParameters( + _ kind: VariableAccessorKind, paramPassingStyle: SelfParameterVariant? + ) -> [ImportedParam] { var params: [ImportedParam] = [] if kind == .set { - let newValueParam: FunctionParameterSyntax = "_ newValue: \(raw: self.returnType.swiftTypeName)" + let newValueParam: FunctionParameterSyntax = + "_ newValue: \(raw: self.returnType.swiftTypeName)" params.append( ImportedParam( - param: newValueParam, + syntax: newValueParam, type: self.returnType) - ) + ) } if let parentName { // Add `self: Self` for method calls on a member // // allocating initializer takes a Self.Type instead, but it's also a pointer - switch selfVariant { - case nil, .wrapper: - break - + switch paramPassingStyle { case .pointer: let selfParam: FunctionParameterSyntax = "self$: $swift_pointer" params.append( ImportedParam( - param: selfParam, + syntax: selfParam, type: parentName ) ) @@ -339,10 +356,15 @@ public struct ImportedVariable: ImportedDecl, CustomStringConvertible { parentForSelf.javaType = .javaForeignMemorySegment params.append( ImportedParam( - param: selfParam, + syntax: selfParam, type: parentForSelf ) ) + + case nil, + .wrapper, + .swiftThunkSelf: + break } } @@ -354,10 +376,12 @@ public struct ImportedVariable: ImportedDecl, CustomStringConvertible { public var syntax: VariableDeclSyntax? = nil public init( + module: String, parentName: TranslatedType?, identifier: String, returnType: TranslatedType ) { + self.module = module self.parentName = parentName self.identifier = identifier self.returnType = returnType diff --git a/Sources/JExtractSwift/Logger.swift b/Sources/JExtractSwift/Logger.swift index 7309c3d7..6a3dcc1b 100644 --- a/Sources/JExtractSwift/Logger.swift +++ b/Sources/JExtractSwift/Logger.swift @@ -88,7 +88,7 @@ public struct Logger { } let metadataString: String = - metadata.isEmpty ? "\(metadata)" : "" + metadata.isEmpty ? "" : "\(metadata)" print("[trace][\(file):\(line)](\(function)) \(message()) \(metadataString)") } diff --git a/Sources/JExtractSwift/Swift2Java.swift b/Sources/JExtractSwift/Swift2Java.swift index e01143cc..4848d051 100644 --- a/Sources/JExtractSwift/Swift2Java.swift +++ b/Sources/JExtractSwift/Swift2Java.swift @@ -28,22 +28,29 @@ public struct SwiftToJava: ParsableCommand { @Option(help: "The package the generated Java code should be emitted into.") var packageName: String - @Option(name: .shortAndLong, help: "The directory in which to output the generated Swift files and manifest.") - var outputDirectory: String = ".build/jextract-swift/generated" + @Option( + name: .shortAndLong, + help: "The directory in which to output the generated Swift files and manifest.") + var outputDirectoryJava: String = ".build/jextract-swift/generated" - @Option(name: .long, help: "Name of the Swift module to import (and the swift interface files belong to)") + @Option(help: "Swift output directory") + var outputDirectorySwift: String + + @Option( + name: .long, + help: "Name of the Swift module to import (and the swift interface files belong to)") var swiftModule: String // TODO: Once we ship this, make this `.warning` by default @Option(name: .shortAndLong, help: "Configure the level of lots that should be printed") var logLevel: Logger.Level = .notice - @Argument(help: "The Swift interface files to export to Java.") - var swiftInterfaceFiles: [String] + @Argument(help: "The Swift files or directories to recursively export to Java.") + var input: [String] public func run() throws { - let interfaceFiles = self.swiftInterfaceFiles.dropFirst() - print("Interface files: \(interfaceFiles)") + let inputPaths = self.input.dropFirst().map { URL(string: $0)! } + print("Input \(inputPaths)") let translator = Swift2JavaTranslator( javaPackage: packageName, @@ -51,20 +58,34 @@ public struct SwiftToJava: ParsableCommand { ) translator.log.logLevel = logLevel - var fileNo = 1 - for interfaceFile in interfaceFiles { - print("[\(fileNo)/\(interfaceFiles.count)] Importing module '\(swiftModule)', interface file: \(interfaceFile)") - defer { fileNo += 1 } + var allFiles: [URL] = [] + let fileManager = FileManager.default + + for path in inputPaths { + if isDirectory(url: path) { + if let enumerator = fileManager.enumerator(at: path, includingPropertiesForKeys: nil) { + for case let fileURL as URL in enumerator { + allFiles.append(fileURL) + } + } + } else if path.isFileURL { + allFiles.append(path) + } + } + + for file in allFiles { + print("Importing module '\(swiftModule)', interface file: \(file)") - try translator.analyze(swiftInterfacePath: interfaceFile) - try translator.writeImportedTypesTo(outputDirectory: outputDirectory) + try translator.analyze(file: file.path) + try translator.writeExportedJavaSources(outputDirectory: outputDirectoryJava) + try translator.writeSwiftThunkSources(outputDirectory: outputDirectorySwift) - print("[\(fileNo)/\(interfaceFiles.count)] Imported interface file: \(interfaceFile) " + "done.".green) + print("Imported interface file: \(file) " + "done.".green) } - try translator.writeModuleTo(outputDirectory: outputDirectory) + try translator.writeExportedJavaModule(outputDirectory: outputDirectoryJava) print("") - print("Generated Java sources in package '\(packageName)' in: \(outputDirectory)/") + print("Generated Java sources in package '\(packageName)' in: \(outputDirectoryJava)/") print("Swift module '\(swiftModule)' import: " + "done.".green) } @@ -79,3 +100,9 @@ extension Logger.Level: ExpressibleByArgument { public private(set) static var defaultCompletionKind: CompletionKind = .default } + +func isDirectory(url: URL) -> Bool { + var isDirectory: ObjCBool = false + FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) + return isDirectory.boolValue +} diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+MemoryLayouts.swift b/Sources/JExtractSwift/Swift2JavaTranslator+MemoryLayouts.swift index 4e8f6aa7..6d7558f2 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+MemoryLayouts.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+MemoryLayouts.swift @@ -20,16 +20,16 @@ import SwiftSyntax extension Swift2JavaTranslator { public func javaMemoryLayoutDescriptors( forParametersOf decl: ImportedFunc, - selfVariant: SelfParameterVariant? + paramPassingStyle: SelfParameterVariant? ) -> [ForeignValueLayout] { var layouts: [ForeignValueLayout] = [] layouts.reserveCapacity(decl.parameters.count + 1) // // When the method is `init()` it does not accept a self (well, unless allocating init but we don't import those) - // let selfVariant: SelfParameterVariant? = + // let paramPassingStyle: SelfParameterVariant? = // decl.isInit ? nil : .wrapper - for param in decl.effectiveParameters(selfVariant: selfVariant) { + for param in decl.effectiveParameters(paramPassingStyle: paramPassingStyle) { layouts.append(param.type.foreignValueLayout) } diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift index c8b9bf09..002337f9 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift @@ -25,16 +25,18 @@ let PATH_SEPARATOR = "/" // TODO: Windows extension Swift2JavaTranslator { /// Every imported public type becomes a public class in its own file in Java. - public func writeImportedTypesTo(outputDirectory: String) throws { + public func writeExportedJavaSources(outputDirectory: String) throws { var printer = CodePrinter() + try writeExportedJavaSources(outputDirectory: outputDirectory, printer: &printer) + } + public func writeExportedJavaSources(outputDirectory: String, printer: inout CodePrinter) throws { for (_, ty) in importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { let filename = "\(ty.javaClassName).java" log.info("Printing contents: \(filename)") printImportedClass(&printer, ty) - try writeContents( - printer.finalize(), + try printer.writeContents( outputDirectory: outputDirectory, javaPackagePath: javaPackagePath, filename: filename @@ -42,48 +44,80 @@ extension Swift2JavaTranslator { } } + public func writeSwiftThunkSources(outputDirectory: String) throws { + var printer = CodePrinter() + try writeSwiftThunkSources(outputDirectory: outputDirectory, printer: &printer) + } + + public func writeSwiftThunkSources(outputDirectory: String, printer: inout CodePrinter) throws { + // ==== Globals + for decl in self.importedGlobalFuncs { + printSwiftThunkSources(&printer, decl: decl) + } + + let moduleFilename = "\(self.swiftModuleName)Module+SwiftJava.swift" + log.info("Printing contents: \(moduleFilename)") + do { + try printer.writeContents( + outputDirectory: outputDirectory, + javaPackagePath: nil, + filename: moduleFilename) + } catch { + log.warning("Failed to write to Swift thunks: \(moduleFilename)") + } + + // === All types + for (_, ty) in importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { + let filename = "\(ty.swiftTypeName)+SwiftJava.swift" + log.info("Printing contents: \(filename)") + + do { + try printSwiftThunkSources(&printer, ty: ty) + + try printer.writeContents( + outputDirectory: outputDirectory, + javaPackagePath: nil, + filename: filename) + } catch { + log.warning("Failed to write to Swift thunks: \(filename)") + } + } + } + + public func printSwiftThunkSources(_ printer: inout CodePrinter, decl: ImportedFunc) { + let stt = SwiftThunkTranslator(self) + + for thunk in stt.render(forFunc: decl) { + printer.print(thunk) + printer.println() + } + } + + public func printSwiftThunkSources(_ printer: inout CodePrinter, ty: ImportedNominalType) throws { + let stt = SwiftThunkTranslator(self) + + for thunk in stt.renderThunks(forType: ty) { + printer.print("\(thunk)") + printer.print("") + } + } + /// A module contains all static and global functions from the Swift module, /// potentially from across multiple swift interfaces. - public func writeModuleTo(outputDirectory: String) throws { + public func writeExportedJavaModule(outputDirectory: String) throws { var printer = CodePrinter() + try writeExportedJavaModule(outputDirectory: outputDirectory, printer: &printer) + } + + public func writeExportedJavaModule(outputDirectory: String, printer: inout CodePrinter) throws { printModule(&printer) - try writeContents( - printer.finalize(), + try printer.writeContents( outputDirectory: outputDirectory, javaPackagePath: javaPackagePath, filename: "\(swiftModuleName).java" ) } - - private func writeContents( - _ contents: String, - outputDirectory: String, - javaPackagePath: String, - filename: String - ) throws { - if outputDirectory == "-" { - print( - "// ==== ---------------------------------------------------------------------------------------------------" - ) - print("// \(javaPackagePath)/\(filename)") - print(contents) - return - } - - let targetDirectory = [outputDirectory, javaPackagePath].joined(separator: PATH_SEPARATOR) - log.trace("Prepare target directory: \(targetDirectory)") - try FileManager.default.createDirectory(atPath: targetDirectory, withIntermediateDirectories: true) - - let targetFilePath = [javaPackagePath, filename].joined(separator: PATH_SEPARATOR) - print("Writing '\(targetFilePath)'...", terminator: "") - try contents.write( - to: Foundation.URL(fileURLWithPath: targetDirectory).appendingPathComponent(filename), - atomically: true, - encoding: .utf8 - ) - print(" done.".green) - } } // ==== --------------------------------------------------------------------------------------------------------------- @@ -119,11 +153,18 @@ extension Swift2JavaTranslator { // Ensure we have loaded the library where the Swift type was declared before we attempt to resolve types in Swift printStaticLibraryLoad(&printer) - // Prepare type metadata, we're going to need these when invoking e.g. initializers so cache them in a static - printer.print( + // Prepare type metadata, we're going to need these when invoking e.g. initializers so cache them in a static. + // We call into source swift-java source generated accessors which give us the type of the Swift object: + // TODO: seems we no longer need the mangled name per se, so avoiding such constant and downcall + // printer.printParts( + // "public static final String TYPE_MANGLED_NAME = ", + // SwiftKitPrinting.renderCallGetSwiftTypeMangledName(module: self.swiftModuleName, nominal: decl), + // ";" + // ) + printer.printParts( """ - public static final String TYPE_MANGLED_NAME = "\(decl.swiftMangledName ?? "")"; - public static final SwiftAnyType TYPE_METADATA = SwiftKit.getTypeByMangledNameInEnvironment(TYPE_MANGLED_NAME).get(); + public static final SwiftAnyType TYPE_METADATA = + new SwiftAnyType(\(SwiftKitPrinting.renderCallGetSwiftType(module: self.swiftModuleName, nominal: decl))); public final SwiftAnyType $swiftType() { return TYPE_METADATA; } @@ -178,8 +219,11 @@ extension Swift2JavaTranslator { printer.print("") } - public func printClass(_ printer: inout CodePrinter, _ decl: ImportedNominalType, body: (inout CodePrinter) -> Void) { - printer.printTypeDecl("public final class \(decl.javaClassName) implements SwiftHeapObject") { printer in + public func printClass( + _ printer: inout CodePrinter, _ decl: ImportedNominalType, body: (inout CodePrinter) -> Void + ) { + printer.printTypeDecl("public final class \(decl.javaClassName) implements SwiftHeapObject") { + printer in // ==== Storage of the class printClassSelfProperty(&printer, decl) @@ -308,7 +352,7 @@ extension Swift2JavaTranslator { """ private static final GroupLayout $LAYOUT = MemoryLayout.structLayout( SWIFT_POINTER - ).withName("\(decl.swiftMangledName ?? decl.swiftTypeName)"); + ).withName("\(decl.swiftTypeName)"); public final GroupLayout $layout() { return $LAYOUT; @@ -391,7 +435,7 @@ extension Swift2JavaTranslator { } public func printClassConstructors(_ printer: inout CodePrinter, _ decl: ImportedFunc) { - guard let parentName = decl.parentName else { + guard let parentName = decl.parent else { fatalError("init must be inside a parent type! Was: \(decl)") } printer.printSeparator(decl.identifier) @@ -399,7 +443,7 @@ extension Swift2JavaTranslator { let descClassIdentifier = renderDescClassName(decl) printer.printTypeDecl("private static class \(descClassIdentifier)") { printer in printFunctionDescriptorValue(&printer, decl) - printFindMemorySegmentAddrByMangledName(&printer, decl) + printAccessorFunctionAddr(&printer, decl) printMethodDowncallHandleForAddrDesc(&printer) } @@ -420,8 +464,8 @@ extension Swift2JavaTranslator { * \(decl.renderCommentSnippet ?? " *") */ - public \(parentName.unqualifiedJavaTypeName)(\(renderJavaParamDecls(decl, selfVariant: .wrapper))) { - this(/*arena=*/null, \(renderForwardParams(decl, selfVariant: .wrapper))); + public \(parentName.unqualifiedJavaTypeName)(\(renderJavaParamDecls(decl, paramPassingStyle: .wrapper))) { + this(/*arena=*/null, \(renderForwardJavaParams(decl, paramPassingStyle: .wrapper))); } """ ) @@ -434,14 +478,14 @@ extension Swift2JavaTranslator { * \(decl.renderCommentSnippet ?? " *") */ - public \(parentName.unqualifiedJavaTypeName)(SwiftArena arena, \(renderJavaParamDecls(decl, selfVariant: .wrapper))) { + public \(parentName.unqualifiedJavaTypeName)(SwiftArena arena, \(renderJavaParamDecls(decl, paramPassingStyle: .wrapper))) { var mh$ = \(descClassIdentifier).HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall(\(renderForwardParams(decl, selfVariant: nil))); + traceDowncall(\(renderForwardJavaParams(decl, paramPassingStyle: nil))); } - this.selfMemorySegment = (MemorySegment) mh$.invokeExact(\(renderForwardParams(decl, selfVariant: nil)), TYPE_METADATA.$memorySegment()); + this.selfMemorySegment = (MemorySegment) mh$.invokeExact(\(renderForwardJavaParams(decl, paramPassingStyle: nil)), TYPE_METADATA.$memorySegment()); if (arena != null) { arena.register(this); } @@ -468,8 +512,8 @@ extension Swift2JavaTranslator { printer.printSeparator(decl.identifier) printer.printTypeDecl("private static class \(decl.baseIdentifier)") { printer in - printFunctionDescriptorValue(&printer, decl); - printFindMemorySegmentAddrByMangledName(&printer, decl) + printFunctionDescriptorValue(&printer, decl) + printAccessorFunctionAddr(&printer, decl) printMethodDowncallHandleForAddrDesc(&printer) } @@ -479,16 +523,18 @@ extension Swift2JavaTranslator { // Render the basic "make the downcall" function if decl.hasParent { - printFuncDowncallMethod(&printer, decl: decl, selfVariant: .memorySegment) - printFuncDowncallMethod(&printer, decl: decl, selfVariant: .wrapper) + printFuncDowncallMethod(&printer, decl: decl, paramPassingStyle: .memorySegment) + printFuncDowncallMethod(&printer, decl: decl, paramPassingStyle: .wrapper) } else { - printFuncDowncallMethod(&printer, decl: decl, selfVariant: nil) + printFuncDowncallMethod(&printer, decl: decl, paramPassingStyle: nil) } } - private func printFunctionAddressMethod(_ printer: inout CodePrinter, - decl: ImportedFunc, - accessorKind: VariableAccessorKind? = nil) { + private func printFunctionAddressMethod( + _ printer: inout CodePrinter, + decl: ImportedFunc, + accessorKind: VariableAccessorKind? = nil + ) { let addrName = accessorKind.renderAddrFieldName let methodNameSegment = accessorKind.renderMethodNameSegment @@ -507,9 +553,11 @@ extension Swift2JavaTranslator { ) } - private func printFunctionMethodHandleMethod(_ printer: inout CodePrinter, - decl: ImportedFunc, - accessorKind: VariableAccessorKind? = nil) { + private func printFunctionMethodHandleMethod( + _ printer: inout CodePrinter, + decl: ImportedFunc, + accessorKind: VariableAccessorKind? = nil + ) { let handleName = accessorKind.renderHandleFieldName let methodNameSegment = accessorKind.renderMethodNameSegment let snippet = decl.renderCommentSnippet ?? "* " @@ -527,9 +575,11 @@ extension Swift2JavaTranslator { ) } - private func printFunctionDescriptorMethod(_ printer: inout CodePrinter, - decl: ImportedFunc, - accessorKind: VariableAccessorKind? = nil) { + private func printFunctionDescriptorMethod( + _ printer: inout CodePrinter, + decl: ImportedFunc, + accessorKind: VariableAccessorKind? = nil + ) { let descName = accessorKind.renderDescFieldName let methodNameSegment = accessorKind.renderMethodNameSegment let snippet = decl.renderCommentSnippet ?? "* " @@ -557,8 +607,8 @@ extension Swift2JavaTranslator { continue } - printFunctionDescriptorValue(&printer, accessor, accessorKind: accessorKind); - printFindMemorySegmentAddrByMangledName(&printer, accessor, accessorKind: accessorKind) + printFunctionDescriptorValue(&printer, accessor, accessorKind: accessorKind) + printAccessorFunctionAddr(&printer, accessor, accessorKind: accessorKind) printMethodDowncallHandleForAddrDesc(&printer, accessorKind: accessorKind) } } @@ -583,24 +633,36 @@ extension Swift2JavaTranslator { // Render the basic "make the downcall" function if decl.hasParent { - printFuncDowncallMethod(&printer, decl: accessor, selfVariant: .memorySegment, accessorKind: accessorKind) - printFuncDowncallMethod(&printer, decl: accessor, selfVariant: .wrapper, accessorKind: accessorKind) + printFuncDowncallMethod( + &printer, decl: accessor, paramPassingStyle: .memorySegment, accessorKind: accessorKind) + printFuncDowncallMethod( + &printer, decl: accessor, paramPassingStyle: .wrapper, accessorKind: accessorKind) } else { - printFuncDowncallMethod(&printer, decl: accessor, selfVariant: nil, accessorKind: accessorKind) + printFuncDowncallMethod( + &printer, decl: accessor, paramPassingStyle: nil, accessorKind: accessorKind) } } } - func printFindMemorySegmentAddrByMangledName(_ printer: inout CodePrinter, _ decl: ImportedFunc, - accessorKind: VariableAccessorKind? = nil) { + func printAccessorFunctionAddr( + _ printer: inout CodePrinter, _ decl: ImportedFunc, + accessorKind: VariableAccessorKind? = nil + ) { + var thunkName = SwiftKitPrinting.Names.functionThunk( + thunkNameRegistry: &self.thunkNameRegistry, + module: self.swiftModuleName, function: decl) + thunkName = thunkNameRegistry.deduplicate(name: thunkName) printer.print( """ - public static final MemorySegment \(accessorKind.renderAddrFieldName) = \(swiftModuleName).findOrThrow("\(decl.swiftMangledName)"); + public static final MemorySegment \(accessorKind.renderAddrFieldName) = + \(self.swiftModuleName).findOrThrow("\(thunkName)"); """ - ); + ) } - func printMethodDowncallHandleForAddrDesc(_ printer: inout CodePrinter, accessorKind: VariableAccessorKind? = nil) { + func printMethodDowncallHandleForAddrDesc( + _ printer: inout CodePrinter, accessorKind: VariableAccessorKind? = nil + ) { printer.print( """ public static final MethodHandle \(accessorKind.renderHandleFieldName) = Linker.nativeLinker().downcallHandle(\(accessorKind.renderAddrFieldName), \(accessorKind.renderDescFieldName)); @@ -611,7 +673,7 @@ extension Swift2JavaTranslator { public func printFuncDowncallMethod( _ printer: inout CodePrinter, decl: ImportedFunc, - selfVariant: SelfParameterVariant?, + paramPassingStyle: SelfParameterVariant?, accessorKind: VariableAccessorKind? = nil ) { let returnTy = decl.returnType.javaType @@ -635,13 +697,13 @@ extension Swift2JavaTranslator { // An identifier may be "getX", "setX" or just the plain method name let identifier = accessorKind.renderMethodName(decl) - if selfVariant == SelfParameterVariant.wrapper { + if paramPassingStyle == SelfParameterVariant.wrapper { // delegate to the MemorySegment "self" accepting overload printer.print( """ \(javaDocComment) - public \(returnTy) \(identifier)(\(renderJavaParamDecls(decl, selfVariant: .wrapper))) { - \(maybeReturnCast) \(identifier)(\(renderForwardParams(decl, selfVariant: .wrapper))); + public \(returnTy) \(identifier)(\(renderJavaParamDecls(decl, paramPassingStyle: .wrapper))) { + \(maybeReturnCast) \(identifier)(\(renderForwardJavaParams(decl, paramPassingStyle: .wrapper))); } """ ) @@ -654,7 +716,7 @@ extension Swift2JavaTranslator { printer.printParts( """ \(javaDocComment) - public static \(returnTy) \(identifier)(\(renderJavaParamDecls(decl, selfVariant: selfVariant))) { + public static \(returnTy) \(identifier)(\(renderJavaParamDecls(decl, paramPassingStyle: paramPassingStyle))) { var mh$ = \(decl.baseIdentifier).\(handleName); \(renderTry(withArena: needsArena)) """, @@ -663,9 +725,9 @@ extension Swift2JavaTranslator { """, """ if (TRACE_DOWNCALLS) { - traceDowncall(\(renderForwardParams(decl, selfVariant: .memorySegment))); + traceDowncall(\(renderForwardJavaParams(decl, paramPassingStyle: .memorySegment))); } - \(maybeReturnCast) mh$.invokeExact(\(renderForwardParams(decl, selfVariant: selfVariant))); + \(maybeReturnCast) mh$.invokeExact(\(renderForwardJavaParams(decl, paramPassingStyle: paramPassingStyle))); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -677,7 +739,7 @@ extension Swift2JavaTranslator { public func printPropertyAccessorDowncallMethod( _ printer: inout CodePrinter, decl: ImportedFunc, - selfVariant: SelfParameterVariant? + paramPassingStyle: SelfParameterVariant? ) { let returnTy = decl.returnType.javaType @@ -688,7 +750,7 @@ extension Swift2JavaTranslator { maybeReturnCast = "return (\(returnTy))" } - if selfVariant == SelfParameterVariant.wrapper { + if paramPassingStyle == SelfParameterVariant.wrapper { // delegate to the MemorySegment "self" accepting overload printer.print( """ @@ -697,8 +759,8 @@ extension Swift2JavaTranslator { * \(/*TODO: make a printSnippet func*/decl.syntax ?? "") * } */ - public \(returnTy) \(decl.baseIdentifier)(\(renderJavaParamDecls(decl, selfVariant: .wrapper))) { - \(maybeReturnCast) \(decl.baseIdentifier)(\(renderForwardParams(decl, selfVariant: .wrapper))); + public \(returnTy) \(decl.baseIdentifier)(\(renderJavaParamDecls(decl, paramPassingStyle: .wrapper))) { + \(maybeReturnCast) \(decl.baseIdentifier)(\(renderForwardJavaParams(decl, paramPassingStyle: .wrapper))); } """ ) @@ -712,13 +774,13 @@ extension Swift2JavaTranslator { * \(/*TODO: make a printSnippet func*/decl.syntax ?? "") * } */ - public static \(returnTy) \(decl.baseIdentifier)(\(renderJavaParamDecls(decl, selfVariant: selfVariant))) { + public static \(returnTy) \(decl.baseIdentifier)(\(renderJavaParamDecls(decl, paramPassingStyle: paramPassingStyle))) { var mh$ = \(decl.baseIdentifier).HANDLE; try { if (TRACE_DOWNCALLS) { - traceDowncall(\(renderForwardParams(decl, selfVariant: .memorySegment))); + traceDowncall(\(renderForwardJavaParams(decl, paramPassingStyle: .memorySegment))); } - \(maybeReturnCast) mh$.invokeExact(\(renderForwardParams(decl, selfVariant: selfVariant))); + \(maybeReturnCast) mh$.invokeExact(\(renderForwardJavaParams(decl, paramPassingStyle: paramPassingStyle))); } catch (Throwable ex$) { throw new AssertionError("should not reach here", ex$); } @@ -737,7 +799,7 @@ extension Swift2JavaTranslator { return "p\(pCounter)" } - for p in decl.effectiveParameters(selfVariant: nil) { + for p in decl.effectiveParameters(paramPassingStyle: nil) { let param = "\(p.effectiveName ?? nextUniqueParamName())" ps.append(param) } @@ -758,7 +820,7 @@ extension Swift2JavaTranslator { return false } - + public func renderTry(withArena: Bool) -> String { if withArena { "try (Arena arena = Arena.ofConfined()) {" @@ -767,7 +829,7 @@ extension Swift2JavaTranslator { } } - public func renderJavaParamDecls(_ decl: ImportedFunc, selfVariant: SelfParameterVariant?) -> String { + public func renderJavaParamDecls(_ decl: ImportedFunc, paramPassingStyle: SelfParameterVariant?) -> String { var ps: [String] = [] var pCounter = 0 @@ -776,7 +838,7 @@ extension Swift2JavaTranslator { return "p\(pCounter)" } - for p in decl.effectiveParameters(selfVariant: selfVariant) { + for p in decl.effectiveParameters(paramPassingStyle: paramPassingStyle) { let param = "\(p.type.javaType.description) \(p.effectiveName ?? nextUniqueParamName())" ps.append(param) } @@ -785,12 +847,45 @@ extension Swift2JavaTranslator { return res } + // TODO: these are stateless, find new place for them? + public func renderSwiftParamDecls(_ decl: ImportedFunc, paramPassingStyle: SelfParameterVariant?) -> String { + var ps: [String] = [] + var pCounter = 0 + + func nextUniqueParamName() -> String { + pCounter += 1 + return "p\(pCounter)" + } + + for p in decl.effectiveParameters(paramPassingStyle: paramPassingStyle) { + let firstName = p.firstName ?? "_" + let secondName = p.secondName ?? p.firstName ?? nextUniqueParamName() + + let param = + if firstName == secondName { + // We have to do this to avoid a 'extraneous duplicate parameter name; 'number' already has an argument label' warning + "\(firstName): \(p.type.swiftTypeName.description)" + } else { + "\(firstName) \(secondName): \(p.type.swiftTypeName.description)" + } + ps.append(param) + } + + if paramPassingStyle == .swiftThunkSelf { + ps.append("_self: Any") + } + + let res = ps.joined(separator: ", ") + return res + } + public func renderUpcallHandles(_ decl: ImportedFunc) -> String { var printer = CodePrinter() for p in decl.parameters where p.type.javaType.isSwiftClosure { if p.type.javaType == .javaLangRunnable { let paramName = p.secondName ?? p.firstName ?? "_" - let handleDesc = p.type.javaType.prepareClosureDowncallHandle(decl: decl, parameter: paramName) + let handleDesc = p.type.javaType.prepareClosureDowncallHandle( + decl: decl, parameter: paramName) printer.print(handleDesc) } } @@ -798,7 +893,7 @@ extension Swift2JavaTranslator { return printer.contents } - public func renderForwardParams(_ decl: ImportedFunc, selfVariant: SelfParameterVariant?) -> String { + public func renderForwardJavaParams(_ decl: ImportedFunc, paramPassingStyle: SelfParameterVariant?) -> String { var ps: [String] = [] var pCounter = 0 @@ -807,12 +902,12 @@ extension Swift2JavaTranslator { return "p\(pCounter)" } - for p in decl.effectiveParameters(selfVariant: selfVariant) { + for p in decl.effectiveParameters(paramPassingStyle: paramPassingStyle) { // FIXME: fix the handling here we're already a memory segment let param: String if p.effectiveName == "self$" { - precondition(selfVariant == .memorySegment) - param = "self$"; + precondition(paramPassingStyle == .memorySegment) + param = "self$" } else { param = "\(p.renderParameterForwarding() ?? nextUniqueParamName())" } @@ -820,30 +915,52 @@ extension Swift2JavaTranslator { } // Add the forwarding "self" - if selfVariant == .wrapper && !decl.isInit { + if paramPassingStyle == .wrapper && !decl.isInit { ps.append("$memorySegment()") } return ps.joined(separator: ", ") } + // TODO: these are stateless, find new place for them? + public func renderForwardSwiftParams(_ decl: ImportedFunc, paramPassingStyle: SelfParameterVariant?) -> String { + var ps: [String] = [] + var pCounter = 0 + + func nextUniqueParamName() -> String { + pCounter += 1 + return "p\(pCounter)" + } + + for p in decl.effectiveParameters(paramPassingStyle: paramPassingStyle) { + if let firstName = p.firstName { + ps.append("\(firstName): \(p.effectiveName ?? nextUniqueParamName())") + } else { + ps.append("\(p.effectiveName ?? nextUniqueParamName())") + } + } + + return ps.joined(separator: ", ") + } + public func printFunctionDescriptorValue( _ printer: inout CodePrinter, _ decl: ImportedFunc, - accessorKind: VariableAccessorKind? = nil) { + accessorKind: VariableAccessorKind? = nil + ) { let fieldName = accessorKind.renderDescFieldName printer.start("public static final FunctionDescriptor \(fieldName) = ") let parameterLayoutDescriptors = javaMemoryLayoutDescriptors( forParametersOf: decl, - selfVariant: .pointer + paramPassingStyle: .pointer ) if decl.returnType.javaType == .void { - printer.print("FunctionDescriptor.ofVoid("); + printer.print("FunctionDescriptor.ofVoid(") printer.indent() } else { - printer.print("FunctionDescriptor.of("); + printer.print("FunctionDescriptor.of(") printer.indent() printer.print("", .continue) @@ -851,7 +968,9 @@ extension Swift2JavaTranslator { let returnTyIsLastTy = decl.parameters.isEmpty && !decl.hasParent if decl.isInit { // when initializing, we return a pointer to the newly created object - printer.print("/* -> */\(ForeignValueLayout.SwiftPointer)", .parameterNewlineSeparator(returnTyIsLastTy)) + printer.print( + "/* -> */\(ForeignValueLayout.SwiftPointer)", .parameterNewlineSeparator(returnTyIsLastTy) + ) } else { var returnDesc = decl.returnType.foreignValueLayout returnDesc.inlineComment = " -> " @@ -864,11 +983,13 @@ extension Swift2JavaTranslator { printer.print(desc, .parameterNewlineSeparator(isLast)) } - printer.outdent(); - printer.print(");"); + printer.outdent() + printer.print(");") } - public func printHeapObjectToStringMethod(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { + public func printHeapObjectToStringMethod( + _ printer: inout CodePrinter, _ decl: ImportedNominalType + ) { printer.print( """ @Override diff --git a/Sources/JExtractSwift/Swift2JavaTranslator.swift b/Sources/JExtractSwift/Swift2JavaTranslator.swift index a954620b..51cb0335 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator.swift @@ -36,15 +36,16 @@ public final class Swift2JavaTranslator { // ==== Output state - // TODO: consider how/if we need to store those etc - public var importedGlobalFuncs: [ImportedFunc] = [] + package var importedGlobalFuncs: [ImportedFunc] = [] /// A mapping from Swift type names (e.g., A.B) over to the imported nominal /// type representation. - public var importedTypes: [String: ImportedNominalType] = [:] + package var importedTypes: [String: ImportedNominalType] = [:] let nominalResolution: NominalTypeResolution = NominalTypeResolution() + var thunkNameRegistry: ThunkNameRegistry = ThunkNameRegistry() + public init( javaPackage: String, swiftModuleName: String @@ -68,31 +69,22 @@ extension Swift2JavaTranslator { var javaPrimitiveForSwiftInt: JavaType { .long } public func analyze( - swiftInterfacePath: String, + file: String, text: String? = nil ) throws { - if text == nil { - precondition( - swiftInterfacePath.hasSuffix(Self.SWIFT_INTERFACE_SUFFIX), - "Swift interface path must end with \(Self.SWIFT_INTERFACE_SUFFIX), was: \(swiftInterfacePath)" - ) - - if !FileManager.default.fileExists(atPath: swiftInterfacePath) { - throw Swift2JavaTranslatorError(message: "Missing input file: \(swiftInterfacePath)") - } + guard text != nil || FileManager.default.fileExists(atPath: file) else { + throw Swift2JavaTranslatorError(message: "Missing input file: \(file)") } - log.trace("Analyze: \(swiftInterfacePath)") - let text = try text ?? String(contentsOfFile: swiftInterfacePath) + log.trace("Analyze: \(file)") + let text = try text ?? String(contentsOfFile: file) - try analyzeSwiftInterface(interfaceFilePath: swiftInterfacePath, text: text) + try analyzeSwiftInterface(interfaceFilePath: file, text: text) - log.info("Done processing: \(swiftInterfacePath)") + log.info("Done processing: \(file)") } package func analyzeSwiftInterface(interfaceFilePath: String, text: String) throws { - assert(interfaceFilePath.hasSuffix(Self.SWIFT_INTERFACE_SUFFIX)) - let sourceFileSyntax = Parser.parse(source: text) // Find all of the types and extensions, then bind the extensions. @@ -168,7 +160,6 @@ extension Swift2JavaTranslator { package: javaPackage, name: fullName ), - swiftMangledName: nominal.mangledNameFromComment, kind: kind ) diff --git a/Sources/JExtractSwift/Swift2JavaVisitor.swift b/Sources/JExtractSwift/Swift2JavaVisitor.swift index 8cf39eab..b3445c05 100644 --- a/Sources/JExtractSwift/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwift/Swift2JavaVisitor.swift @@ -59,7 +59,8 @@ final class Swift2JavaVisitor: SyntaxVisitor { // Resolve the extended type of the extension as an imported nominal, and // recurse if we found it. guard let nominal = translator.nominalResolution.extendedType(of: node), - let importedNominalType = translator.importedNominalType(nominal) else { + let importedNominalType = translator.importedNominalType(nominal) + else { return .skipChildren } @@ -94,7 +95,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { // TODO: more robust parameter handling // TODO: More robust type handling return ImportedParam( - param: param, + syntax: param, type: try cCompatibleType(for: param.type) ) } @@ -107,18 +108,14 @@ final class Swift2JavaVisitor: SyntaxVisitor { let fullName = "\(node.name.text)" - var funcDecl = ImportedFunc( - parentName: currentTypeName.map { translator.importedTypes[$0] }??.translatedType, + let funcDecl = ImportedFunc( + module: self.translator.swiftModuleName, + decl: node.trimmed, + parent: currentTypeName.map { translator.importedTypes[$0] }??.translatedType, identifier: fullName, returnType: javaResultType, parameters: params ) - funcDecl.syntax = "\(node.trimmed)" // TODO: rethink this, it's useful for comments in Java - - // Retrieve the mangled name, if available. - if let mangledName = node.mangledNameFromComment { - funcDecl.swiftMangledName = mangledName - } if let currentTypeName { log.debug("Record method in \(currentTypeName)") @@ -142,7 +139,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { self.log.info("Import variable: \(node.kind) \(fullName)") let returnTy: TypeSyntax - if let typeAnnotation = binding.typeAnnotation{ + if let typeAnnotation = binding.typeAnnotation { returnTy = typeAnnotation.type } else { returnTy = "Swift.Void" @@ -157,6 +154,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { } var varDecl = ImportedVariable( + module: self.translator.swiftModuleName, parentName: currentTypeName.map { translator.importedTypes[$0] }??.translatedType, identifier: fullName, returnType: javaResultType @@ -180,7 +178,8 @@ final class Swift2JavaVisitor: SyntaxVisitor { override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { guard let currentTypeName, - let currentType = translator.importedTypes[currentTypeName] else { + let currentType = translator.importedTypes[currentTypeName] + else { fatalError("Initializer must be within a current type, was: \(node)") } guard node.shouldImport(log: log) else { @@ -194,7 +193,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { // TODO: more robust parameter handling // TODO: More robust type handling return ImportedParam( - param: param, + syntax: param, type: try cCompatibleType(for: param.type) ) } @@ -207,20 +206,17 @@ final class Swift2JavaVisitor: SyntaxVisitor { "init(\(String(params.flatMap { "\($0.effectiveName ?? "_"):" })))" var funcDecl = ImportedFunc( - parentName: currentType.translatedType, + module: self.translator.swiftModuleName, + decl: node.trimmed, + parent: currentType.translatedType, identifier: initIdentifier, returnType: currentType.translatedType, parameters: params ) funcDecl.isInit = true - funcDecl.syntax = "\(node.trimmed)" // TODO: rethink this, it's useful for comments in Java - - // Retrieve the mangled name, if available. - if let mangledName = node.mangledNameFromComment { - funcDecl.swiftMangledName = mangledName - } - log.info("Record initializer method in \(currentType.javaType.description): \(funcDecl.identifier)") + log.info( + "Record initializer method in \(currentType.javaType.description): \(funcDecl.identifier)") translator.importedTypes[currentTypeName]!.initializers.append(funcDecl) return .skipChildren diff --git a/Sources/JExtractSwift/SwiftKit+Printing.swift b/Sources/JExtractSwift/SwiftKit+Printing.swift new file mode 100644 index 00000000..1f9a4ecd --- /dev/null +++ b/Sources/JExtractSwift/SwiftKit+Printing.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftBasicFormat +import SwiftParser +import SwiftSyntax + +/// Helper for printing calls into SwiftKit generated code from generated sources. +package struct SwiftKitPrinting { + + /// Forms syntax for a Java call to a swiftkit exposed function. + static func renderCallGetSwiftType(module: String, nominal: ImportedNominalType) -> String { + """ + SwiftKit.swiftjava.getType("\(module)", "\(nominal.swiftTypeName)") + """ + } +} + +// ==== ------------------------------------------------------------------------ +// Helpers to form names of "well known" SwiftKit generated functions + +extension SwiftKitPrinting { + enum Names { + } +} + +extension SwiftKitPrinting.Names { + static func getType(module: String, nominal: ImportedNominalType) -> String { + "swiftjava_getType_\(module)_\(nominal.swiftTypeName)" + } + + static func functionThunk( + thunkNameRegistry: inout ThunkNameRegistry, + module: String, function: ImportedFunc) -> String { + let params = function.effectiveParameters(paramPassingStyle: .swiftThunkSelf) + var paramsPart = "" + if !params.isEmpty { + paramsPart = "_" + params.map { param in + param.firstName ?? "_" + }.joined(separator: "_") + } + + let name = + if let parent = function.parent { + "swiftjava_\(module)_\(parent.swiftTypeName)_\(function.baseIdentifier)\(paramsPart)" + } else { + "swiftjava_\(module)_\(function.baseIdentifier)\(paramsPart)" + } + + return thunkNameRegistry.deduplicate(name: name) + } +} diff --git a/Sources/JExtractSwift/SwiftThunkTranslator.swift b/Sources/JExtractSwift/SwiftThunkTranslator.swift new file mode 100644 index 00000000..a3010490 --- /dev/null +++ b/Sources/JExtractSwift/SwiftThunkTranslator.swift @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import Foundation +import SwiftBasicFormat +import SwiftParser +import SwiftSyntax + +struct SwiftThunkTranslator { + + let st: Swift2JavaTranslator + + init(_ st: Swift2JavaTranslator) { + self.st = st + } + + /// Render all the thunks that make Swift methods accessible to Java. + func renderThunks(forType nominal: ImportedNominalType) -> [DeclSyntax] { + var decls: [DeclSyntax] = [] + decls.reserveCapacity(nominal.initializers.count + nominal.methods.count) + + decls.append(renderSwiftTypeAccessor(nominal)) + + for decl in st.importedGlobalFuncs { + decls.append(contentsOf: render(forFunc: decl)) + } + + for decl in nominal.initializers { + decls.append(contentsOf: renderSwiftInitAccessor(decl)) + } + + for decl in nominal.methods { + decls.append(contentsOf: render(forFunc: decl)) + } + +// for v in nominal.variables { +// if let acc = v.accessorFunc(kind: .get) { +// decls.append(contentsOf: render(forFunc: acc)) +// } +// if let acc = v.accessorFunc(kind: .set) { +// decls.append(contentsOf: render(forFunc: acc)) +// } +// } + + return decls + } + + /// Accessor to get the `T.self` of the Swift type, without having to rely on mangled name lookups. + func renderSwiftTypeAccessor(_ nominal: ImportedNominalType) -> DeclSyntax { + let funcName = SwiftKitPrinting.Names.getType( + module: st.swiftModuleName, + nominal: nominal) + + return + """ + @_cdecl("\(raw: funcName)") + public func \(raw: funcName)() -> Any /* Any.Type */ { + return \(raw: nominal.swiftTypeName).self + } + """ + } + + func renderSwiftInitAccessor(_ function: ImportedFunc) -> [DeclSyntax] { + guard let parent = function.parent else { + fatalError("Cannot render initializer accessor if init function has no parent! Was: \(function)") + } + + let funcName = SwiftKitPrinting.Names.functionThunk( + thunkNameRegistry: &self.st.thunkNameRegistry, + module: st.swiftModuleName, + function: function) + + // FIXME: handle in thunk: return types + // FIXME: handle in thunk: parameters + // FIXME: handle in thunk: errors + return + [ + """ + @_cdecl("\(raw: funcName)") + public func \(raw: funcName)(\(raw: st.renderSwiftParamDecls(function, paramPassingStyle: nil))) -> Any /* \(raw: parent.swiftTypeName) */ { + \(raw: parent.swiftTypeName)(\(raw: st.renderForwardSwiftParams(function, paramPassingStyle: nil))) + } + """ + ] + } + + + func render(forFunc decl: ImportedFunc) -> [DeclSyntax] { + st.log.trace("Rendering thunks for: \(decl.baseIdentifier)") + let funcName = SwiftKitPrinting.Names.functionThunk( + thunkNameRegistry: &st.thunkNameRegistry, + module: st.swiftModuleName, + function: decl) + + // Do we need to pass a self parameter? + let paramPassingStyle: SelfParameterVariant? + let callBaseDot: String + if let parent = decl.parent { + paramPassingStyle = .swiftThunkSelf + // TODO: unsafe bitcast + callBaseDot = "(_self as! \(parent.originalSwiftType))." + } else { + paramPassingStyle = nil + callBaseDot = "" + } + + return + [ + """ + @_cdecl("\(raw: funcName)") + public func \(raw: funcName)(\(raw: st.renderSwiftParamDecls(decl, paramPassingStyle: paramPassingStyle))) -> \(decl.returnType.cCompatibleSwiftType) /* \(raw: decl.returnType.swiftTypeName) */ { + \(raw: callBaseDot)\(raw: decl.baseIdentifier)(\(raw: st.renderForwardSwiftParams(decl, paramPassingStyle: paramPassingStyle))) + } + """ + ] + } +} diff --git a/Sources/JExtractSwift/ThunkNameRegistry.swift b/Sources/JExtractSwift/ThunkNameRegistry.swift new file mode 100644 index 00000000..2a41ce4a --- /dev/null +++ b/Sources/JExtractSwift/ThunkNameRegistry.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Registry of names we've already emitted as @_cdecl and must be kept unique. +/// In order to avoid duplicate symbols, the registry can append some unique identifier to duplicated names +package struct ThunkNameRegistry { + /// Maps base names such as "swiftjava_Module_Type_method_a_b_c" to the number of times we've seen them. + /// This is used to de-duplicate symbols as we emit them. + private var baseNames: [String: Int] = [:] + + package init() {} + + package mutating func deduplicate(name: String) -> String { + var emittedCount = self.baseNames[name, default: 0] + self.baseNames[name] = emittedCount + 1 + + if emittedCount == 0 { + return name // first occurrence of a name we keep as-is + } else { + return "\(name)$\(emittedCount)" + } + } +} \ No newline at end of file diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftAnyType.java b/SwiftKit/src/main/java/org/swift/swiftkit/SwiftAnyType.java index 6dcd1f6b..cf7cc238 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftAnyType.java +++ b/SwiftKit/src/main/java/org/swift/swiftkit/SwiftAnyType.java @@ -27,11 +27,11 @@ public final class SwiftAnyType { private final MemorySegment memorySegment; public SwiftAnyType(MemorySegment memorySegment) { - if (memorySegment.byteSize() == 0) { - throw new IllegalArgumentException("A Swift Any.Type cannot be null!"); - } +// if (SwiftKit.getSwiftInt(memorySegment, 0) > 0) { +// throw new IllegalArgumentException("A Swift Any.Type cannot be null!"); +// } - this.memorySegment = memorySegment; + this.memorySegment = memorySegment.asReadOnly(); } public SwiftAnyType(SwiftHeapObject object) { diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java b/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java index 064be4ec..c1344375 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java +++ b/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java @@ -260,17 +260,6 @@ private static class swift_getTypeByMangledNameInEnvironment { public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } - private static class getTypeByStringByteArray { - public static final FunctionDescriptor DESC = FunctionDescriptor.of( - /*returns=*/SwiftValueLayout.SWIFT_POINTER, - ValueLayout.ADDRESS - ); - - public static final MemorySegment ADDR = findOrThrow("getTypeByStringByteArray"); - - public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); - } - /** * Get a Swift {@code Any.Type} wrapped by {@link SwiftAnyType} which represents the type metadata available at runtime. * @@ -308,6 +297,64 @@ public static Optional getTypeByMangledNameInEnvironment(String ma } } + /** + * Produce the name of the Swift type given its Swift type metadata. + *

+ * If 'qualified' is true, leave all the qualification in place to + * disambiguate the type, producing a more complete (but longer) type name. + * + * @param typeMetadata the memory segment must point to a Swift metadata, + * e.g. the result of a {@link swift_getTypeByMangledNameInEnvironment} call + */ + public static String nameOfSwiftType(MemorySegment typeMetadata, boolean qualified) { + MethodHandle mh = swift_getTypeName.HANDLE; + + try (Arena arena = Arena.ofConfined()) { + MemorySegment charsAndLength = (MemorySegment) mh.invokeExact((SegmentAllocator) arena, typeMetadata, qualified); + MemorySegment utf8Chars = charsAndLength.get(SwiftValueLayout.SWIFT_POINTER, 0); + String typeName = utf8Chars.getString(0); + + // FIXME: this free is not always correct: + // java(80175,0x17008f000) malloc: *** error for object 0x600000362610: pointer being freed was not allocated + // cFree(utf8Chars); + + return typeName; + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + + /*** + * Namespace for calls down into swift-java generated thunks and accessors, such as {@code swiftjava_getType_...} etc. + *

Not intended to be used by end-user code directly, but used by swift-java generated Java code. + */ + @SuppressWarnings("unused") // used by source generated Java code + public static final class swiftjava { + private swiftjava() { /* just a namespace */ } + + private static class getType { + public static final FunctionDescriptor DESC = FunctionDescriptor.of( + /* -> */ValueLayout.ADDRESS); + } + + public static MemorySegment getType(String moduleName, String nominalName) { + // We cannot cache this statically since it depends on the type names we're looking up + // TODO: we could cache the handles per type once we have them, to speed up subsequent calls + String symbol = "swiftjava_getType_" + moduleName + "_" + nominalName; + + try { + var addr = findOrThrow(symbol); + var mh$ = Linker.nativeLinker().downcallHandle(addr, getType.DESC); + return (MemorySegment) mh$.invokeExact(); + } catch (Throwable e) { + throw new AssertionError("Failed to call: " + symbol, e); + } + } + } + + // ==== ------------------------------------------------------------------------------------------------------------ + // Get Swift values out of native memory segments + /** * Read a Swift.Int value from memory at the given offset and translate it into a Java long. *

@@ -329,7 +376,7 @@ private static class swift_getTypeName { * Descriptor for the swift_getTypeName runtime function. */ public static final FunctionDescriptor DESC = FunctionDescriptor.of( - /*returns=*/MemoryLayout.structLayout( + /* -> */MemoryLayout.structLayout( SwiftValueLayout.SWIFT_POINTER.withName("utf8Chars"), SwiftValueLayout.SWIFT_INT.withName("length") ), @@ -348,31 +395,4 @@ private static class swift_getTypeName { public static final MethodHandle HANDLE = Linker.nativeLinker().downcallHandle(ADDR, DESC); } - /** - * Produce the name of the Swift type given its Swift type metadata. - *

- * If 'qualified' is true, leave all the qualification in place to - * disambiguate the type, producing a more complete (but longer) type name. - * - * @param typeMetadata the memory segment must point to a Swift metadata, - * e.g. the result of a {@link swift_getTypeByMangledNameInEnvironment} call - */ - public static String nameOfSwiftType(MemorySegment typeMetadata, boolean qualified) { - MethodHandle mh = swift_getTypeName.HANDLE; - - try (Arena arena = Arena.ofConfined()) { - MemorySegment charsAndLength = (MemorySegment) mh.invokeExact((SegmentAllocator) arena, typeMetadata, qualified); - MemorySegment utf8Chars = charsAndLength.get(SwiftValueLayout.SWIFT_POINTER, 0); - String typeName = utf8Chars.getString(0); - - // FIXME: this free is not always correct: - // java(80175,0x17008f000) malloc: *** error for object 0x600000362610: pointer being freed was not allocated - // cFree(utf8Chars); - - return typeName; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - } diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index 84c4b2ec..e0f84237 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -16,6 +16,112 @@ import JExtractSwift import Testing import struct Foundation.CharacterSet +enum RenderKind { + case swift + case java +} + +func assertOutput( + dump: Bool = false, + _ translator: Swift2JavaTranslator, + input: String, + _ renderKind: RenderKind, + detectChunkByInitialLines: Int = 4, + expectedChunks: [String], + fileID: String = #fileID, + filePath: String = #filePath, + line: Int = #line, + column: Int = #column +) throws { + try! translator.analyze(file: "/fake/Fake.swiftinterface", text: input) + + let output: String + var printer: CodePrinter = CodePrinter(io: .accumulateAll) + switch renderKind { + case .swift: + try translator.writeSwiftThunkSources(outputDirectory: "/fake", printer: &printer) + case .java: + try translator.writeExportedJavaSources(outputDirectory: "/fake", printer: &printer) + } + output = printer.finalize() + + let gotLines = output.split(separator: "\n") + for expected in expectedChunks { + let expectedLines = expected.split(separator: "\n") + + var matchingOutputOffset: Int? = nil + let expectedInitialMatchingLines = expectedLines[0.. (offset+detectChunkByInitialLines) { + let textLinesAtOffset = gotLines[offset.. 0 + if hasDiff || dump { + print("") + if hasDiff { + print("error: Number of not matching lines: \(diffLineNumbers.count)!".red) + + print("==== ---------------------------------------------------------------") + print("Expected output:") + for (n, e) in expectedLines.enumerated() { + print("\(n): \(e)".yellow(if: diffLineNumbers.map({$0 - matchingOutputOffset}).contains(n))) + } + } + + print("==== ---------------------------------------------------------------") + print("Got output:") + let printFromLineNo = matchingOutputOffset + let printToLineNo = matchingOutputOffset + expectedLines.count + for (n, g) in gotLines.enumerated() where n >= printFromLineNo && n <= printToLineNo { + print("\(n): \(g)".red(if: diffLineNumbers.contains(n))) + } + print("==== ---------------------------------------------------------------\n") + } + } +} + func assertOutput( dump: Bool = false, _ got: String, @@ -32,21 +138,13 @@ func assertOutput( for (no, (g, e)) in zip(gotLines, expectedLines).enumerated() { if g.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).count == 0 - || e.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).count == 0 - { + || e.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).count == 0 { continue } let ge = g.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) let ee = e.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) if ge.commonPrefix(with: ee) != ee { - // print("") - // print("[\(file):\(line)] " + "Difference found on line: \(no + 1)!".red) - // print("Expected @ \(file):\(Int(line) + no + 3 /*formatting*/ + 1):") - // print(e.yellow) - // print("Got instead:") - // print(g.red) - diffLineNumbers.append(no) let sourceLocation = SourceLocation( @@ -57,7 +155,7 @@ func assertOutput( } let hasDiff = diffLineNumbers.count > 0 - if hasDiff || dump{ + if hasDiff || dump { print("") if hasDiff { print("error: Number of not matching lines: \(diffLineNumbers.count)!".red) diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index 90541263..41440f8e 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -41,12 +41,12 @@ final class FuncCallbackImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/Fake.swiftinterface", text: Self.class_interfaceFile) + try st.analyze(file: "Fake.swift", text: Self.class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.baseIdentifier == "callMe" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: nil) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: nil) } assertOutput( diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index 2f3bccb1..3160d55f 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -53,7 +53,6 @@ final class FunctionDescriptorTests { } """ - @Test func FunctionDescriptor_globalTakeInt() throws { try functionDescriptorTest("globalTakeInt") { output in @@ -141,7 +140,7 @@ extension FunctionDescriptorTests { javaPackage: String = "com.example.swift", swiftModuleName: String = "SwiftModule", logLevel: Logger.Level = .trace, - body: (String) throws -> () + body: (String) throws -> Void ) throws { let st = Swift2JavaTranslator( javaPackage: javaPackage, @@ -149,7 +148,7 @@ extension FunctionDescriptorTests { ) st.log.logLevel = logLevel - try st.analyze(swiftInterfacePath: "/fake/Sample.swiftinterface", text: interfaceFile) + try st.analyze(file: "/fake/Sample.swiftinterface", text: interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.baseIdentifier == methodIdentifier @@ -168,7 +167,7 @@ extension FunctionDescriptorTests { javaPackage: String = "com.example.swift", swiftModuleName: String = "SwiftModule", logLevel: Logger.Level = .trace, - body: (String) throws -> () + body: (String) throws -> Void ) throws { let st = Swift2JavaTranslator( javaPackage: javaPackage, @@ -176,20 +175,21 @@ extension FunctionDescriptorTests { ) st.log.logLevel = logLevel - try st.analyze(swiftInterfacePath: "/fake/Sample.swiftinterface", text: interfaceFile) + try st.analyze(file: "/fake/Sample.swiftinterface", text: interfaceFile) let varDecl: ImportedVariable? = st.importedTypes.values.compactMap { - $0.variables.first { - $0.identifier == identifier - } - }.first + $0.variables.first { + $0.identifier == identifier + } + }.first guard let varDecl else { fatalError("Cannot find descriptor of: \(identifier)") } let getOutput = CodePrinter.toString { printer in - st.printFunctionDescriptorValue(&printer, varDecl.accessorFunc(kind: accessorKind)!, accessorKind: accessorKind) + st.printFunctionDescriptorValue( + &printer, varDecl.accessorFunc(kind: accessorKind)!, accessorKind: accessorKind) } try body(getOutput) diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index 3b2ef4f4..bc5b33f8 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -70,12 +70,12 @@ final class MethodImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/Fake.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.baseIdentifier == "helloWorld" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: nil) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: nil) } assertOutput( @@ -111,14 +111,14 @@ final class MethodImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.baseIdentifier == "globalTakeInt" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: nil) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: nil) } assertOutput( @@ -154,14 +154,14 @@ final class MethodImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.baseIdentifier == "globalTakeIntLongString" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .memorySegment) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .memorySegment) } assertOutput( @@ -197,14 +197,14 @@ final class MethodImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "helloMemberFunction" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .memorySegment) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .memorySegment) } assertOutput( @@ -240,14 +240,14 @@ final class MethodImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "helloMemberInExtension" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .memorySegment) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .memorySegment) } assertOutput( @@ -283,14 +283,14 @@ final class MethodImportTests { ) st.log.logLevel = .info - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "helloMemberFunction" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .memorySegment) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .memorySegment) } assertOutput( @@ -326,14 +326,14 @@ final class MethodImportTests { ) st.log.logLevel = .info - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "helloMemberFunction" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .wrapper) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .wrapper) } assertOutput( @@ -361,14 +361,14 @@ final class MethodImportTests { ) st.log.logLevel = .info - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "makeInt" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .wrapper) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .wrapper) } assertOutput( @@ -396,14 +396,14 @@ final class MethodImportTests { ) st.log.logLevel = .info - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let initDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.initializers.first { $0.identifier == "init(len:cap:)" }! let output = CodePrinter.toString { printer in - st.printClassInitializerConstructors(&printer, initDecl, parentName: initDecl.parentName!) + st.printClassInitializerConstructors(&printer, initDecl, parentName: initDecl.parent!) } assertOutput( diff --git a/Tests/JExtractSwiftTests/MethodThunkTests.swift b/Tests/JExtractSwiftTests/MethodThunkTests.swift new file mode 100644 index 00000000..705a4c7c --- /dev/null +++ b/Tests/JExtractSwiftTests/MethodThunkTests.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import JExtractSwift +import Testing + +final class MethodThunkTests { + let input = + """ + import Swift + + public func globalFunc(a: Int32, b: Int64) {} + public func globalFunc(a: Double, b: Int64) {} + """ + + @Test("Thunk overloads: globalFunc(a: Int32, b: Int64) & globalFunc(i32: Int32, l: Int64)") + func thunk_overloads() throws { + let st = Swift2JavaTranslator( + javaPackage: "com.example.swift", + swiftModuleName: "FakeModule" + ) + st.log.logLevel = .error + + try assertOutput( + st, input: input, .swift, + detectChunkByInitialLines: 1, + expectedChunks: + [ + """ + @_cdecl("swiftjava_FakeModule_globalFunc_a_b") + public func swiftjava_FakeModule_globalFunc_a_b(a: Int32, b: Int64) -> Swift.Void /* Void */ { + globalFunc(a: a, b: b) + } + """, + """ + @_cdecl("swiftjava_FakeModule_globalFunc_a_b$1") + public func swiftjava_FakeModule_globalFunc_a_b$1(a: Double, b: Int64) -> Swift.Void /* Void */ { + globalFunc(a: a, b: b) + } + """ + ] + ) + } + +} diff --git a/Tests/JExtractSwiftTests/ThunkNameRegistryTests.swift b/Tests/JExtractSwiftTests/ThunkNameRegistryTests.swift new file mode 100644 index 00000000..58447268 --- /dev/null +++ b/Tests/JExtractSwiftTests/ThunkNameRegistryTests.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +import JExtractSwift +import Testing + +final class ThunkNameRegistryTests { + @Test("Thunk names: deduplicate names") + func deduplicate() throws { + var registry = ThunkNameRegistry() + #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello") + #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello$1") + #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello$2") + #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello$3") + #expect(registry.deduplicate(name: "swiftjava_other") == "swiftjava_other") + #expect(registry.deduplicate(name: "swiftjava_other") == "swiftjava_other$1") + } +} \ No newline at end of file diff --git a/Tests/JExtractSwiftTests/VariableImportTests.swift b/Tests/JExtractSwiftTests/VariableImportTests.swift index d776b42a..ac3b003e 100644 --- a/Tests/JExtractSwiftTests/VariableImportTests.swift +++ b/Tests/JExtractSwiftTests/VariableImportTests.swift @@ -34,138 +34,164 @@ final class VariableImportTests { """ @Test("Import: var counter: Int") - func variable_() throws { + func variable_int() throws { let st = Swift2JavaTranslator( javaPackage: "com.example.swift", - swiftModuleName: "__FakeModule" + swiftModuleName: "FakeModule" ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/Fake.swiftinterface", text: class_interfaceFile) - - let identifier = "counterInt" - let varDecl: ImportedVariable? = - st.importedTypes.values.compactMap { - $0.variables.first { - $0.identifier == identifier - } - }.first - guard let varDecl else { - fatalError("Cannot find: \(identifier)") - } - - let output = CodePrinter.toString { printer in - st.printVariableDowncallMethods(&printer, varDecl) - } - - assertOutput( - dump: true, - output, - expected: - """ - // ==== -------------------------------------------------- - // counterInt - private static class counterInt { - public static final FunctionDescriptor DESC_GET = FunctionDescriptor.of( - /* -> */SWIFT_INT, - SWIFT_POINTER - ); - public static final MemorySegment ADDR_GET = __FakeModule.findOrThrow("g"); - public static final MethodHandle HANDLE_GET = Linker.nativeLinker().downcallHandle(ADDR_GET, DESC_GET); - public static final FunctionDescriptor DESC_SET = FunctionDescriptor.ofVoid( - SWIFT_INT, - SWIFT_POINTER - ); - public static final MemorySegment ADDR_SET = __FakeModule.findOrThrow("s"); - public static final MethodHandle HANDLE_SET = Linker.nativeLinker().downcallHandle(ADDR_SET, DESC_SET); - } - /** - * Function descriptor for: - * - */ - public static FunctionDescriptor counterInt$get$descriptor() { - return counterInt.DESC_GET; - } - /** - * Downcall method handle for: - * - */ - public static MethodHandle counterInt$get$handle() { - return counterInt.HANDLE_GET; - } - /** - * Address for: - * - */ - public static MemorySegment counterInt$get$address() { - return counterInt.ADDR_GET; - } - /** - * Function descriptor for: - * - */ - public static FunctionDescriptor counterInt$set$descriptor() { - return counterInt.DESC_SET; - } - /** - * Downcall method handle for: - * - */ - public static MethodHandle counterInt$set$handle() { - return counterInt.HANDLE_SET; - } - /** - * Address for: - * - */ - public static MemorySegment counterInt$set$address() { - return counterInt.ADDR_SET; - } - /** - * Downcall to Swift: - * - */ - public static long getCounterInt(java.lang.foreign.MemorySegment self$) { - var mh$ = counterInt.HANDLE_GET; + try assertOutput( + st, input: class_interfaceFile, .java, + detectChunkByInitialLines: 7, + expectedChunks: + [ + """ + private static class counterInt { + public static final FunctionDescriptor DESC_GET = FunctionDescriptor.of( + /* -> */SWIFT_INT, + SWIFT_POINTER + ); + public static final MemorySegment ADDR_GET = + FakeModule.findOrThrow("swiftjava_FakeModule_MySwiftClass_counterInt$1"); + public static final MethodHandle HANDLE_GET = Linker.nativeLinker().downcallHandle(ADDR_GET, DESC_GET); + public static final FunctionDescriptor DESC_SET = FunctionDescriptor.ofVoid( + SWIFT_INT, + SWIFT_POINTER + ); + public static final MemorySegment ADDR_SET = + FakeModule.findOrThrow("swiftjava_FakeModule_MySwiftClass_counterInt__$1"); + public static final MethodHandle HANDLE_SET = Linker.nativeLinker().downcallHandle(ADDR_SET, DESC_SET); + } + """, + """ + /** + * Function descriptor for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static FunctionDescriptor counterInt$get$descriptor() { + return counterInt.DESC_GET; + } + """, + """ + /** + * Downcall method handle for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static MethodHandle counterInt$get$handle() { + return counterInt.HANDLE_GET; + } + """, + """ + /** + * Address for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static MemorySegment counterInt$get$address() { + return counterInt.ADDR_GET; + } + """, + """ + /** + * Function descriptor for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static FunctionDescriptor counterInt$set$descriptor() { + return counterInt.DESC_SET; + } + """ + , + """ + /** + * Downcall method handle for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static MethodHandle counterInt$set$handle() { + return counterInt.HANDLE_SET; + } + """, + """ + /** + * Address for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static MemorySegment counterInt$set$address() { + return counterInt.ADDR_SET; + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static long getCounterInt(java.lang.foreign.MemorySegment self$) { + var mh$ = counterInt.HANDLE_GET; + try { + if (TRACE_DOWNCALLS) { + traceDowncall(self$); + } + return (long) mh$.invokeExact(self$); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public long getCounterInt() { + return (long) getCounterInt($memorySegment()); + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static void setCounterInt(long newValue, java.lang.foreign.MemorySegment self$) { + var mh$ = counterInt.HANDLE_SET; try { - if (TRACE_DOWNCALLS) { - traceDowncall(self$); - } - return (long) mh$.invokeExact(self$); + if (TRACE_DOWNCALLS) { + traceDowncall(newValue, self$); + } + mh$.invokeExact(newValue, self$); } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); + throw new AssertionError("should not reach here", ex$); } - } - /** - * Downcall to Swift: - * - */ - public long getCounterInt() { - return (long) getCounterInt($memorySegment()); - } - /** - * Downcall to Swift: - * - */ - public static void setCounterInt(long newValue, java.lang.foreign.MemorySegment self$) { - var mh$ = counterInt.HANDLE_SET; - try { - if (TRACE_DOWNCALLS) { - traceDowncall(newValue, self$); - } - mh$.invokeExact(newValue, self$); - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); } - } - /** - * Downcall to Swift: - * - */ - public void setCounterInt(long newValue) { - setCounterInt(newValue, $memorySegment()); - } - """ + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public void setCounterInt(long newValue) { + setCounterInt(newValue, $memorySegment()); + } + """ + ] ) } }