diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index 8d0be4559..2808b6d31 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -25,9 +25,12 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { let toolURL = try context.tool(named: "SwiftJavaTool").url - + + var commands: [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. for dependency in target.dependencies { @@ -80,7 +83,7 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { let (moduleName, configFile) = moduleAndConfigFile return [ "--depends-on", - "\(configFile.path(percentEncoded: false))" + "\(moduleName)=\(configFile.path(percentEncoded: false))" ] } arguments += dependentConfigFilesArguments @@ -123,15 +126,165 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { print("[swift-java-plugin] Output swift files:\n - \(outputSwiftFiles.map({$0.absoluteString}).joined(separator: "\n - "))") - return [ + var jextractOutputFiles = outputSwiftFiles + + // If the developer has enabled java callbacks in the configuration (default is false) + // and we are running in JNI mode, we will run additional phases in this build plugin + // to generate Swift wrappers using wrap-java that can be used to callback to Java. + let shouldRunJavaCallbacksPhases = + if let configuration, + configuration.enableJavaCallbacks == true, + configuration.effectiveMode == .jni { + true + } else { + false + } + + // Extract list of all sources + let javaSourcesListFileName = "jextract-generated-sources.txt" + let javaSourcesFile = outputJavaDirectory.appending(path: javaSourcesListFileName) + if shouldRunJavaCallbacksPhases { + arguments += [ + "--generated-java-sources-list-file-output", javaSourcesListFileName + ] + jextractOutputFiles += [javaSourcesFile] + } + + commands += [ .buildCommand( displayName: "Generate Java wrappers for Swift types", executable: toolURL, arguments: arguments, inputFiles: [ configFile ] + swiftFiles, - outputFiles: outputSwiftFiles + outputFiles: jextractOutputFiles + ) + ] + + // If we do not need Java callbacks, we can skip the remaining steps. + guard shouldRunJavaCallbacksPhases else { + return commands + } + + // The URL of the compiled Java sources + let javaCompiledClassesURL = context.pluginWorkDirectoryURL + .appending(path: "compiled-java-output") + + // Build SwiftKitCore and get the classpath + // as the jextracted sources will depend on that + + guard let swiftJavaDirectory = findSwiftJavaDirectory(for: target) else { + fatalError("Unable to find the path to the swift-java sources, please file an issue.") + } + log("Found swift-java at \(swiftJavaDirectory)") + + let swiftKitCoreClassPath = swiftJavaDirectory.appending(path: "SwiftKitCore/build/classes/java/main") + + // We need to use a different gradle home, because + // this plugin might be run from inside another gradle task + // and that would cause conflicts. + let gradleUserHome = context.pluginWorkDirectoryURL.appending(path: "gradle-user-home") + + let GradleUserHome = "GRADLE_USER_HOME" + let gradleUserHomePath = gradleUserHome.path(percentEncoded: false) + log("Prepare command: :SwiftKitCore:build in \(GradleUserHome)=\(gradleUserHomePath)") + var gradlewEnvironment = ProcessInfo.processInfo.environment + gradlewEnvironment[GradleUserHome] = gradleUserHomePath + log("Forward environment: \(gradlewEnvironment)") + + let gradleExecutable = findExecutable(name: "gradle") ?? // try using installed 'gradle' if available in PATH + swiftJavaDirectory.appending(path: "gradlew") // fallback to calling ./gradlew if gradle is not installed + log("Detected 'gradle' executable (or gradlew fallback): \(gradleExecutable)") + + commands += [ + .buildCommand( + displayName: "Build SwiftKitCore using Gradle (Java)", + executable: gradleExecutable, + arguments: [ + ":SwiftKitCore:build", + "--project-dir", swiftJavaDirectory.path(percentEncoded: false), + "--gradle-user-home", gradleUserHomePath, + "--configure-on-demand", + "--no-daemon" + ], + environment: gradlewEnvironment, + inputFiles: [swiftJavaDirectory], + outputFiles: [swiftKitCoreClassPath] + ) + ] + + // Compile the jextracted sources + let javaHome = URL(filePath: findJavaHome()) + + commands += [ + .buildCommand( + displayName: "Build extracted Java sources", + executable: javaHome + .appending(path: "bin") + .appending(path: self.javacName), + arguments: [ + "@\(javaSourcesFile.path(percentEncoded: false))", + "-d", javaCompiledClassesURL.path(percentEncoded: false), + "-parameters", + "-classpath", swiftKitCoreClassPath.path(percentEncoded: false) + ], + inputFiles: [javaSourcesFile, swiftKitCoreClassPath], + outputFiles: [javaCompiledClassesURL] + ) + ] + + // Run `configure` to extract a swift-java config to use for wrap-java + let swiftJavaConfigURL = context.pluginWorkDirectoryURL.appending(path: "swift-java.config") + + commands += [ + .buildCommand( + displayName: "Output swift-java.config that contains all extracted Java sources", + executable: toolURL, + arguments: [ + "configure", + "--output-directory", context.pluginWorkDirectoryURL.path(percentEncoded: false), + "--cp", javaCompiledClassesURL.path(percentEncoded: false), + "--swift-module", sourceModule.name, + "--swift-type-prefix", "Java" + ], + inputFiles: [javaCompiledClassesURL], + outputFiles: [swiftJavaConfigURL] ) ] + + let singleSwiftFileOutputName = "WrapJavaGenerated.swift" + + // In the end we can run wrap-java on the previous inputs + var wrapJavaArguments = [ + "wrap-java", + "--swift-module", sourceModule.name, + "--output-directory", outputSwiftDirectory.path(percentEncoded: false), + "--config", swiftJavaConfigURL.path(percentEncoded: false), + "--cp", swiftKitCoreClassPath.path(percentEncoded: false), + "--single-swift-file-output", singleSwiftFileOutputName + ] + + // Add any dependent config files as arguments + wrapJavaArguments += dependentConfigFilesArguments + + commands += [ + .buildCommand( + displayName: "Wrap compiled Java sources using wrap-java", + executable: toolURL, + arguments: wrapJavaArguments, + inputFiles: [swiftJavaConfigURL, swiftKitCoreClassPath], + outputFiles: [outputSwiftDirectory.appending(path: singleSwiftFileOutputName)] + ) + ] + + return commands + } + + var javacName: String { +#if os(Windows) + "javac.exe" +#else + "javac" +#endif } /// Find the manifest files from other swift-java executions in any targets @@ -181,5 +334,43 @@ struct JExtractSwiftBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { return dependentConfigFiles } + + private func findSwiftJavaDirectory(for target: any Target) -> URL? { + for dependency in target.dependencies { + switch dependency { + case .target(let target): + continue + + case .product(let product): + guard let swiftJava = product.sourceModules.first(where: { $0.name == "SwiftJava" }) else { + return nil + } + + // We are inside Sources/SwiftJava + return swiftJava.directoryURL.deletingLastPathComponent().deletingLastPathComponent() + + @unknown default: + continue + } + } + + return nil + } } +func findExecutable(name: String) -> URL? { + let fileManager = FileManager.default + + guard let path = ProcessInfo.processInfo.environment["PATH"] else { + return nil + } + + for path in path.split(separator: ":") { + let fullURL = URL(fileURLWithPath: String(path)).appendingPathComponent(name) + if fileManager.isExecutableFile(atPath: fullURL.path) { + return fullURL + } + } + + return nil +} \ No newline at end of file diff --git a/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift b/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift index 7908932d1..a578091f1 100644 --- a/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift +++ b/Plugins/SwiftJavaPlugin/SwiftJavaPlugin.swift @@ -171,8 +171,6 @@ struct SwiftJavaBuildToolPlugin: SwiftJavaPluginProtocol, BuildToolPlugin { arguments += javaStdlibModules.flatMap { ["--depends-on", $0] } if !outputSwiftFiles.isEmpty { - arguments += [ configFile.path(percentEncoded: false) ] - let displayName = "Wrapping \(classes.count) Java classes in Swift target '\(sourceModule.name)'" log("Prepared: \(displayName)") commands += [ @@ -266,4 +264,4 @@ func getExtractedJavaStdlibModules() -> [String] { } return url.lastPathComponent }.sorted() -} \ No newline at end of file +} diff --git a/Samples/JavaDependencySampleApp/ci-validate.sh b/Samples/JavaDependencySampleApp/ci-validate.sh index feeb87675..1c3e2d552 100755 --- a/Samples/JavaDependencySampleApp/ci-validate.sh +++ b/Samples/JavaDependencySampleApp/ci-validate.sh @@ -3,9 +3,16 @@ set -e set -x +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + # invoke resolve as part of a build run swift build \ - --disable-experimental-prebuilts \ + $DISABLE_EXPERIMENTAL_PREBUILTS \ --disable-sandbox # explicitly invoke resolve without explicit path or dependency @@ -13,7 +20,7 @@ swift build \ # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 swift run \ - --disable-experimental-prebuilts \ + $DISABLE_EXPERIMENTAL_PREBUILTS \ swift-java resolve \ Sources/JavaCommonsCSV/swift-java.config \ --swift-module JavaCommonsCSV \ diff --git a/Samples/JavaKitSampleApp/ci-validate.sh b/Samples/JavaKitSampleApp/ci-validate.sh index 297f5c885..327baadf9 100755 --- a/Samples/JavaKitSampleApp/ci-validate.sh +++ b/Samples/JavaKitSampleApp/ci-validate.sh @@ -3,8 +3,14 @@ set -e set -x -swift build \ - --disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + +swift build $DISABLE_EXPERIMENTAL_PREBUILTS "$JAVA_HOME/bin/java" \ -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java \ diff --git a/Samples/JavaProbablyPrime/ci-validate.sh b/Samples/JavaProbablyPrime/ci-validate.sh index dc6249969..202dcbabe 100755 --- a/Samples/JavaProbablyPrime/ci-validate.sh +++ b/Samples/JavaProbablyPrime/ci-validate.sh @@ -3,7 +3,13 @@ set -e set -x -# FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + swift run \ - --disable-experimental-prebuilts \ + $DISABLE_EXPERIMENTAL_PREBUILTS \ JavaProbablyPrime 1337 \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift new file mode 100644 index 000000000..985771bef --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/CallbackProtcol.swift @@ -0,0 +1,77 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public protocol CallbackProtocol { + func withBool(_ input: Bool) -> Bool + func withInt8(_ input: Int8) -> Int8 + func withUInt16(_ input: UInt16) -> UInt16 + func withInt16(_ input: Int16) -> Int16 + func withInt32(_ input: Int32) -> Int32 + func withInt64(_ input: Int64) -> Int64 + func withFloat(_ input: Float) -> Float + func withDouble(_ input: Double) -> Double + func withString(_ input: String) -> String + func withVoid() + func withObject(_ input: MySwiftClass) -> MySwiftClass + func withOptionalInt64(_ input: Int64?) -> Int64? + func withOptionalObject(_ input: MySwiftClass?) -> Optional +} + +public struct CallbackOutput { + public let bool: Bool + public let int8: Int8 + public let uint16: UInt16 + public let int16: Int16 + public let int32: Int32 + public let int64: Int64 + public let _float: Float + public let _double: Double + public let string: String + public let object: MySwiftClass + public let optionalInt64: Int64? + public let optionalObject: MySwiftClass? +} + +public func outputCallbacks( + _ callbacks: some CallbackProtocol, + bool: Bool, + int8: Int8, + uint16: UInt16, + int16: Int16, + int32: Int32, + int64: Int64, + _float: Float, + _double: Double, + string: String, + object: MySwiftClass, + optionalInt64: Int64?, + optionalObject: MySwiftClass? +) -> CallbackOutput { + return CallbackOutput( + bool: callbacks.withBool(bool), + int8: callbacks.withInt8(int8), + uint16: callbacks.withUInt16(uint16), + int16: callbacks.withInt16(int16), + int32: callbacks.withInt32(int32), + int64: callbacks.withInt64(int64), + _float: callbacks.withFloat(_float), + _double: callbacks.withDouble(_double), + string: callbacks.withString(string), + object: callbacks.withObject(object), + optionalInt64: callbacks.withOptionalInt64(optionalInt64), + optionalObject: callbacks.withOptionalObject(optionalObject) + ) +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift new file mode 100644 index 000000000..488a78cc1 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Storage.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public class StorageItem { + public let value: Int64 + + public init(value: Int64) { + self.value = value + } +} + +public protocol Storage { + func load() -> StorageItem + func save(_ item: StorageItem) +} + +public func saveWithStorage(_ item: StorageItem, s: any Storage) { + s.save(item); +} + +public func loadWithStorage(s: any Storage) -> StorageItem { + return s.load(); +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config index 3d6a12012..52143b7f7 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/swift-java.config @@ -1,5 +1,6 @@ { "javaPackage": "com.example.swift", "mode": "jni", + "enableJavaCallbacks": true, "logLevel": "debug" } diff --git a/Samples/SwiftJavaExtractJNISampleApp/build.gradle b/Samples/SwiftJavaExtractJNISampleApp/build.gradle index a8ce51d27..7bb64c554 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/build.gradle +++ b/Samples/SwiftJavaExtractJNISampleApp/build.gradle @@ -101,7 +101,7 @@ def jextract = tasks.register("jextract", Exec) { } // FIXME: disable prebuilts until swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 - def cmdArgs = ["build", "--disable-experimental-prebuilts"] + def cmdArgs = ["build", "--disable-experimental-prebuilts", "--disable-sandbox"] // Check if the 'swiftSdk' project property was passed if (project.hasProperty('swiftSdk')) { @@ -209,3 +209,11 @@ jmh { "-Djextract.trace.downcalls=false" ] } + +task printGradleHome { + doLast { + println "Gradle Home: ${gradle.gradleHomeDir}" + println "Gradle Version: ${gradle.gradleVersion}" + println "Gradle User Home: ${gradle.gradleUserHomeDir}" + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh b/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh index ff5c32c80..d62a09102 100755 --- a/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh +++ b/Samples/SwiftJavaExtractJNISampleApp/ci-validate.sh @@ -3,7 +3,44 @@ set -x set -e -swift build --disable-experimental-prebuilts # FIXME: until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 +# WORKAROUND: prebuilts broken on Swift 6.2.1 and Linux and tests using macros https://github.com/swiftlang/swift-java/issues/418 +if [ "$(uname)" = "Darwin" ]; then + DISABLE_EXPERIMENTAL_PREBUILTS='' +else + DISABLE_EXPERIMENTAL_PREBUILTS='--disable-experimental-prebuilts' +fi + +if [[ "$(uname)" == "Darwin" && -n "$GITHUB_ACTION" ]]; then + # WORKAROUND: GitHub Actions on macOS issue with downloading gradle wrapper + # We seem to be hitting a problem when the swiftpm plugin, needs to execute gradle wrapper in a new gradle_user_home. + # Normally, this would just download gradle again and kick off a build, this seems to timeout *specifically* on + # github actions runners. + # + # It is not a sandbox problem, becuase the ./gradlew is run without sandboxing as we already execute + # the entire swift build with '--disable-sandbox' for other reasons. + # + # We cannot use the same gradle user home as the default one since we might make gradle think we're + # building the same project concurrently, which we kind of are, however only a limited subset in order + # to trigger wrap-java with those dependencies. + # + # TODO: this may use some further improvements so normal usage does not incur another wrapper download. + + ./gradlew -h # prime ~/.gradle/wrapper/dists/... + + # Worst part of workaround here; we make sure to pre-load the resolved gradle wrapper downloaded distribution + # to the "known" location the plugin will use for its local builds, which are done in order to compile SwiftKitCore. + # This build is only necessary in order to drive wrap-java on sources generated during the build itself + # which enables the "Implement Swift protocols in Java" feature of jextract/jni mode. + GRADLE_USER_HOME="$(pwd)/.build/plugins/outputs/swiftjavaextractjnisampleapp/MySwiftLibrary/destination/JExtractSwiftPlugin/gradle-user-home" + if [ -d "$HOME/.gradle" ] ; then + echo "COPY $HOME/.gradle to $GRADLE_USER_HOME" + mkdir -p "$GRADLE_USER_HOME" + cp -r "$HOME/.gradle/"* "$GRADLE_USER_HOME/" || true + fi +fi + +# FIXME: disable prebuilts until prebuilt swift-syntax isn't broken on 6.2 anymore: https://github.com/swiftlang/swift-java/issues/418 +swift build $DISABLE_EXPERIMENTAL_PREBUILTS --disable-sandbox ./gradlew run -./gradlew test \ No newline at end of file +./gradlew test diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java new file mode 100644 index 000000000..e79fd4a3b --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolCallbacksTest.java @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// +// 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.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; +import org.swift.swiftkit.core.annotations.Unsigned; + +import java.util.Optional; +import java.util.OptionalLong; + +import static org.junit.jupiter.api.Assertions.*; + +public class ProtocolCallbacksTest { + static class JavaCallbacks implements CallbackProtocol { + @Override + public boolean withBool(boolean input) { + return input; + } + + @Override + public byte withInt8(byte input) { + return input; + } + + @Override + public @Unsigned char withUInt16(char input) { + return input; + } + + @Override + public short withInt16(short input) { + return input; + } + + @Override + public int withInt32(int input) { + return input; + } + + @Override + public long withInt64(long input) { + return input; + } + + @Override + public float withFloat(float input) { + return input; + } + + @Override + public double withDouble(double input) { + return input; + } + + @Override + public String withString(String input) { + return input; + } + + @Override + public void withVoid() {} + + @Override + public MySwiftClass withObject(MySwiftClass input) { + return input; + } + + @Override + public OptionalLong withOptionalInt64(OptionalLong input) { + return input; + } + + @Override + public Optional withOptionalObject(Optional input) { + return input; + } + } + + @Test + void primitiveCallbacks() { + try (var arena = SwiftArena.ofConfined()) { + JavaCallbacks callbacks = new JavaCallbacks(); + var object = MySwiftClass.init(5, 3, arena); + var optionalObject = Optional.of(MySwiftClass.init(10, 10, arena)); + var output = MySwiftLibrary.outputCallbacks(callbacks, true, (byte) 1, (char) 16, (short) 16, (int) 32, 64L, 1.34f, 1.34, "Hello from Java!", object, OptionalLong.empty(), optionalObject, arena); + + assertEquals(1, output.getInt8()); + assertEquals(16, output.getUint16()); + assertEquals(16, output.getInt16()); + assertEquals(32, output.getInt32()); + assertEquals(64, output.getInt64()); + assertEquals(1.34f, output.get_float()); + assertEquals(1.34, output.get_double()); + assertEquals("Hello from Java!", output.getString()); + assertFalse(output.getOptionalInt64().isPresent()); + assertEquals(5, output.getObject(arena).getX()); + assertEquals(3, output.getObject(arena).getY()); + + var optionalObjectOutput = output.getOptionalObject(arena); + assertTrue(optionalObjectOutput.isPresent()); + assertEquals(10, optionalObjectOutput.get().getX()); + } + } +} \ No newline at end of file diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java index c095a42a4..f5d1ffcf7 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java @@ -72,4 +72,34 @@ void protocolMethod() { assertEquals("ConcreteProtocolAB", proto1.name()); } } + + static class JavaStorage implements Storage { + StorageItem item; + + JavaStorage(StorageItem item) { + this.item = item; + } + + @Override + public StorageItem load() { + return item; + } + + @Override + public void save(StorageItem item) { + this.item = item; + } + } + + @Test + void useStorage() { + try (var arena = SwiftArena.ofConfined()) { + JavaStorage storage = new JavaStorage(null); + MySwiftLibrary.saveWithStorage(StorageItem.init(10, arena), storage); + assertEquals(10, MySwiftLibrary.loadWithStorage(storage, arena).getValue()); + MySwiftLibrary.saveWithStorage(StorageItem.init(7, arena), storage); + MySwiftLibrary.saveWithStorage(StorageItem.init(5, arena), storage); + assertEquals(5, MySwiftLibrary.loadWithStorage(storage, arena).getValue()); + } + } } \ No newline at end of file diff --git a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift index 645e5aa48..9f7a19cce 100644 --- a/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/JavaType+Extensions.swift @@ -102,7 +102,7 @@ extension JavaType { } } - /// Returns whether this type returns `JavaValue` from JavaKit + /// Returns whether this type returns `JavaValue` from SwiftJava var implementsJavaValue: Bool { return switch self { case .boolean, .byte, .char, .short, .int, .long, .float, .double, .void, .javaLangString: diff --git a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift index 82ce5c1c0..5641c7f23 100644 --- a/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/String+Extensions.swift @@ -68,7 +68,7 @@ extension String { /// Looks up self as a SwiftJava wrapped class name and converts it /// into a `JavaType.class` if it exists in `lookupTable`. - func parseJavaClassFromJavaKitName(in lookupTable: [String: String]) -> JavaType? { + func parseJavaClassFromSwiftJavaName(in lookupTable: [String: String]) -> JavaType? { guard let canonicalJavaName = lookupTable[self] else { return nil } diff --git a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift index d3902aa4d..c9f87b6dd 100644 --- a/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift +++ b/Sources/JExtractSwiftLib/Convenience/SwiftSyntax+Extensions.swift @@ -128,7 +128,7 @@ extension WithModifiersSyntax { } extension AttributeListSyntax.Element { - /// Whether this node has `JavaKit` attributes. + /// Whether this node has `SwiftJava` attributes. var isJava: Bool { guard case let .attribute(attr) = self else { // FIXME: Handle #if. diff --git a/Sources/JExtractSwiftLib/ImportedDecls.swift b/Sources/JExtractSwiftLib/ImportedDecls.swift index b9cc2d497..aa4014d21 100644 --- a/Sources/JExtractSwiftLib/ImportedDecls.swift +++ b/Sources/JExtractSwiftLib/ImportedDecls.swift @@ -264,3 +264,12 @@ extension ImportedFunc { } } } + +extension ImportedNominalType: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + public static func == (lhs: ImportedNominalType, rhs: ImportedNominalType) -> Bool { + return lhs === rhs + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift new file mode 100644 index 000000000..f1e7c6851 --- /dev/null +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -0,0 +1,342 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JavaTypes +import SwiftJavaConfigurationShared +import SwiftSyntax + +extension JNISwift2JavaGenerator { + + func generateInterfaceWrappers( + _ types: [ImportedNominalType] + ) -> [ImportedNominalType: JavaInterfaceSwiftWrapper] { + var wrappers = [ImportedNominalType: JavaInterfaceSwiftWrapper]() + + for type in types { + do { + let translator = JavaInterfaceProtocolWrapperGenerator() + wrappers[type] = try translator.generate(for: type) + } catch { + self.logger.warning("Failed to generate protocol wrapper for: '\(type.swiftNominal.qualifiedName)'; \(error)") + } + } + + return wrappers + } + + /// A type that describes a Swift protocol + /// that uses an underlying wrap-java `@JavaInterface` + /// to make callbacks to Java from Swift using protocols. + struct JavaInterfaceSwiftWrapper { + let protocolType: SwiftNominalType + let functions: [Function] + let variables: [Variable] + + var wrapperName: String { + protocolType.nominalTypeDecl.javaInterfaceSwiftProtocolWrapperName + } + + var swiftName: String { + protocolType.nominalTypeDecl.qualifiedName + } + + var javaInterfaceVariableName: String { + protocolType.nominalTypeDecl.javaInterfaceVariableName + } + + var javaInterfaceName: String { + protocolType.nominalTypeDecl.javaInterfaceName + } + + struct Function { + let swiftFunctionName: String + let originalFunctionSignature: SwiftFunctionSignature + let swiftDecl: any DeclSyntaxProtocol + let parameterConversions: [UpcallConversionStep] + let resultConversion: UpcallConversionStep + } + + struct Variable { + let swiftDecl: any DeclSyntaxProtocol + let getter: Function + let setter: Function? + } + } + + + struct JavaInterfaceProtocolWrapperGenerator { + func generate(for type: ImportedNominalType) throws -> JavaInterfaceSwiftWrapper { + let functions = try type.methods.map { method in + try translate(function: method) + } + + // FIXME: Finish support for variables + if !type.variables.isEmpty { + throw JavaTranslationError.protocolVariablesNotSupported + } + + let variables = try Dictionary(grouping: type.variables, by: { $0.swiftDecl.id }).map { (id, funcs) in + precondition(funcs.count > 0 && funcs.count <= 2, "Variables must contain a getter and optionally a setter") + guard let getter = funcs.first(where: { $0.apiKind == .getter }) else { + fatalError("Getter not found for variable with imported funcs: \(funcs)") + } + let setter = funcs.first(where: { $0.apiKind == .setter }) + + return try self.translateVariable(getter: getter, setter: setter) + } + + return JavaInterfaceSwiftWrapper( + protocolType: SwiftNominalType(nominalTypeDecl: type.swiftNominal), + functions: functions, + variables: variables + ) + } + + private func translate(function: ImportedFunc) throws -> JavaInterfaceSwiftWrapper.Function { + let parameters = try function.functionSignature.parameters.map { + try self.translateParameter($0) + } + + let result = try translateResult(function.functionSignature.result, methodName: function.name) + + return JavaInterfaceSwiftWrapper.Function( + swiftFunctionName: function.name, + originalFunctionSignature: function.functionSignature, + swiftDecl: function.swiftDecl, + parameterConversions: parameters, + resultConversion: result + ) + } + + private func translateVariable(getter: ImportedFunc, setter: ImportedFunc?) throws -> JavaInterfaceSwiftWrapper.Variable { + return try JavaInterfaceSwiftWrapper.Variable( + swiftDecl: getter.swiftDecl, // they should be the same + getter: translate(function: getter), + setter: setter.map { try self.translate(function: $0) } + ) + } + + private func translateParameter(_ parameter: SwiftParameter) throws -> UpcallConversionStep { + try self.translateParameter(parameterName: parameter.parameterName!, type: parameter.type) + } + + private func translateParameter(parameterName: String, type: SwiftType) throws -> UpcallConversionStep { + + switch type { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return try translateOptionalParameter( + name: parameterName, + wrappedType: genericArgs[0] + ) + + default: + guard knownType.isDirectlyTranslatedToWrapJava else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + + return .placeholder + } + } + + // We assume this is then a JExtracted Swift class + return .toJavaWrapper( + .placeholder, + name: parameterName, + nominalType: nominalType + ) + + case .tuple([]): // void + return .placeholder + + case .optional(let wrappedType): + return try translateOptionalParameter( + name: parameterName, + wrappedType: wrappedType + ) + + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .array: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + + private func translateOptionalParameter(name: String, wrappedType: SwiftType) throws -> UpcallConversionStep { + let wrappedConversion = try translateParameter(parameterName: name, type: wrappedType) + return .toJavaOptional(.map(.placeholder, body: wrappedConversion)) + } + + private func translateResult(_ result: SwiftResult, methodName: String) throws -> UpcallConversionStep { + try self.translateResult(type: result.type, methodName: methodName) + } + + private func translateResult( + type: SwiftType, + methodName: String, + allowNilForObjects: Bool = false + ) throws -> UpcallConversionStep { + switch type { + case .nominal(let nominalType): + if let knownType = nominalType.nominalTypeDecl.knownTypeKind { + switch knownType { + case .optional: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return try self.translateOptionalResult( + wrappedType: genericArgs[0], + methodName: methodName + ) + + default: + guard knownType.isDirectlyTranslatedToWrapJava else { + throw JavaTranslationError.unsupportedSwiftType(type) + } + return .placeholder + } + } + + let inner: UpcallConversionStep = !allowNilForObjects ? + .unwrapOptional(.placeholder, message: "Upcall to \(methodName) unexpectedly returned nil") + : .placeholder + + // We assume this is then a JExtracted Swift class + return .toSwiftClass( + inner, + name: "result$", + nominalType: nominalType + ) + + case .tuple([]): // void + return .placeholder + + case .optional(let wrappedType): + return try self.translateOptionalResult(wrappedType: wrappedType, methodName: methodName) + + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite, .array: + throw JavaTranslationError.unsupportedSwiftType(type) + } + } + + private func translateOptionalResult(wrappedType: SwiftType, methodName: String) throws -> UpcallConversionStep { + // The `fromJavaOptional` will handle the nullability + let wrappedConversion = try translateResult( + type: wrappedType, + methodName: methodName, + allowNilForObjects: true + ) + return .map(.fromJavaOptional(.placeholder), body: wrappedConversion) + } + } +} + + /// Describes how to convert values from and to wrap-java types + enum UpcallConversionStep { + case placeholder + + case constant(String) + + indirect case toJavaWrapper( + UpcallConversionStep, + name: String, + nominalType: SwiftNominalType + ) + + indirect case toSwiftClass( + UpcallConversionStep, + name: String, + nominalType: SwiftNominalType + ) + + indirect case unwrapOptional( + UpcallConversionStep, + message: String + ) + + indirect case toJavaOptional(UpcallConversionStep) + + indirect case fromJavaOptional(UpcallConversionStep) + + indirect case map(UpcallConversionStep, body: UpcallConversionStep) + + /// Returns the conversion string applied to the placeholder. + func render(_ printer: inout CodePrinter, _ placeholder: String) -> String { + switch self { + case .placeholder: + return placeholder + + case .constant(let constant): + return constant + + case .toJavaWrapper(let inner, let name, let nominalType): + let inner = inner.render(&printer, placeholder) + printer.print( + """ + let \(name)Class = try! JavaClass<\(nominalType.nominalTypeDecl.generatedJavaClassMacroName)>(environment: JavaVirtualMachine.shared().environment()) + let \(name)Pointer = UnsafeMutablePointer<\(nominalType.nominalTypeDecl.qualifiedName)>.allocate(capacity: 1) + \(name)Pointer.initialize(to: \(inner)) + """ + ) + + return "\(name)Class.wrapMemoryAddressUnsafe(Int64(Int(bitPattern: \(name)Pointer)))" + + case .toSwiftClass(let inner, let name, let nominalType): + let inner = inner.render(&printer, placeholder) + + // The wrap-java methods will return null + printer.print( + """ + let \(name)MemoryAddress$ = \(inner).as(JavaJNISwiftInstance.self)!.memoryAddress() + let \(name)Pointer = UnsafeMutablePointer<\(nominalType.nominalTypeDecl.qualifiedName)>(bitPattern: Int(\(name)MemoryAddress$))! + """ + ) + + return "\(name)Pointer.pointee" + + case .unwrapOptional(let inner, let message): + let inner = inner.render(&printer, placeholder) + + printer.print( + """ + guard let unwrapped$ = \(inner) else { + fatalError("\(message)") + } + """ + ) + + return "unwrapped$" + + case .toJavaOptional(let inner): + let inner = inner.render(&printer, placeholder) + return "\(inner).toJavaOptional()" + + case .fromJavaOptional(let inner): + let inner = inner.render(&printer, placeholder) + return "Optional(javaOptional: \(inner))" + + case .map(let inner, let body): + let inner = inner.render(&printer, placeholder) + var printer = CodePrinter() + printer.printBraceBlock("\(inner).map") { printer in + let body = body.render(&printer, "$0") + printer.print("return \(body)") + } + return printer.finalize() + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 1e18cbe8f..c492d439d 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -12,7 +12,9 @@ // //===----------------------------------------------------------------------===// +import Foundation import JavaTypes +import OrderedCollections // MARK: Defaults @@ -40,6 +42,8 @@ extension JNISwift2JavaGenerator { package func writeExportedJavaSources(_ printer: inout CodePrinter) throws { let importedTypes = analysis.importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) + var exportedFileNames: OrderedSet = [] + // Each parent type goes into its own file // any nested types are printed inside the body as `static class` for (_, ty) in importedTypes.filter({ _, type in type.parent == nil }) { @@ -52,6 +56,7 @@ extension JNISwift2JavaGenerator { javaPackagePath: javaPackagePath, filename: filename ) { + exportedFileNames.append(outputFile.path(percentEncoded: false)) logger.info("[swift-java] Generated: \(ty.swiftNominal.name.bold).java (at \(outputFile))") } } @@ -65,10 +70,24 @@ extension JNISwift2JavaGenerator { javaPackagePath: javaPackagePath, filename: filename ) { + exportedFileNames.append(outputFile.path(percentEncoded: false)) logger.info("[swift-java] Generated: \(self.swiftModuleName).java (at \(outputFile))") } + + // Write java sources list file + if let generatedJavaSourcesListFileOutput = config.generatedJavaSourcesListFileOutput, !exportedFileNames.isEmpty { + let outputPath = URL(fileURLWithPath: javaOutputDirectory).appending(path: generatedJavaSourcesListFileOutput) + try exportedFileNames.joined(separator: "\n").write( + to: outputPath, + atomically: true, + encoding: .utf8 + ) + logger.info("Generated file at \(outputPath)") + } } + + private func printModule(_ printer: inout CodePrinter) { printHeader(&printer) printPackage(&printer) @@ -113,20 +132,29 @@ extension JNISwift2JavaGenerator { } private func printProtocol(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { - let extends = ["JNISwiftInstance"] - printer.printBraceBlock("public interface \(decl.swiftNominal.name) extends \(extends.joined(separator: ", "))") { printer in + var extends = [String]() + + // If we cannot generate Swift wrappers + // that allows the user to implement the wrapped interface in Java + // then we require only JExtracted types can conform to this. + if !self.interfaceProtocolWrappers.keys.contains(decl) { + extends.append("JNISwiftInstance") + } + let extendsString = extends.isEmpty ? "" : " extends \(extends.joined(separator: ", "))" + + printer.printBraceBlock("public interface \(decl.swiftNominal.name)\(extendsString)") { printer in for initializer in decl.initializers { - printFunctionDowncallMethods(&printer, initializer, signaturesOnly: true) + printFunctionDowncallMethods(&printer, initializer, skipMethodBody: true, skipArenas: true) printer.println() } for method in decl.methods { - printFunctionDowncallMethods(&printer, method, signaturesOnly: true) + printFunctionDowncallMethods(&printer, method, skipMethodBody: true, skipArenas: true) printer.println() } for variable in decl.variables { - printFunctionDowncallMethods(&printer, variable, signaturesOnly: true) + printFunctionDowncallMethods(&printer, variable, skipMethodBody: true, skipArenas: true) printer.println() } } @@ -184,6 +212,10 @@ extension JNISwift2JavaGenerator { public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) { return new \(decl.swiftNominal.name)(selfPointer, swiftArena); } + + public static \(decl.swiftNominal.name) wrapMemoryAddressUnsafe(long selfPointer) { + return new \(decl.swiftNominal.name)(selfPointer, SwiftMemoryManagement.GLOBAL_SWIFT_JAVA_ARENA); + } """ ) @@ -359,10 +391,10 @@ extension JNISwift2JavaGenerator { ["\(conversion.native.javaType) \(value.parameter.name)"] } - printer.print("record $NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") + printer.print("record _NativeParameters(\(nativeParameters.joined(separator: ", "))) {}") } - self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction, signaturesOnly: false) + self.printJavaBindingWrapperMethod(&printer, translatedCase.getAsCaseFunction, skipMethodBody: false, skipArenas: false) printer.println() } } @@ -370,7 +402,8 @@ extension JNISwift2JavaGenerator { private func printFunctionDowncallMethods( _ printer: inout CodePrinter, _ decl: ImportedFunc, - signaturesOnly: Bool = false + skipMethodBody: Bool = false, + skipArenas: Bool = false ) { guard translatedDecl(for: decl) != nil else { // Failed to translate. Skip. @@ -381,7 +414,7 @@ extension JNISwift2JavaGenerator { printJavaBindingWrapperHelperClass(&printer, decl) - printJavaBindingWrapperMethod(&printer, decl, signaturesOnly: signaturesOnly) + printJavaBindingWrapperMethod(&printer, decl, skipMethodBody: skipMethodBody, skipArenas: skipArenas) } /// Print the helper type container for a user-facing Java API. @@ -427,19 +460,21 @@ extension JNISwift2JavaGenerator { private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ decl: ImportedFunc, - signaturesOnly: Bool + skipMethodBody: Bool, + skipArenas: Bool ) { guard let translatedDecl = translatedDecl(for: decl) else { fatalError("Decl was not translated, \(decl)") } - printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl, signaturesOnly: signaturesOnly) + printJavaBindingWrapperMethod(&printer, translatedDecl, importedFunc: decl, skipMethodBody: skipMethodBody, skipArenas: skipArenas) } private func printJavaBindingWrapperMethod( _ printer: inout CodePrinter, _ translatedDecl: TranslatedFunctionDecl, importedFunc: ImportedFunc? = nil, - signaturesOnly: Bool + skipMethodBody: Bool, + skipArenas: Bool ) { var modifiers = ["public"] if translatedDecl.isStatic { @@ -494,14 +529,14 @@ extension JNISwift2JavaGenerator { printer.println() } - if translatedSignature.requiresSwiftArena { + if translatedSignature.requiresSwiftArena, !skipArenas { parameters.append("SwiftArena swiftArena$") } if let importedFunc { printDeclDocumentation(&printer, importedFunc) } let signature = "\(annotationsStr)\(modifiers.joined(separator: " ")) \(resultType) \(translatedDecl.name)(\(parameters.joined(separator: ", ")))\(throwsClause)" - if signaturesOnly { + if skipMethodBody { printer.print("\(signature);") } else { printer.printBraceBlock(signature) { printer in diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index 6a2771c30..553b3e10b 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -16,6 +16,17 @@ import JavaTypes import SwiftJavaConfigurationShared extension JNISwift2JavaGenerator { + var javaTranslator: JavaTranslation { + JavaTranslation( + config: config, + swiftModuleName: swiftModuleName, + javaPackage: self.javaPackage, + javaClassLookupTable: self.javaClassLookupTable, + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), + protocolWrappers: self.interfaceProtocolWrappers + ) + } + func translatedDecl( for decl: ImportedFunc ) -> TranslatedFunctionDecl? { @@ -25,14 +36,7 @@ extension JNISwift2JavaGenerator { let translated: TranslatedFunctionDecl? do { - let translation = JavaTranslation( - config: config, - swiftModuleName: swiftModuleName, - javaPackage: self.javaPackage, - javaClassLookupTable: self.javaClassLookupTable, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) - ) - translated = try translation.translate(decl) + translated = try self.javaTranslator.translate(decl) } catch { self.logger.debug("Failed to translate: '\(decl.swiftDecl.qualifiedNameForDebug)'; \(error)") translated = nil @@ -56,7 +60,8 @@ extension JNISwift2JavaGenerator { swiftModuleName: swiftModuleName, javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, - knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable) + knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), + protocolWrappers: self.interfaceProtocolWrappers ) translated = try translation.translate(enumCase: decl) } catch { @@ -74,13 +79,15 @@ extension JNISwift2JavaGenerator { let javaPackage: String let javaClassLookupTable: JavaClassLookupTable var knownTypes: SwiftKnownTypes + let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] func translate(enumCase: ImportedEnumCase) throws -> TranslatedEnumCase { let nativeTranslation = NativeJavaTranslation( config: self.config, javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, - knownTypes: self.knownTypes + knownTypes: self.knownTypes, + protocolWrappers: self.protocolWrappers ) let methodName = "" // TODO: Used for closures, replace with better name? @@ -105,7 +112,7 @@ extension JNISwift2JavaGenerator { let caseName = enumCase.name.firstCharacterUppercased let enumName = enumCase.enumType.nominalTypeDecl.name - let nativeParametersType = JavaType.class(package: nil, name: "\(caseName).$NativeParameters") + let nativeParametersType = JavaType.class(package: nil, name: "\(caseName)._NativeParameters") let getAsCaseName = "getAs\(caseName)" // If the case has no parameters, we can skip the native call. let constructRecordConversion = JavaNativeConversionStep.method(.constant("Optional"), function: "of", arguments: [ @@ -168,7 +175,8 @@ extension JNISwift2JavaGenerator { config: self.config, javaPackage: self.javaPackage, javaClassLookupTable: self.javaClassLookupTable, - knownTypes: self.knownTypes + knownTypes: self.knownTypes, + protocolWrappers: self.protocolWrappers ) // Types with no parent will be outputted inside a "module" class. @@ -405,8 +413,8 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isJavaKitWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) } @@ -456,7 +464,7 @@ extension JNISwift2JavaGenerator { return try translateProtocolParameter( protocolType: proto, parameterName: parameterName, - javaGenericName: "$T\(parameterPosition)" + javaGenericName: "_T\(parameterPosition)" ) case .genericParameter(let generic): @@ -585,14 +593,14 @@ extension JNISwift2JavaGenerator { } } - // We assume this is a JExtract class. + // We just pass down the jobject return TranslatedParameter( parameter: JavaParameter( name: parameterName, type: .generic(name: javaGenericName, extends: javaProtocolTypes), annotations: [] ), - conversion: .commaSeparated([.valueMemoryAddress(.placeholder), .typeMetadataAddress(.placeholder)]) + conversion: .placeholder ) } @@ -628,8 +636,8 @@ extension JNISwift2JavaGenerator { ) } - if nominalType.isJavaKitWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) } @@ -703,7 +711,7 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isJavaKitWrapper { + if nominalType.isSwiftJavaWrapper { throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -789,7 +797,7 @@ extension JNISwift2JavaGenerator { } } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -836,7 +844,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { throw JavaTranslationError.unsupportedSwiftType(elementType) } @@ -885,7 +893,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { throw JavaTranslationError.unsupportedSwiftType(elementType) } @@ -965,7 +973,7 @@ extension JNISwift2JavaGenerator { /// Function signature of the native function that will be implemented by Swift let nativeFunctionSignature: NativeFunctionSignature - + /// Annotations to include on the Java function declaration var annotations: [JavaAnnotation] { self.translatedFunctionSignature.annotations @@ -1332,5 +1340,8 @@ extension JNISwift2JavaGenerator { /// The user has not supplied a mapping from `SwiftType` to /// a java class. case wrappedJavaClassTranslationNotProvided(SwiftType) + + // FIXME: Remove once we support protocol variables + case protocolVariablesNotSupported } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 24e469067..b744a33b4 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -22,6 +22,7 @@ extension JNISwift2JavaGenerator { let javaPackage: String let javaClassLookupTable: JavaClassLookupTable var knownTypes: SwiftKnownTypes + let protocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] /// Translates a Swift function into the native JNI method signature. func translate( @@ -113,8 +114,8 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isJavaKitWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(type) } @@ -122,7 +123,7 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .initializeJavaKitWrapper( + conversion: .initializeSwiftJavaWrapper( .unwrapOptional( .placeholder, name: parameterName, @@ -181,14 +182,18 @@ extension JNISwift2JavaGenerator { case .opaque(let proto), .existential(let proto): return try translateProtocolParameter( protocolType: proto, - parameterName: parameterName + methodName: methodName, + parameterName: parameterName, + parentName: parentName ) case .genericParameter: if let concreteTy = type.typeIn(genericParameters: genericParameters, genericRequirements: genericRequirements) { return try translateProtocolParameter( protocolType: concreteTy, - parameterName: parameterName + methodName: methodName, + parameterName: parameterName, + parentName: parentName ) } @@ -207,22 +212,23 @@ extension JNISwift2JavaGenerator { func translateProtocolParameter( protocolType: SwiftType, - parameterName: String + methodName: String, + parameterName: String, + parentName: String? ) throws -> NativeParameter { switch protocolType { case .nominal(let nominalType): - let protocolName = nominalType.nominalTypeDecl.qualifiedName - return try translateProtocolParameter(protocolNames: [protocolName], parameterName: parameterName) + return try translateProtocolParameter(protocolTypes: [nominalType], methodName: methodName, parameterName: parameterName, parentName: parentName) case .composite(let types): - let protocolNames = try types.map { - guard let nominalTypeName = $0.asNominalType?.nominalTypeDecl.qualifiedName else { + let protocolTypes = try types.map { + guard let nominalTypeName = $0.asNominalType else { throw JavaTranslationError.unsupportedSwiftType($0) } return nominalTypeName } - return try translateProtocolParameter(protocolNames: protocolNames, parameterName: parameterName) + return try translateProtocolParameter(protocolTypes: protocolTypes, methodName: methodName, parameterName: parameterName, parentName: parentName) default: throw JavaTranslationError.unsupportedSwiftType(protocolType) @@ -230,18 +236,30 @@ extension JNISwift2JavaGenerator { } private func translateProtocolParameter( - protocolNames: [String], - parameterName: String + protocolTypes: [SwiftNominalType], + methodName: String, + parameterName: String, + parentName: String? ) throws -> NativeParameter { + // We allow Java implementations if we are able to generate the needed + // Swift wrappers for all the protocol types. + let allowsJavaImplementations = protocolTypes.allSatisfy { protocolType in + self.protocolWrappers.contains(where: { $0.value.protocolType == protocolType }) + } + return NativeParameter( parameters: [ - JavaParameter(name: parameterName, type: .long), - JavaParameter(name: "\(parameterName)_typeMetadataAddress", type: .long) + JavaParameter(name: parameterName, type: .javaLangObject) ], - conversion: .extractSwiftProtocolValue( + conversion: .interfaceToSwiftObject( .placeholder, - typeMetadataVariableName: .combinedName(component: "typeMetadataAddress"), - protocolNames: protocolNames + swiftWrapperClassName: JNISwift2JavaGenerator.protocolParameterWrapperClassName( + methodName: methodName, + parameterName: parameterName, + parentName: parentName + ), + protocolTypes: protocolTypes, + allowsJavaImplementations: allowsJavaImplementations ) ) } @@ -276,8 +294,8 @@ extension JNISwift2JavaGenerator { ) } - if nominalType.isJavaKitWrapper { - guard let javaType = nominalTypeName.parseJavaClassFromJavaKitName(in: self.javaClassLookupTable) else { + if nominalType.isSwiftJavaWrapper { + guard let javaType = nominalTypeName.parseJavaClassFromSwiftJavaName(in: self.javaClassLookupTable) else { throw JavaTranslationError.wrappedJavaClassTranslationNotProvided(swiftType) } @@ -285,7 +303,7 @@ extension JNISwift2JavaGenerator { parameters: [ JavaParameter(name: parameterName, type: javaType) ], - conversion: .optionalMap(.initializeJavaKitWrapper(.placeholder, wrapperName: nominalTypeName)) + conversion: .optionalMap(.initializeSwiftJavaWrapper(.placeholder, wrapperName: nominalTypeName)) ) } @@ -359,7 +377,7 @@ extension JNISwift2JavaGenerator { } } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { // TODO: Should be the same as above throw JavaTranslationError.unsupportedSwiftType(swiftType) } @@ -479,7 +497,7 @@ extension JNISwift2JavaGenerator { } } - if nominalType.isJavaKitWrapper { + if nominalType.isSwiftJavaWrapper { throw JavaTranslationError.unsupportedSwiftType(swiftResult.type) } @@ -526,7 +544,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) } @@ -574,7 +592,7 @@ extension JNISwift2JavaGenerator { ) } - guard !nominalType.isJavaKitWrapper else { + guard !nominalType.isSwiftJavaWrapper else { throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) } @@ -645,6 +663,13 @@ extension JNISwift2JavaGenerator { /// `SwiftType(from: value, in: environment)` indirect case initFromJNI(NativeSwiftConversionStep, swiftType: SwiftType) + indirect case interfaceToSwiftObject( + NativeSwiftConversionStep, + swiftWrapperClassName: String, + protocolTypes: [SwiftNominalType], + allowsJavaImplementations: Bool + ) + indirect case extractSwiftProtocolValue( NativeSwiftConversionStep, typeMetadataVariableName: NativeSwiftConversionStep, @@ -668,7 +693,7 @@ extension JNISwift2JavaGenerator { indirect case closureLowering(parameters: [NativeParameter], result: NativeResult) - indirect case initializeJavaKitWrapper(NativeSwiftConversionStep, wrapperName: String) + indirect case initializeSwiftJavaWrapper(NativeSwiftConversionStep, wrapperName: String) indirect case optionalLowering(NativeSwiftConversionStep, discriminatorName: String, valueName: String) @@ -723,6 +748,60 @@ extension JNISwift2JavaGenerator { let inner = inner.render(&printer, placeholder) return "\(swiftType)(fromJNI: \(inner), in: environment)" + case .interfaceToSwiftObject( + let inner, + let swiftWrapperClassName, + let protocolTypes, + let allowsJavaImplementations + ): + let protocolNames = protocolTypes.map { $0.nominalTypeDecl.qualifiedName } + + let inner = inner.render(&printer, placeholder) + let variableName = "\(inner)swiftObject$" + let compositeProtocolName = "(\(protocolNames.joined(separator: " & ")))" + printer.print("let \(variableName): \(compositeProtocolName)") + + func printStandardJExtractBlock(_ printer: inout CodePrinter) { + let pointerVariableName = "\(inner)pointer$" + let typeMetadataVariableName = "\(inner)typeMetadata$" + printer.print( + """ + let \(pointerVariableName) = environment.interface.CallLongMethodA(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) + let \(typeMetadataVariableName) = environment.interface.CallLongMethodA(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) + """ + ) + let existentialName = NativeSwiftConversionStep.extractSwiftProtocolValue( + .constant(pointerVariableName), + typeMetadataVariableName: .constant(typeMetadataVariableName), + protocolNames: protocolNames + ).render(&printer, placeholder) + + printer.print("\(variableName) = \(existentialName)") + } + + // If this protocol type supports being implemented by the user + // then we will check whether it is a JNI SwiftInstance type + // or if its a custom class implementing the interface. + if allowsJavaImplementations { + printer.printBraceBlock( + "if environment.interface.IsInstanceOf(environment, \(inner), _JNIMethodIDCache.JNISwiftInstance.class) != 0" + ) { printer in + printStandardJExtractBlock(&printer) + } + printer.printBraceBlock("else") { printer in + let arguments = protocolTypes.map { protocolType in + let nominalTypeDecl = protocolType.nominalTypeDecl + return "\(nominalTypeDecl.javaInterfaceVariableName): \(nominalTypeDecl.javaInterfaceName)(javaThis: \(inner)!, environment: environment)" + } + printer.print("\(variableName) = \(swiftWrapperClassName)(\(arguments.joined(separator: ", ")))") + } + } else { + printStandardJExtractBlock(&printer) + } + + + return variableName + case .extractSwiftProtocolValue(let inner, let typeMetadataVariableName, let protocolNames): let inner = inner.render(&printer, placeholder) let typeMetadataVariableName = typeMetadataVariableName.render(&printer, placeholder) @@ -839,7 +918,7 @@ extension JNISwift2JavaGenerator { return printer.finalize() - case .initializeJavaKitWrapper(let inner, let wrapperName): + case .initializeSwiftJavaWrapper(let inner, let wrapperName): let inner = inner.render(&printer, placeholder) return "\(wrapperName)(javaThis: \(inner), environment: environment)" diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 6628be333..85fdc6404 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -116,6 +116,75 @@ extension JNISwift2JavaGenerator { } } + /// Prints the extension needed to make allow upcalls from Swift to Java for protocols + private func printSwiftInterfaceWrapper( + _ printer: inout CodePrinter, + _ translatedWrapper: JavaInterfaceSwiftWrapper + ) throws { + printer.printBraceBlock("protocol \(translatedWrapper.wrapperName): \(translatedWrapper.swiftName)") { printer in + printer.print("var \(translatedWrapper.javaInterfaceVariableName): \(translatedWrapper.javaInterfaceName) { get }") + } + printer.println() + printer.printBraceBlock("extension \(translatedWrapper.wrapperName)") { printer in + for function in translatedWrapper.functions { + printInterfaceWrapperFunctionImpl(&printer, function, inside: translatedWrapper) + printer.println() + } + + // FIXME: Add support for protocol variables https://github.com/swiftlang/swift-java/issues/457 +// for variable in translatedWrapper.variables { +// printerInterfaceWrapperVariable(&printer, variable, inside: translatedWrapper) +// printer.println() +// } + } + } + + private func printInterfaceWrapperFunctionImpl( + _ printer: inout CodePrinter, + _ function: JavaInterfaceSwiftWrapper.Function, + inside wrapper: JavaInterfaceSwiftWrapper + ) { + printer.printBraceBlock(function.swiftDecl.signatureString) { printer in + let upcallArguments = zip( + function.originalFunctionSignature.parameters, + function.parameterConversions + ).map { param, conversion in + // Wrap-java does not extract parameter names, so no labels + conversion.render(&printer, param.parameterName!) + } + + let javaUpcall = "\(wrapper.javaInterfaceVariableName).\(function.swiftFunctionName)(\(upcallArguments.joined(separator: ", ")))" + + let resultType = function.originalFunctionSignature.result.type + let result = function.resultConversion.render(&printer, javaUpcall) + if resultType.isVoid { + printer.print(result) + } else { + printer.print("return \(result)") + } + } + } + + private func printerInterfaceWrapperVariable( + _ printer: inout CodePrinter, + _ variable: JavaInterfaceSwiftWrapper.Variable, + inside wrapper: JavaInterfaceSwiftWrapper + ) { + // FIXME: Add support for variables. This won't get printed yet + // so we no need to worry about fatalErrors. + printer.printBraceBlock(variable.swiftDecl.signatureString) { printer in + printer.printBraceBlock("get") { printer in + printer.print("fatalError()") + } + + if let setter = variable.setter { + printer.printBraceBlock("set") { printer in + printer.print("fatalError()") + } + } + } + } + private func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws { printHeader(&printer) @@ -154,7 +223,7 @@ extension JNISwift2JavaGenerator { case .actor, .class, .enum, .struct: printConcreteTypeThunks(&printer, type) case .protocol: - printProtocolThunks(&printer, type) + try printProtocolThunks(&printer, type) } } @@ -184,13 +253,18 @@ extension JNISwift2JavaGenerator { printer.println() } + printTypeMetadataAddressThunk(&printer, type) printer.println() printDestroyFunctionThunk(&printer, type) } - private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) { - let protocolName = type.swiftNominal.name + private func printProtocolThunks(_ printer: inout CodePrinter, _ type: ImportedNominalType) throws { + guard let protocolWrapper = self.interfaceProtocolWrappers[type] else { + return + } + + try printSwiftInterfaceWrapper(&printer, protocolWrapper) } @@ -232,11 +306,19 @@ extension JNISwift2JavaGenerator { } private func renderEnumCaseCacheInit(_ enumCase: TranslatedEnumCase) -> String { - let nativeParametersClassName = "\(javaPackagePath)/\(enumCase.enumName)$\(enumCase.name)$$NativeParameters" + let nativeParametersClassName = "\(enumCase.enumName)$\(enumCase.name)$_NativeParameters" let methodSignature = MethodSignature(resultType: .void, parameterTypes: enumCase.parameterConversions.map(\.native.javaType)) - let methods = #"[.init(name: "", signature: "\#(methodSignature.mangledName)")]"# - return #"_JNIMethodIDCache(className: "\#(nativeParametersClassName)", methods: \#(methods))"# + return renderJNICacheInit(className: nativeParametersClassName, methods: [("", methodSignature)]) + } + + private func renderJNICacheInit(className: String, methods: [(String, MethodSignature)]) -> String { + let fullClassName = "\(javaPackagePath)/\(className)" + let methods = methods.map { name, signature in + #".init(name: "\#(name)", signature: "\#(signature.mangledName)")"# + }.joined(separator: ",\n") + + return #"_JNIMethodIDCache(className: "\#(fullClassName)", methods: [\#(methods)])"# } private func printEnumGetAsCaseThunk( @@ -287,6 +369,8 @@ extension JNISwift2JavaGenerator { return } + printSwiftFunctionHelperClasses(&printer, decl) + printCDecl( &printer, translatedDecl @@ -295,6 +379,96 @@ extension JNISwift2JavaGenerator { } } + + private func printSwiftFunctionHelperClasses( + _ printer: inout CodePrinter, + _ decl: ImportedFunc + ) { + let protocolParameters = decl.functionSignature.parameters.compactMap { parameter in + if let concreteType = parameter.type.typeIn( + genericParameters: decl.functionSignature.genericParameters, + genericRequirements: decl.functionSignature.genericRequirements + ) { + return (parameter, concreteType) + } + + switch parameter.type { + case .opaque(let protocolType), + .existential(let protocolType): + return (parameter, protocolType) + + default: + return nil + } + }.map { parameter, protocolType in + // We flatten any composite types + switch protocolType { + case .composite(let protocols): + return (parameter, protocols) + + default: + return (parameter, [protocolType]) + } + } + + // For each parameter that is a generic or a protocol, + // we generate a Swift class that conforms to all of those. + for (parameter, protocolTypes) in protocolParameters { + let protocolWrappers: [JavaInterfaceSwiftWrapper] = protocolTypes.compactMap { protocolType in + guard let importedType = self.asImportedNominalTypeDecl(protocolType), + let wrapper = self.interfaceProtocolWrappers[importedType] + else { + return nil + } + return wrapper + } + + // Make sure we can generate wrappers for all the protocols + // that the parameter requires + guard protocolWrappers.count == protocolTypes.count else { + // We cannot extract a wrapper for this class + // so it must only be passed in by JExtract instances + continue + } + + guard let parameterName = parameter.parameterName else { + // TODO: Throw + fatalError() + } + let swiftClassName = JNISwift2JavaGenerator.protocolParameterWrapperClassName( + methodName: decl.name, + parameterName: parameterName, + parentName: decl.parentType?.asNominalType?.nominalTypeDecl.qualifiedName ?? swiftModuleName + ) + let implementingProtocols = protocolWrappers.map(\.wrapperName).joined(separator: ", ") + + printer.printBraceBlock("final class \(swiftClassName): \(implementingProtocols)") { printer in + let variables: [(String, String)] = protocolWrappers.map { wrapper in + return (wrapper.javaInterfaceVariableName, wrapper.javaInterfaceName) + } + for (name, type) in variables { + printer.print("let \(name): \(type)") + } + printer.println() + let initializerParameters = variables.map { "\($0): \($1)" }.joined(separator: ", ") + + printer.printBraceBlock("init(\(initializerParameters))") { printer in + for (name, _) in variables { + printer.print("self.\(name) = \(name)") + } + } + } + } + } + + private func asImportedNominalTypeDecl(_ type: SwiftType) -> ImportedNominalType? { + self.analysis.importedTypes.first(where: ( { name, nominalType in + nominalType.swiftType == type + })).map { + $0.value + } + } + private func printFunctionDowncall( _ printer: inout CodePrinter, _ decl: ImportedFunc @@ -562,4 +736,44 @@ extension JNISwift2JavaGenerator { ) return newSelfParamName } + + static func protocolParameterWrapperClassName( + methodName: String, + parameterName: String, + parentName: String? + ) -> String { + let parent = if let parentName { + "\(parentName)_" + } else { + "" + } + return "_\(parent)\(methodName)_\(parameterName)_Wrapper" + } +} + +extension SwiftNominalTypeDeclaration { + private var safeProtocolName: String { + self.qualifiedName.replacingOccurrences(of: ".", with: "_") + } + + /// The name of the corresponding `@JavaInterface` of this type. + var javaInterfaceName: String { + "Java\(safeProtocolName)" + } + + var javaInterfaceSwiftProtocolWrapperName: String { + "SwiftJava\(safeProtocolName)Wrapper" + } + + var javaInterfaceVariableName: String { + "_\(javaInterfaceName.firstCharacterLowercased)Interface" + } + + var generatedJavaClassMacroName: String { + if let parent { + return "\(parent.generatedJavaClassMacroName).Java\(self.name)" + } + + return "Java\(self.name)" + } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index 3b84cfb97..692b66b69 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -41,6 +41,7 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { /// Cached Java translation result. 'nil' indicates failed translation. var translatedDecls: [ImportedFunc: TranslatedFunctionDecl] = [:] var translatedEnumCases: [ImportedEnumCase: TranslatedEnumCase] = [:] + var interfaceProtocolWrappers: [ImportedNominalType: JavaInterfaceSwiftWrapper] = [:] /// Because we need to write empty files for SwiftPM, keep track which files we didn't write yet, /// and write an empty file for those. @@ -78,6 +79,13 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { } else { self.expectedOutputSwiftFiles = [] } + + if translator.config.enableJavaCallbacks ?? false { + // We translate all the protocol wrappers + // as we need them to know what protocols we can allow the user to implement themselves + // in Java. + self.interfaceProtocolWrappers = self.generateInterfaceWrappers(Array(self.analysis.importedTypes.values)) + } } func generate() throws { diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift index 511bf8de6..e1aabd7fd 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+JDK.swift @@ -40,6 +40,12 @@ extension JavaType { .class(package: "java.lang", name: "Throwable") } + /// The description of the type java.lang.Object. + static var javaLangObject: JavaType { + .class(package: "java.lang", name: "Object") + } + + /// The description of the type java.util.concurrent.CompletableFuture static func completableFuture(_ T: JavaType) -> JavaType { .class(package: "java.util.concurrent", name: "CompletableFuture", typeParameters: [T.boxedType]) diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift index 4a0cb9e8a..363663217 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift @@ -65,4 +65,18 @@ enum SwiftKnownTypeDeclKind: String, Hashable { return false } } + + /// Indicates whether this known type is translated by `wrap-java` + /// into the same type as `jextract`. + /// + /// This means we do not have to perform any mapping when passing + /// this type between jextract and wrap-java + var isDirectlyTranslatedToWrapJava: Bool { + switch self { + case .bool, .int, .uint, .int8, .uint8, .int16, .uint16, .int32, .uint32, .int64, .uint64, .float, .double, .string, .void: + return true + default: + return false + } + } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index b2f8d6eac..aeb88bfba 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -210,9 +210,13 @@ extension SwiftNominalType: CustomStringConvertible { extension SwiftNominalType { // TODO: Better way to detect Java wrapped classes. - var isJavaKitWrapper: Bool { + var isSwiftJavaWrapper: Bool { nominalTypeDecl.name.hasPrefix("Java") } + + var isProtocol: Bool { + nominalTypeDecl.kind == .protocol + } } extension SwiftType { diff --git a/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift b/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift index 8f57ffa51..4042ec766 100644 --- a/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift +++ b/Sources/JavaStdlib/JavaLangReflect/Constructor+Utilities.swift @@ -13,11 +13,6 @@ //===----------------------------------------------------------------------===// extension Constructor { - /// Whether this is a 'public' constructor. - public var isPublic: Bool { - return (getModifiers() & 1) != 0 - } - /// Whether this is a 'native' constructor. public var isNative: Bool { return (getModifiers() & 256) != 0 diff --git a/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift b/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift new file mode 100644 index 000000000..fc10edfea --- /dev/null +++ b/Sources/JavaStdlib/JavaLangReflect/HasJavaModifiers.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +public protocol HasJavaModifiers { + func getModifiers() -> Int32 +} + +extension HasJavaModifiers { + /// Whether the modifiers contain 'public'. + public var isPublic: Bool { + return (getModifiers() & 0x00000001) != 0 + } + + /// Whether the modifiers contain 'private'. + public var isPrivate: Bool { + return (getModifiers() & 0x00000002) != 0 + } + + /// Whether the modifiers contain 'protected'. + public var isProtected: Bool { + return (getModifiers() & 0x00000004) != 0 + } + + /// Whether the modifiers is equivelant to 'package'.. + /// + /// The "default" access level in Java is 'package', it is signified by lack of a different access modifier. + public var isPackage: Bool { + return !isPublic && !isPrivate && !isProtected + } +} + +extension Constructor: HasJavaModifiers {} +extension JavaClass: HasJavaModifiers {} +extension Method: HasJavaModifiers {} diff --git a/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift b/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift index ecc11b507..00ca3d6bf 100644 --- a/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift +++ b/Sources/JavaStdlib/JavaLangReflect/Method+Utilities.swift @@ -13,28 +13,6 @@ //===----------------------------------------------------------------------===// extension Method { - - /// Whether this is a 'public' method. - public var isPublic: Bool { - return (getModifiers() & 0x00000001) != 0 - } - - /// Whether this is a 'private' method. - public var isPrivate: Bool { - return (getModifiers() & 0x00000002) != 0 - } - - /// Whether this is a 'protected' method. - public var isProtected: Bool { - return (getModifiers() & 0x00000004) != 0 - } - - /// Whether this is a 'package' method. - /// - /// The "default" access level in Java is 'package', it is signified by lack of a different access modifier. - public var isPackage: Bool { - return !isPublic && !isPrivate && !isProtected - } /// Whether this is a 'static' method. public var isStatic: Bool { diff --git a/Sources/SwiftJava/JVM/JavaVirtualMachine.swift b/Sources/SwiftJava/JVM/JavaVirtualMachine.swift index 1c7936a34..bb574c8ad 100644 --- a/Sources/SwiftJava/JVM/JavaVirtualMachine.swift +++ b/Sources/SwiftJava/JVM/JavaVirtualMachine.swift @@ -350,4 +350,4 @@ extension JavaVirtualMachine { enum JavaKitError: Error { case classpathEntryNotFound(entry: String, classpath: [String]) } -} \ No newline at end of file +} diff --git a/Sources/SwiftJava/Macros.swift b/Sources/SwiftJava/Macros.swift index eb9c43745..4c0353661 100644 --- a/Sources/SwiftJava/Macros.swift +++ b/Sources/SwiftJava/Macros.swift @@ -141,6 +141,7 @@ public macro JavaStaticField(_ javaFieldName: String? = nil, isFinal: Bool = fal /// returning method signature, and then, convert the result to the expected `T` type on the Swift side. @attached(body) public macro JavaMethod( + _ javaMethodName: String? = nil, typeErasedResult: String? = nil ) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") @@ -154,9 +155,7 @@ public macro JavaMethod( /// func sayHelloBack(_ i: Int32) -> Double /// ``` @attached(body) -public macro JavaStaticMethod( - typeErasedResult: String? = nil -) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") +public macro JavaStaticMethod(_ javaMethodName: String? = nil) = #externalMacro(module: "SwiftJavaMacros", type: "JavaMethodMacro") /// Macro that marks extensions to specify that all of the @JavaMethod /// methods are implementations of Java methods spelled as `native`. diff --git a/Sources/SwiftJava/String+Extensions.swift b/Sources/SwiftJava/String+Extensions.swift index 0af0de107..b87e6a0c6 100644 --- a/Sources/SwiftJava/String+Extensions.swift +++ b/Sources/SwiftJava/String+Extensions.swift @@ -27,10 +27,14 @@ extension String { } extension String { - /// Replace all of the $'s for nested names with "." to turn a Java class - /// name into a Java canonical class name, + /// Convert a Java class name to its canonical name. + /// Replaces `$` with `.` for nested classes but preserves `$` at the start of identifiers. package var javaClassNameToCanonicalName: String { - return replacing("$", with: ".") + self.replacingOccurrences( + of: #"(?<=\w)\$"#, + with: ".", + options: .regularExpression + ) } /// Whether this is the name of an anonymous class. diff --git a/Sources/SwiftJavaConfigurationShared/Configuration.swift b/Sources/SwiftJavaConfigurationShared/Configuration.swift index 661cf9633..37d1e1e79 100644 --- a/Sources/SwiftJavaConfigurationShared/Configuration.swift +++ b/Sources/SwiftJavaConfigurationShared/Configuration.swift @@ -65,6 +65,10 @@ public struct Configuration: Codable { asyncFuncMode ?? .default } + public var enableJavaCallbacks: Bool? // FIXME: default it to false, but that plays not nice with Codable + + public var generatedJavaSourcesListFileOutput: String? + // ==== wrap-java --------------------------------------------------------- /// The Java class path that should be passed along to the swift-java tool. @@ -91,6 +95,8 @@ public struct Configuration: Codable { /// Exclude input Java types by their package prefix or exact match. public var filterExclude: [String]? + public var singleSwiftFileOutput: String? + // ==== dependencies --------------------------------------------------------- // Java dependencies we need to fetch for this target. @@ -162,7 +168,11 @@ public func readConfiguration(sourceDir: String, file: String = #fileID, line: U /// Configuration is expected to be "JSON-with-comments". /// Specifically "//" comments are allowed and will be trimmed before passing the rest of the config into a standard JSON parser. public func readConfiguration(configPath: URL, file: String = #fileID, line: UInt = #line) throws -> Configuration? { - guard let configData = try? Data(contentsOf: configPath) else { + let configData: Data + do { + configData = try Data(contentsOf: configPath) + } catch { + print("Failed to read SwiftJava configuration at '\(configPath.absoluteURL)', error: \(error)") return nil } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md index 460f396d5..4383727d9 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/index.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/index.md @@ -20,7 +20,7 @@ Reasons why you might want to reach for Swift and Java interoperability include, - Reuse existing libraries which exist in one ecosystem, but don't have a direct equivalent in the other SwiftJava is offering several core libraries which support language interoperability: -- `JavaKit` (Swift -> Java) - JNI-based support library and Swift macros +- `SwiftJava` (Swift -> Java) - JNI-based support library and Swift macros - `SwiftKit` (Java -> Swift) - Support library for Java calling Swift code (either using JNI or FFM) - `swift-java` - command line tool; Supports source generation and also dependency management operations - Build tool integration - SwiftPM Plugin diff --git a/Sources/SwiftJavaMacros/JavaMethodMacro.swift b/Sources/SwiftJavaMacros/JavaMethodMacro.swift index f992ee760..099484528 100644 --- a/Sources/SwiftJavaMacros/JavaMethodMacro.swift +++ b/Sources/SwiftJavaMacros/JavaMethodMacro.swift @@ -50,15 +50,27 @@ extension JavaMethodMacro: BodyMacro { fatalError("not a function: \(declaration)") } + let funcName = + if case .argumentList(let arguments) = node.arguments, + let argument = arguments.first, + argument.label?.text != "typeErasedResult", + let stringLiteral = argument.expression.as(StringLiteralExprSyntax.self), + stringLiteral.segments.count == 1, + case let .stringSegment(funcNameSegment)? = stringLiteral.segments.first + { + funcNameSegment.content.text + } else { + funcDecl.name.text + } + let isStatic = node.attributeName.trimmedDescription == "JavaStaticMethod" - let funcName = funcDecl.name.text let params = funcDecl.signature.parameterClause.parameters let paramNames = params.map { param in param.parameterName?.text ?? "" }.joined(separator: ", ") let genericResultType: String? = if case let .argumentList(arguments) = node.arguments, - let firstElement = arguments.first, - let stringLiteral = firstElement.expression + let element = arguments.first(where: { $0.label?.text == "typeErasedResult" }), + let stringLiteral = element.expression .as(StringLiteralExprSyntax.self), stringLiteral.segments.count == 1, case let .stringSegment(wrapperName)? = stringLiteral.segments.first { diff --git a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift index 16a9c899e..7be79a9fa 100644 --- a/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift +++ b/Sources/SwiftJavaRuntimeSupport/JNIMethodIDCaches.swift @@ -91,4 +91,33 @@ extension _JNIMethodIDCache { cache.methods[messageConstructor]! } } + + public enum JNISwiftInstance { + private static let memoryAddressMethod = Method( + name: "$memoryAddress", + signature: "()J" + ) + + private static let typeMetadataAddressMethod = Method( + name: "$typeMetadataAddress", + signature: "()J" + ) + + private static let cache = _JNIMethodIDCache( + className: "org/swift/swiftkit/core/JNISwiftInstance", + methods: [memoryAddressMethod, typeMetadataAddressMethod] + ) + + public static var `class`: jclass { + cache.javaClass + } + + public static var memoryAddress: jmethodID { + cache.methods[memoryAddressMethod]! + } + + public static var typeMetadataAddress: jmethodID { + cache.methods[typeMetadataAddressMethod]! + } + } } diff --git a/Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift b/Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift new file mode 100644 index 000000000..4040d0bcb --- /dev/null +++ b/Sources/SwiftJavaRuntimeSupport/generated/JavaJNISwiftInstance.swift @@ -0,0 +1,21 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftJava + +@JavaInterface("org.swift.swiftkit.core.JNISwiftInstance") +public struct JavaJNISwiftInstance { + @JavaMethod("$memoryAddress") + public func memoryAddress() -> Int64 +} diff --git a/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift b/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift index 455cb962d..05bf3b8f3 100644 --- a/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift +++ b/Sources/SwiftJavaTool/Commands/ConfigureCommand.swift @@ -59,8 +59,8 @@ extension SwiftJava { swiftModule } - @Argument(help: "The input file, which is either a swift-java configuration file or (if '-jar' was specified) a Jar file.") - var input: String? + @Option(help: "A prefix that will be added to the names of the Swift types") + var swiftTypePrefix: String? } } @@ -137,14 +137,19 @@ extension SwiftJava.ConfigureCommand { print("[debug][swift-java] Importing classpath entry: \(entry)") if entry.hasSuffix(".jar") { + print("[debug][swift-java] Importing classpath as JAR file: \(entry)") let jarFile = try JarFile(entry, false, environment: environment) try addJavaToSwiftMappings( to: &config, forJar: jarFile, environment: environment ) - } else if FileManager.default.fileExists(atPath: entry) { - log.warning("Currently unable handle directory classpath entries for config generation! Skipping: \(entry)") + } else if FileManager.default.fileExists(atPath: entry), let entryURL = URL(string: entry) { + print("[debug][swift-java] Importing classpath as directory: \(entryURL)") + try addJavaToSwiftMappings( + to: &config, + forDirectory: entryURL + ) } else { log.warning("Classpath entry does not exist, skipping: \(entry)") } @@ -162,62 +167,82 @@ extension SwiftJava.ConfigureCommand { ) } + mutating func addJavaToSwiftMappings( + to configuration: inout Configuration, + forDirectory url: Foundation.URL + ) throws { + let enumerator = FileManager.default.enumerator(atPath: url.path()) + + while let filePath = enumerator?.nextObject() as? String { + try addJavaToSwiftMappings(to: &configuration, fileName: filePath) + } + } + mutating func addJavaToSwiftMappings( to configuration: inout Configuration, forJar jarFile: JarFile, environment: JNIEnvironment ) throws { - let log = Self.log - for entry in jarFile.entries()! { - // We only look at class files in the Jar file. - guard entry.getName().hasSuffix(".class") else { - continue - } + try addJavaToSwiftMappings(to: &configuration, fileName: entry.getName()) + } + } - // Skip some "common" files we know that would be duplicated in every jar - guard !entry.getName().hasPrefix("META-INF") else { - continue - } - guard !entry.getName().hasSuffix("package-info") else { - continue - } - guard !entry.getName().hasSuffix("package-info.class") else { - continue - } + mutating func addJavaToSwiftMappings( + to configuration: inout Configuration, + fileName: String + ) throws { + // We only look at class files + guard fileName.hasSuffix(".class") else { + return + } - // If this is a local class, it cannot be mapped into Swift. - if entry.getName().isLocalJavaClass { - continue - } + // Skip some "common" files we know that would be duplicated in every jar + guard !fileName.hasPrefix("META-INF") else { + return + } + guard !fileName.hasSuffix("package-info") else { + return + } + guard !fileName.hasSuffix("package-info.class") else { + return + } - let javaCanonicalName = String(entry.getName().replacing("/", with: ".") - .dropLast(".class".count)) + // If this is a local class, it cannot be mapped into Swift. + if fileName.isLocalJavaClass { + return + } - guard SwiftJava.shouldImport(javaCanonicalName: javaCanonicalName, commonOptions: self.commonOptions) else { - log.info("Skip importing class: \(javaCanonicalName) due to include/exclude filters") - continue - } + let javaCanonicalName = String(fileName.replacing("/", with: ".") + .dropLast(".class".count)) - if configuration.classes?[javaCanonicalName] != nil { - // We never overwrite an existing class mapping configuration. - // E.g. the user may have configured a custom name for a type. - continue - } + guard SwiftJava.shouldImport(javaCanonicalName: javaCanonicalName, commonOptions: self.commonOptions) else { + log.info("Skip importing class: \(javaCanonicalName) due to include/exclude filters") + return + } - if configuration.classes == nil { - configuration.classes = [:] - } + if configuration.classes?[javaCanonicalName] != nil { + // We never overwrite an existing class mapping configuration. + // E.g. the user may have configured a custom name for a type. + return + } - if let configuredSwiftName = configuration.classes![javaCanonicalName] { - log.info("Java type '\(javaCanonicalName)' already configured as '\(configuredSwiftName)' Swift type.") - } else { - log.info("Configure Java type '\(javaCanonicalName)' as '\(javaCanonicalName.defaultSwiftNameForJavaClass.bold)' Swift type.") - } + if configuration.classes == nil { + configuration.classes = [:] + } - configuration.classes![javaCanonicalName] = - javaCanonicalName.defaultSwiftNameForJavaClass + var swiftName = javaCanonicalName.defaultSwiftNameForJavaClass + if let swiftTypePrefix { + swiftName = "\(swiftTypePrefix)\(swiftName)" } + + if let configuredSwiftName = configuration.classes![javaCanonicalName] { + log.info("Java type '\(javaCanonicalName)' already configured as '\(configuredSwiftName)' Swift type.") + } else { + log.info("Configure Java type '\(javaCanonicalName)' as '\(swiftName.bold)' Swift type.") + } + + configuration.classes![javaCanonicalName] = swiftName } } diff --git a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift index b5c3a7bb9..775ab6040 100644 --- a/Sources/SwiftJavaTool/Commands/JExtractCommand.swift +++ b/Sources/SwiftJavaTool/Commands/JExtractCommand.swift @@ -81,6 +81,12 @@ extension SwiftJava { @Option(help: "The mode to use for extracting asynchronous Swift functions. By default async methods are extracted as Java functions returning CompletableFuture.") var asyncFuncMode: JExtractAsyncFuncMode? + + @Flag(help: "By enabling this mode, JExtract will generate Java code that allows you to implement Swift protocols using Java classes. This feature requires disabling the sandbox mode in SwiftPM. This only works in the 'jni' mode.") + var enableJavaCallbacks: Bool = false + + @Option(help: "If specified, JExtract will output to this file a list of paths to all generated Java source files") + var generatedJavaSourcesListFileOutput: String? } } @@ -96,10 +102,14 @@ extension SwiftJava.JExtractCommand { let writeEmptyFiles = CommandLine.arguments.contains("--write-empty-files") ? true : nil configure(&config.writeEmptyFiles, overrideWith: writeEmptyFiles) + let enableJavaCallbacks = CommandLine.arguments.contains("--enable-java-callbacks") ? true : nil + configure(&config.enableJavaCallbacks, overrideWith: enableJavaCallbacks) + configure(&config.unsignedNumbersMode, overrideWith: self.unsignedNumbersMode) configure(&config.minimumInputAccessLevelMode, overrideWith: self.minimumInputAccessLevelMode) configure(&config.memoryManagementMode, overrideWith: self.memoryManagementMode) configure(&config.asyncFuncMode, overrideWith: self.asyncFuncMode) + configure(&config.generatedJavaSourcesListFileOutput, overrideWith: self.generatedJavaSourcesListFileOutput) try checkModeCompatibility(config: config) @@ -126,11 +136,15 @@ extension SwiftJava.JExtractCommand { case .annotate: () // OK case .wrapGuava: - throw IllegalModeCombinationError("JNI mode does not support '\(JExtractUnsignedIntegerMode.wrapGuava)' Unsigned integer mode! \(Self.helpMessage)") + throw IllegalModeCombinationError("JNI mode does not support '\(JExtractUnsignedIntegerMode.wrapGuava)' Unsigned integer mode! \(Self.helpMessage())") } } else if config.effectiveMode == .ffm { guard config.effectiveMemoryManagementMode == .explicit else { - throw IllegalModeCombinationError("FFM mode does not support '\(self.memoryManagementMode)' memory management mode! \(Self.helpMessage)") + throw IllegalModeCombinationError("FFM mode does not support '\(self.memoryManagementMode ?? .default)' memory management mode! \(Self.helpMessage())") + } + + if let enableJavaCallbacks = config.enableJavaCallbacks, enableJavaCallbacks { + throw IllegalModeCombinationError("FFM mode does not support enabling Java callbacks! \(Self.helpMessage())") } } } diff --git a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift index 51315102a..3e4e482b3 100644 --- a/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift +++ b/Sources/SwiftJavaTool/Commands/WrapJavaCommand.swift @@ -58,8 +58,8 @@ extension SwiftJava { @Option(help: "Match java package directory structure with generated Swift files") var swiftMatchPackageDirectoryStructure: Bool = false - @Argument(help: "Path to .jar file whose Java classes should be wrapped using Swift bindings") - var input: String + @Option(help: "If specified, a single Swift file will be generated containing all the generated code") + var singleSwiftFileOutput: String? } } @@ -69,6 +69,7 @@ extension SwiftJava.WrapJavaCommand { print("self.commonOptions.filterInclude = \(self.commonOptions.filterInclude)") configure(&config.filterInclude, append: self.commonOptions.filterInclude) configure(&config.filterExclude, append: self.commonOptions.filterExclude) + configure(&config.singleSwiftFileOutput, overrideWith: self.singleSwiftFileOutput) // Get base classpath configuration for this target and configuration var classpathSearchDirs = [self.effectiveSwiftModuleURL] @@ -78,7 +79,6 @@ extension SwiftJava.WrapJavaCommand { } else { print("[trace][swift-java] Cache directory: none") } - print("[trace][swift-java] INPUT: \(input)") var classpathEntries = self.configureCommandJVMClasspath( searchDirs: classpathSearchDirs, config: config, log: Self.log) @@ -151,21 +151,41 @@ extension SwiftJava.WrapJavaCommand { .getSystemClassLoader()! var javaClasses: [JavaClass] = [] for (javaClassName, _) in config.classes ?? [:] { + func remove() { + translator.translatedClasses.removeValue(forKey: javaClassName) + } + guard shouldImportJavaClass(javaClassName, config: config) else { + remove() continue } - log.info("Wrapping java type: \(javaClassName)") - guard let javaClass = try classLoader.loadClass(javaClassName) else { log.warning("Could not load Java class '\(javaClassName)', skipping.") + remove() + continue + } + + guard self.shouldExtract(javaClass: javaClass, config: config) else { + log.info("Skip Java type: \(javaClassName) (does not match minimum access level)") + remove() + continue + } + + guard !javaClass.isEnum() else { + log.info("Skip Java type: \(javaClassName) (enums do not currently work)") + remove() continue } + log.info("Wrapping java type: \(javaClassName)") + // Add this class to the list of classes we'll translate. javaClasses.append(javaClass) } + log.info("OK now we go to nested classes") + // Find all of the nested classes for each class, adding them to the list // of classes to be translated if they were already specified. var allClassesToVisit = javaClasses @@ -212,6 +232,16 @@ extension SwiftJava.WrapJavaCommand { return nil } + guard self.shouldExtract(javaClass: nestedClass, config: config) else { + log.info("Skip Java type: \(javaClassName) (does not match minimum access level)") + return nil + } + + guard !nestedClass.isEnum() else { + log.info("Skip Java type: \(javaClassName) (enums do not currently work)") + return nil + } + // Record this as a translated class. let swiftUnqualifiedName = javaClassName.javaClassNameToCanonicalName .defaultSwiftNameForJavaClass @@ -237,9 +267,13 @@ extension SwiftJava.WrapJavaCommand { try translator.validateClassConfiguration() // Translate all of the Java classes into Swift classes. - for javaClass in javaClasses { + + if let singleSwiftFileOutput = config.singleSwiftFileOutput { translator.startNewFile() - let swiftClassDecls = try translator.translateClass(javaClass) + + let swiftClassDecls = try javaClasses.flatMap { + try translator.translateClass($0) + } let importDecls = translator.getImportDecls() let swiftFileText = """ @@ -249,19 +283,53 @@ extension SwiftJava.WrapJavaCommand { """ - var generatedFileOutputDir = self.actualOutputDirectory - if self.swiftMatchPackageDirectoryStructure { - generatedFileOutputDir?.append(path: javaClass.getPackageName().replacing(".", with: "/")) - } - - let swiftFileName = try! translator.getSwiftTypeName(javaClass, preferValueTypes: false) - .swiftName.replacing(".", with: "+") + ".swift" try writeContents( swiftFileText, - outputDirectory: generatedFileOutputDir, - to: swiftFileName, - description: "Java class '\(javaClass.getName())' translation" + outputDirectory: self.actualOutputDirectory, + to: singleSwiftFileOutput, + description: "Java class translation" ) + } else { + for javaClass in javaClasses { + translator.startNewFile() + + let swiftClassDecls = try translator.translateClass(javaClass) + let importDecls = translator.getImportDecls() + + let swiftFileText = """ + // Auto-generated by Java-to-Swift wrapper generator. + \(importDecls.map { $0.description }.joined()) + \(swiftClassDecls.map { $0.description }.joined(separator: "\n")) + + """ + + var generatedFileOutputDir = self.actualOutputDirectory + if self.swiftMatchPackageDirectoryStructure { + generatedFileOutputDir?.append(path: javaClass.getPackageName().replacing(".", with: "/")) + } + + let swiftFileName = try translator.getSwiftTypeName(javaClass, preferValueTypes: false) + .swiftName.replacing(".", with: "+") + ".swift" + try writeContents( + swiftFileText, + outputDirectory: generatedFileOutputDir, + to: swiftFileName, + description: "Java class '\(javaClass.getName())' translation" + ) + } + } + } + + /// Determines whether a method should be extracted for translation. + /// Only look at public and protected methods here. + private func shouldExtract(javaClass: JavaClass, config: Configuration) -> Bool { + switch config.effectiveMinimumInputAccessLevelMode { + case .internal: + return javaClass.isPublic || javaClass.isProtected || javaClass.isPackage + case .package: + return javaClass.isPublic || javaClass.isProtected || javaClass.isPackage + case .public: + return javaClass.isPublic || javaClass.isProtected } } diff --git a/Sources/SwiftJavaTool/CommonOptions.swift b/Sources/SwiftJavaTool/CommonOptions.swift index c313202bc..4627381e0 100644 --- a/Sources/SwiftJavaTool/CommonOptions.swift +++ b/Sources/SwiftJavaTool/CommonOptions.swift @@ -67,6 +67,9 @@ extension SwiftJava { @Option(name: .long, help: "While scanning a classpath, skip types which match the filter prefix") var filterExclude: [String] = [] + + @Option(help: "A path to a custom swift-java.config to use") + var config: String? = nil } struct CommonJVMOptions: ParsableArguments { @@ -145,4 +148,4 @@ extension HasCommonJVMOptions { func makeJVM(classpathEntries: [String]) throws -> JavaVirtualMachine { try JavaVirtualMachine.shared(classpath: classpathEntries) } -} \ No newline at end of file +} diff --git a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift index 9763b4b38..92818e43a 100644 --- a/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift +++ b/Sources/SwiftJavaTool/SwiftJavaBaseAsyncParsableCommand.swift @@ -163,14 +163,24 @@ extension SwiftJavaBaseAsyncParsableCommand { func readInitialConfiguration(command: some SwiftJavaBaseAsyncParsableCommand) throws -> Configuration { var earlyConfig: Configuration? - if let moduleBaseDir { + if let configPath = commonOptions.config { + let configURL = URL(filePath: configPath, directoryHint: .notDirectory) + print("[debug][swift-java] Load config from passed in path: \(configURL)") + earlyConfig = try readConfiguration(configPath: configURL) + } else if let moduleBaseDir { print("[debug][swift-java] Load config from module base directory: \(moduleBaseDir.path)") earlyConfig = try readConfiguration(sourceDir: moduleBaseDir.path) } else if let inputSwift = commonOptions.inputSwift { print("[debug][swift-java] Load config from module swift input directory: \(inputSwift)") earlyConfig = try readConfiguration(sourceDir: inputSwift) } - var config = earlyConfig ?? Configuration() + var config: Configuration + if let earlyConfig { + config = earlyConfig + } else { + log.warning("[swift-java] Failed to load initial configuration. Proceeding with empty configuration.") + config = Configuration() + } // override configuration with options from command line config.logLevel = command.logLevel return config diff --git a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift index 0f7aa45d6..ce780f904 100644 --- a/Sources/SwiftJavaToolLib/JavaClassTranslator.swift +++ b/Sources/SwiftJavaToolLib/JavaClassTranslator.swift @@ -228,6 +228,11 @@ struct JavaClassTranslator { continue } + guard method.getName().isValidSwiftFunctionName else { + log.warning("Skipping method \(method.getName()) because it is not a valid Swift function name") + continue + } + addMethod(method, isNative: false) } @@ -604,6 +609,15 @@ extension JavaClassTranslator { } } + // --- Parameter types + for parameter in method.getParameters() { + if let parameterizedType = parameter?.getParameterizedType() { + if parameterizedType.isEqualTo(typeParam.as(Type.self)) { + return true + } + } + } + return false } diff --git a/Sources/SwiftJavaToolLib/StringExtras.swift b/Sources/SwiftJavaToolLib/StringExtras.swift index e69f379c3..c3ac5390f 100644 --- a/Sources/SwiftJavaToolLib/StringExtras.swift +++ b/Sources/SwiftJavaToolLib/StringExtras.swift @@ -35,6 +35,11 @@ extension String { return "`\(self)`" } + /// Returns whether this is a valid Swift function name + var isValidSwiftFunctionName: Bool { + !self.starts(with: "$") + } + /// Replace all occurrences of one character in the string with another. public func replacing(_ character: Character, with replacement: Character) -> String { return replacingOccurrences(of: String(character), with: String(replacement)) diff --git a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift index 38ef87895..2188bcd62 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIEnumTests.swift @@ -170,17 +170,17 @@ struct JNIEnumTests { """, """ public record First() implements Case { - record $NativeParameters() {} + record _NativeParameters() {} } """, """ public record Second(java.lang.String arg0) implements Case { - record $NativeParameters(java.lang.String arg0) {} + record _NativeParameters(java.lang.String arg0) {} } """, """ public record Third(long x, int y) implements Case { - record $NativeParameters(long x, int y) {} + record _NativeParameters(long x, int y) {} } """ ]) @@ -268,7 +268,7 @@ struct JNIEnumTests { if (getDiscriminator() != Discriminator.SECOND) { return Optional.empty(); } - Second.$NativeParameters $nativeParameters = MyEnum.$getAsSecond(this.$memoryAddress()); + Second._NativeParameters $nativeParameters = MyEnum.$getAsSecond(this.$memoryAddress()); return Optional.of(new Second($nativeParameters.arg0)); } """, @@ -277,7 +277,7 @@ struct JNIEnumTests { if (getDiscriminator() != Discriminator.THIRD) { return Optional.empty(); } - Third.$NativeParameters $nativeParameters = MyEnum.$getAsThird(this.$memoryAddress()); + Third._NativeParameters $nativeParameters = MyEnum.$getAsThird(this.$memoryAddress()); return Optional.of(new Third($nativeParameters.x, $nativeParameters.y)); } """ diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index c5302ca64..231c4d25d 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -12,16 +12,22 @@ // //===----------------------------------------------------------------------===// +import SwiftJavaConfigurationShared import JExtractSwiftLib import Testing @Suite struct JNIProtocolTests { + var config: Configuration { + var config = Configuration() + config.enableJavaCallbacks = true + return config + } + let source = """ public protocol SomeProtocol { - var x: Int64 { get set } - public func method() {} + public func withObject(c: SomeClass) -> SomeClass {} } public protocol B {} @@ -37,6 +43,7 @@ struct JNIProtocolTests { func generatesJavaInterface() throws { try assertOutput( input: source, + config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ @@ -50,18 +57,13 @@ struct JNIProtocolTests { import org.swift.swiftkit.core.util.*; """, """ - public interface SomeProtocol extends JNISwiftInstance { - ... + public interface SomeProtocol { + ... + public void method(); + ... + public SomeClass withObject(SomeClass c); + ... } - """, - """ - public long getX(); - """, - """ - public void setX(long newValue); - """, - """ - public void method(); """ ]) } @@ -70,6 +72,7 @@ struct JNIProtocolTests { func generatesJavaClassWithExtends() throws { try assertOutput( input: source, + config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ @@ -83,16 +86,17 @@ struct JNIProtocolTests { func takeProtocol_java() throws { try assertOutput( input: source, + config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ """ - public static <$T0 extends SomeProtocol, $T1 extends SomeProtocol> void takeProtocol($T0 x, $T1 y) { - SwiftModule.$takeProtocol(x.$memoryAddress(), x.$typeMetadataAddress(), y.$memoryAddress(), y.$typeMetadataAddress()); + public static <_T0 extends SomeProtocol, _T1 extends SomeProtocol> void takeProtocol(_T0 x, _T1 y) { + SwiftModule.$takeProtocol(x, y); } """, """ - private static native void $takeProtocol(long x, long x_typeMetadataAddress, long y, long y_typeMetadataAddress); + private static native void $takeProtocol(java.lang.Object x, java.lang.Object y); """ ]) } @@ -101,43 +105,50 @@ struct JNIProtocolTests { func takeProtocol_swift() throws { try assertOutput( input: source, + config: config, .jni, .swift, detectChunkByInitialLines: 1, expectedChunks: [ """ - @_cdecl("Java_com_example_swift_SwiftModule__00024takeProtocol__JJJJ") - func Java_com_example_swift_SwiftModule__00024takeProtocol__JJJJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, x_typeMetadataAddress: jlong, y: jlong, y_typeMetadataAddress: jlong) { - guard let xTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: x_typeMetadataAddress, in: environment))) else { - fatalError("x_typeMetadataAddress memory address was null") - } - let xDynamicType$: Any.Type = unsafeBitCast(xTypeMetadataPointer$, to: Any.Type.self) - guard let xRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: x, in: environment))) else { - fatalError("x memory address was null") + final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper { + let _javaSomeProtocolInterface: JavaSomeProtocol + init(_javaSomeProtocolInterface: JavaSomeProtocol) { + self._javaSomeProtocolInterface = _javaSomeProtocolInterface } - #if hasFeature(ImplicitOpenExistentials) - let xExistential$ = xRawPointer$.load(as: xDynamicType$) as! any (SomeProtocol) - #else - func xDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { - xRawPointer$.load(as: ty) as! any (SomeProtocol) + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeProtocol__Ljava_lang_Object_2Ljava_lang_Object_2") + func Java_com_example_swift_SwiftModule__00024takeProtocol__Ljava_lang_Object_2Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, x: jobject?, y: jobject?) { + let xswiftObject$: (SomeProtocol) + if environment.interface.IsInstanceOf(environment, x, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + ... + let xpointer$DynamicType$: Any.Type = unsafeBitCast(xpointer$TypeMetadataPointer$, to: Any.Type.self) + guard let xpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: xpointer$, in: environment))) else { + fatalError("xpointer$ memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let xpointer$Existential$ = xpointer$RawPointer$.load(as: xpointer$DynamicType$) as! any (SomeProtocol) + #else + func xpointer$DoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { + xpointer$RawPointer$.load(as: ty) as! any (SomeProtocol) + } + let xpointer$Existential$ = _openExistential(xpointer$DynamicType$, do: xpointer$DoLoad) + #endif + xswiftObject$ = xpointer$Existential$ } - let xExistential$ = _openExistential(xDynamicType$, do: xDoLoad) - #endif - guard let yTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: y_typeMetadataAddress, in: environment))) else { - fatalError("y_typeMetadataAddress memory address was null") + else { + xswiftObject$ = _SwiftModule_takeProtocol_x_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: x!, environment: environment)) } - let yDynamicType$: Any.Type = unsafeBitCast(yTypeMetadataPointer$, to: Any.Type.self) - guard let yRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: y, in: environment))) else { - fatalError("y memory address was null") + let yswiftObject$: (SomeProtocol) + if environment.interface.IsInstanceOf(environment, y, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + ... + yswiftObject$ = ypointer$Existential$ } - #if hasFeature(ImplicitOpenExistentials) - let yExistential$ = yRawPointer$.load(as: yDynamicType$) as! any (SomeProtocol) - #else - func yDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { - yRawPointer$.load(as: ty) as! any (SomeProtocol) + else { + yswiftObject$ = _SwiftModule_takeProtocol_y_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: y!, environment: environment)) } - let yExistential$ = _openExistential(yDynamicType$, do: yDoLoad) - #endif - SwiftModule.takeProtocol(x: xExistential$, y: yExistential$) + SwiftModule.takeProtocol(x: xswiftObject$, y: yswiftObject$) } """ ] @@ -148,16 +159,17 @@ struct JNIProtocolTests { func takeGeneric_java() throws { try assertOutput( input: source, + config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ """ public static void takeGeneric(S s) { - SwiftModule.$takeGeneric(s.$memoryAddress(), s.$typeMetadataAddress()); + SwiftModule.$takeGeneric(s); } """, """ - private static native void $takeGeneric(long s, long s_typeMetadataAddress); + private static native void $takeGeneric(java.lang.Object s); """ ]) } @@ -166,28 +178,30 @@ struct JNIProtocolTests { func takeGeneric_swift() throws { try assertOutput( input: source, + config: config, .jni, .swift, detectChunkByInitialLines: 1, expectedChunks: [ """ - @_cdecl("Java_com_example_swift_SwiftModule__00024takeGeneric__JJ") - func Java_com_example_swift_SwiftModule__00024takeGeneric__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, s: jlong, s_typeMetadataAddress: jlong) { - guard let sTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: s_typeMetadataAddress, in: environment))) else { - fatalError("s_typeMetadataAddress memory address was null") + final class _SwiftModule_takeGeneric_s_Wrapper: SwiftJavaSomeProtocolWrapper { + let _javaSomeProtocolInterface: JavaSomeProtocol + init(_javaSomeProtocolInterface: JavaSomeProtocol) { + self._javaSomeProtocolInterface = _javaSomeProtocolInterface } - let sDynamicType$: Any.Type = unsafeBitCast(sTypeMetadataPointer$, to: Any.Type.self) - guard let sRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: s, in: environment))) else { - fatalError("s memory address was null") + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeGeneric__Ljava_lang_Object_2") + func Java_com_example_swift_SwiftModule__00024takeGeneric__Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, s: jobject?) { + let sswiftObject$: (SomeProtocol) + if environment.interface.IsInstanceOf(environment, s, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + ... + sswiftObject$ = spointer$Existential$ } - #if hasFeature(ImplicitOpenExistentials) - let sExistential$ = sRawPointer$.load(as: sDynamicType$) as! any (SomeProtocol) - #else - func sDoLoad(_ ty: Ty.Type) -> any (SomeProtocol) { - sRawPointer$.load(as: ty) as! any (SomeProtocol) + else { + sswiftObject$ = _SwiftModule_takeGeneric_s_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: s!, environment: environment)) } - let sExistential$ = _openExistential(sDynamicType$, do: sDoLoad) - #endif - SwiftModule.takeGeneric(s: sExistential$) + SwiftModule.takeGeneric(s: sswiftObject$) } """ ] @@ -198,16 +212,17 @@ struct JNIProtocolTests { func takeComposite_java() throws { try assertOutput( input: source, + config: config, .jni, .java, detectChunkByInitialLines: 1, expectedChunks: [ """ - public static <$T0 extends SomeProtocol & B> void takeComposite($T0 x) { - SwiftModule.$takeComposite(x.$memoryAddress(), x.$typeMetadataAddress()); + public static <_T0 extends SomeProtocol & B> void takeComposite(_T0 x) { + SwiftModule.$takeComposite(x); } """, """ - private static native void $takeComposite(long x, long x_typeMetadataAddress); + private static native void $takeComposite(java.lang.Object x); """ ]) } @@ -216,28 +231,92 @@ struct JNIProtocolTests { func takeComposite_swift() throws { try assertOutput( input: source, + config: config, .jni, .swift, detectChunkByInitialLines: 1, expectedChunks: [ """ - @_cdecl("Java_com_example_swift_SwiftModule__00024takeComposite__JJ") - func Java_com_example_swift_SwiftModule__00024takeComposite__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, x: jlong, x_typeMetadataAddress: jlong) { - guard let xTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: x_typeMetadataAddress, in: environment))) else { - fatalError("x_typeMetadataAddress memory address was null") + final class _SwiftModule_takeComposite_x_Wrapper: SwiftJavaSomeProtocolWrapper, SwiftJavaBWrapper { + let _javaSomeProtocolInterface: JavaSomeProtocol + let _javaBInterface: JavaB + init(_javaSomeProtocolInterface: JavaSomeProtocol, _javaBInterface: JavaB) { + self._javaSomeProtocolInterface = _javaSomeProtocolInterface + self._javaBInterface = _javaBInterface } - let xDynamicType$: Any.Type = unsafeBitCast(xTypeMetadataPointer$, to: Any.Type.self) - guard let xRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: x, in: environment))) else { - fatalError("x memory address was null") + } + """, + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024takeComposite__Ljava_lang_Object_2") + func Java_com_example_swift_SwiftModule__00024takeComposite__Ljava_lang_Object_2(environment: UnsafeMutablePointer!, thisClass: jclass, x: jobject?) { + let xswiftObject$: (SomeProtocol & B) + if environment.interface.IsInstanceOf(environment, x, _JNIMethodIDCache.JNISwiftInstance.class) != 0 { + let xpointer$ = environment.interface.CallLongMethodA(environment, x, _JNIMethodIDCache.JNISwiftInstance.memoryAddress, []) + let xtypeMetadata$ = environment.interface.CallLongMethodA(environment, x, _JNIMethodIDCache.JNISwiftInstance.typeMetadataAddress, []) + guard let xpointer$TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: xtypeMetadata$, in: environment))) else { + fatalError("xtypeMetadata$ memory address was null") + } + let xpointer$DynamicType$: Any.Type = unsafeBitCast(xpointer$TypeMetadataPointer$, to: Any.Type.self) + guard let xpointer$RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: xpointer$, in: environment))) else { + fatalError("xpointer$ memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let xpointer$Existential$ = xpointer$RawPointer$.load(as: xpointer$DynamicType$) as! any (SomeProtocol & B) + #else + func xpointer$DoLoad(_ ty: Ty.Type) -> any (SomeProtocol & B) { + xpointer$RawPointer$.load(as: ty) as! any (SomeProtocol & B) + } + let xpointer$Existential$ = _openExistential(xpointer$DynamicType$, do: xpointer$DoLoad) + #endif + xswiftObject$ = xpointer$Existential$ } - #if hasFeature(ImplicitOpenExistentials) - let xExistential$ = xRawPointer$.load(as: xDynamicType$) as! any (SomeProtocol & B) - #else - func xDoLoad(_ ty: Ty.Type) -> any (SomeProtocol & B) { - xRawPointer$.load(as: ty) as! any (SomeProtocol & B) + else { + xswiftObject$ = _SwiftModule_takeComposite_x_Wrapper(_javaSomeProtocolInterface: JavaSomeProtocol(javaThis: x!, environment: environment), _javaBInterface: JavaB(javaThis: x!, environment: environment)) } - let xExistential$ = _openExistential(xDynamicType$, do: xDoLoad) - #endif - SwiftModule.takeComposite(x: xExistential$) + SwiftModule.takeComposite(x: xswiftObject$) + } + """ + ] + ) + } + + @Test + func generatesProtocolWrappers() throws { + try assertOutput( + input: source, + config: config, + .jni, .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + protocol SwiftJavaSomeProtocolWrapper: SomeProtocol { + var _javaSomeProtocolInterface: JavaSomeProtocol { get } + } + """, + """ + extension SwiftJavaSomeProtocolWrapper { + public func method() { + _javaSomeProtocolInterface.method() + } + public func withObject(c: SomeClass) -> SomeClass { + let cClass = try! JavaClass(environment: JavaVirtualMachine.shared().environment()) + let cPointer = UnsafeMutablePointer.allocate(capacity: 1) + cPointer.initialize(to: c) + guard let unwrapped$ = _javaSomeProtocolInterface.withObject(cClass.wrapMemoryAddressUnsafe(Int64(Int(bitPattern: cPointer)))) else { + fatalError("Upcall to withObject unexpectedly returned nil") + } + let result$MemoryAddress$ = unwrapped$.as(JavaJNISwiftInstance.self)!.memoryAddress() + let result$Pointer = UnsafeMutablePointer(bitPattern: Int(result$MemoryAddress$))! + return result$Pointer.pointee + } + } + """, + """ + protocol SwiftJavaBWrapper: B { + var _javaBInterface: JavaB { get } + } + """, + """ + extension SwiftJavaBWrapper { } """ ] diff --git a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift index 2228aad88..7e78434d1 100644 --- a/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift +++ b/Tests/JExtractSwiftTests/MemoryManagementModeTests.swift @@ -99,7 +99,7 @@ struct MemoryManagementModeTests { } """, """ - public MyClass f(SwiftArena swiftArena$); + public MyClass f(); """ ] ) diff --git a/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift index cba7b38f5..ceb05df97 100644 --- a/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift +++ b/Tests/SwiftJavaToolLibTests/WrapJavaTests/GenericsWrapJavaTests.swift @@ -338,6 +338,45 @@ final class GenericsWrapJavaTests: XCTestCase { ) } + func test_wrapJava_genericMethodTypeErasure_customInterface_staticMethods() async throws { + let classpathURL = try await compileJava( + """ + package com.example; + + interface MyInterface {} + + final class Public { + public static void useInterface(T myInterface) { } + } + """) + + try assertWrapJavaOutput( + javaClassNames: [ + "com.example.MyInterface", + "com.example.Public" + ], + classpath: [classpathURL], + expectedChunks: [ + """ + @JavaInterface("com.example.MyInterface") + public struct MyInterface { + """, + """ + @JavaClass("com.example.Public") + open class Public: JavaObject { + """, + """ + extension JavaClass { + """, + """ + @JavaStaticMethod + public func useInterface(_ arg0: T?) + } + """ + ] + ) + } + // TODO: this should be improved some more, we need to generated a `: Map` on the Swift side func test_wrapJava_genericMethodTypeErasure_genericExtendsMap() async throws { let classpathURL = try await compileJava( @@ -370,4 +409,4 @@ final class GenericsWrapJavaTests: XCTestCase { ) } -} \ No newline at end of file +}