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

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

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

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

- * If 'qualified' is true, leave all the qualification in place to - * disambiguate the type, producing a more complete (but longer) type name. - * - * @param typeMetadata the memory segment must point to a Swift metadata, - * e.g. the result of a {@link swift_getTypeByMangledNameInEnvironment} call - */ - public static String nameOfSwiftType(MemorySegment typeMetadata, boolean qualified) { - MethodHandle mh = swift_getTypeName.HANDLE; - - try (Arena arena = Arena.ofConfined()) { - MemorySegment charsAndLength = (MemorySegment) mh.invokeExact((SegmentAllocator) arena, typeMetadata, qualified); - MemorySegment utf8Chars = charsAndLength.get(SwiftValueLayout.SWIFT_POINTER, 0); - String typeName = utf8Chars.getString(0); - - // FIXME: this free is not always correct: - // java(80175,0x17008f000) malloc: *** error for object 0x600000362610: pointer being freed was not allocated - // cFree(utf8Chars); - - return typeName; - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); - } - } - } diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index 84c4b2ec..e0f84237 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -16,6 +16,112 @@ import JExtractSwift import Testing import struct Foundation.CharacterSet +enum RenderKind { + case swift + case java +} + +func assertOutput( + dump: Bool = false, + _ translator: Swift2JavaTranslator, + input: String, + _ renderKind: RenderKind, + detectChunkByInitialLines: Int = 4, + expectedChunks: [String], + fileID: String = #fileID, + filePath: String = #filePath, + line: Int = #line, + column: Int = #column +) throws { + try! translator.analyze(file: "/fake/Fake.swiftinterface", text: input) + + let output: String + var printer: CodePrinter = CodePrinter(io: .accumulateAll) + switch renderKind { + case .swift: + try translator.writeSwiftThunkSources(outputDirectory: "/fake", printer: &printer) + case .java: + try translator.writeExportedJavaSources(outputDirectory: "/fake", printer: &printer) + } + output = printer.finalize() + + let gotLines = output.split(separator: "\n") + for expected in expectedChunks { + let expectedLines = expected.split(separator: "\n") + + var matchingOutputOffset: Int? = nil + let expectedInitialMatchingLines = expectedLines[0.. (offset+detectChunkByInitialLines) { + let textLinesAtOffset = gotLines[offset.. 0 + if hasDiff || dump { + print("") + if hasDiff { + print("error: Number of not matching lines: \(diffLineNumbers.count)!".red) + + print("==== ---------------------------------------------------------------") + print("Expected output:") + for (n, e) in expectedLines.enumerated() { + print("\(n): \(e)".yellow(if: diffLineNumbers.map({$0 - matchingOutputOffset}).contains(n))) + } + } + + print("==== ---------------------------------------------------------------") + print("Got output:") + let printFromLineNo = matchingOutputOffset + let printToLineNo = matchingOutputOffset + expectedLines.count + for (n, g) in gotLines.enumerated() where n >= printFromLineNo && n <= printToLineNo { + print("\(n): \(g)".red(if: diffLineNumbers.contains(n))) + } + print("==== ---------------------------------------------------------------\n") + } + } +} + func assertOutput( dump: Bool = false, _ got: String, @@ -32,21 +138,13 @@ func assertOutput( for (no, (g, e)) in zip(gotLines, expectedLines).enumerated() { if g.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).count == 0 - || e.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).count == 0 - { + || e.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).count == 0 { continue } let ge = g.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) let ee = e.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) if ge.commonPrefix(with: ee) != ee { - // print("") - // print("[\(file):\(line)] " + "Difference found on line: \(no + 1)!".red) - // print("Expected @ \(file):\(Int(line) + no + 3 /*formatting*/ + 1):") - // print(e.yellow) - // print("Got instead:") - // print(g.red) - diffLineNumbers.append(no) let sourceLocation = SourceLocation( @@ -57,7 +155,7 @@ func assertOutput( } let hasDiff = diffLineNumbers.count > 0 - if hasDiff || dump{ + if hasDiff || dump { print("") if hasDiff { print("error: Number of not matching lines: \(diffLineNumbers.count)!".red) diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index 90541263..41440f8e 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -41,12 +41,12 @@ final class FuncCallbackImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/Fake.swiftinterface", text: Self.class_interfaceFile) + try st.analyze(file: "Fake.swift", text: Self.class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.baseIdentifier == "callMe" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: nil) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: nil) } assertOutput( diff --git a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift index 2f3bccb1..3160d55f 100644 --- a/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift +++ b/Tests/JExtractSwiftTests/FunctionDescriptorImportTests.swift @@ -53,7 +53,6 @@ final class FunctionDescriptorTests { } """ - @Test func FunctionDescriptor_globalTakeInt() throws { try functionDescriptorTest("globalTakeInt") { output in @@ -141,7 +140,7 @@ extension FunctionDescriptorTests { javaPackage: String = "com.example.swift", swiftModuleName: String = "SwiftModule", logLevel: Logger.Level = .trace, - body: (String) throws -> () + body: (String) throws -> Void ) throws { let st = Swift2JavaTranslator( javaPackage: javaPackage, @@ -149,7 +148,7 @@ extension FunctionDescriptorTests { ) st.log.logLevel = logLevel - try st.analyze(swiftInterfacePath: "/fake/Sample.swiftinterface", text: interfaceFile) + try st.analyze(file: "/fake/Sample.swiftinterface", text: interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.baseIdentifier == methodIdentifier @@ -168,7 +167,7 @@ extension FunctionDescriptorTests { javaPackage: String = "com.example.swift", swiftModuleName: String = "SwiftModule", logLevel: Logger.Level = .trace, - body: (String) throws -> () + body: (String) throws -> Void ) throws { let st = Swift2JavaTranslator( javaPackage: javaPackage, @@ -176,20 +175,21 @@ extension FunctionDescriptorTests { ) st.log.logLevel = logLevel - try st.analyze(swiftInterfacePath: "/fake/Sample.swiftinterface", text: interfaceFile) + try st.analyze(file: "/fake/Sample.swiftinterface", text: interfaceFile) let varDecl: ImportedVariable? = st.importedTypes.values.compactMap { - $0.variables.first { - $0.identifier == identifier - } - }.first + $0.variables.first { + $0.identifier == identifier + } + }.first guard let varDecl else { fatalError("Cannot find descriptor of: \(identifier)") } let getOutput = CodePrinter.toString { printer in - st.printFunctionDescriptorValue(&printer, varDecl.accessorFunc(kind: accessorKind)!, accessorKind: accessorKind) + st.printFunctionDescriptorValue( + &printer, varDecl.accessorFunc(kind: accessorKind)!, accessorKind: accessorKind) } try body(getOutput) diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index 3b2ef4f4..bc5b33f8 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -70,12 +70,12 @@ final class MethodImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/Fake.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.baseIdentifier == "helloWorld" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: nil) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: nil) } assertOutput( @@ -111,14 +111,14 @@ final class MethodImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.baseIdentifier == "globalTakeInt" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: nil) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: nil) } assertOutput( @@ -154,14 +154,14 @@ final class MethodImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl = st.importedGlobalFuncs.first { $0.baseIdentifier == "globalTakeIntLongString" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .memorySegment) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .memorySegment) } assertOutput( @@ -197,14 +197,14 @@ final class MethodImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "helloMemberFunction" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .memorySegment) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .memorySegment) } assertOutput( @@ -240,14 +240,14 @@ final class MethodImportTests { ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "helloMemberInExtension" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .memorySegment) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .memorySegment) } assertOutput( @@ -283,14 +283,14 @@ final class MethodImportTests { ) st.log.logLevel = .info - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "helloMemberFunction" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .memorySegment) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .memorySegment) } assertOutput( @@ -326,14 +326,14 @@ final class MethodImportTests { ) st.log.logLevel = .info - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "helloMemberFunction" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .wrapper) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .wrapper) } assertOutput( @@ -361,14 +361,14 @@ final class MethodImportTests { ) st.log.logLevel = .info - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let funcDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.methods.first { $0.baseIdentifier == "makeInt" }! let output = CodePrinter.toString { printer in - st.printFuncDowncallMethod(&printer, decl: funcDecl, selfVariant: .wrapper) + st.printFuncDowncallMethod(&printer, decl: funcDecl, paramPassingStyle: .wrapper) } assertOutput( @@ -396,14 +396,14 @@ final class MethodImportTests { ) st.log.logLevel = .info - try st.analyze(swiftInterfacePath: "/fake/__FakeModule/SwiftFile.swiftinterface", text: class_interfaceFile) + try st.analyze(file: "Fake.swift", text: class_interfaceFile) let initDecl: ImportedFunc = st.importedTypes["MySwiftClass"]!.initializers.first { $0.identifier == "init(len:cap:)" }! let output = CodePrinter.toString { printer in - st.printClassInitializerConstructors(&printer, initDecl, parentName: initDecl.parentName!) + st.printClassInitializerConstructors(&printer, initDecl, parentName: initDecl.parent!) } assertOutput( diff --git a/Tests/JExtractSwiftTests/MethodThunkTests.swift b/Tests/JExtractSwiftTests/MethodThunkTests.swift new file mode 100644 index 00000000..705a4c7c --- /dev/null +++ b/Tests/JExtractSwiftTests/MethodThunkTests.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwift +import Testing + +final class MethodThunkTests { + let input = + """ + import Swift + + public func globalFunc(a: Int32, b: Int64) {} + public func globalFunc(a: Double, b: Int64) {} + """ + + @Test("Thunk overloads: globalFunc(a: Int32, b: Int64) & globalFunc(i32: Int32, l: Int64)") + func thunk_overloads() throws { + let st = Swift2JavaTranslator( + javaPackage: "com.example.swift", + swiftModuleName: "FakeModule" + ) + st.log.logLevel = .error + + try assertOutput( + st, input: input, .swift, + detectChunkByInitialLines: 1, + expectedChunks: + [ + """ + @_cdecl("swiftjava_FakeModule_globalFunc_a_b") + public func swiftjava_FakeModule_globalFunc_a_b(a: Int32, b: Int64) -> Swift.Void /* Void */ { + globalFunc(a: a, b: b) + } + """, + """ + @_cdecl("swiftjava_FakeModule_globalFunc_a_b$1") + public func swiftjava_FakeModule_globalFunc_a_b$1(a: Double, b: Int64) -> Swift.Void /* Void */ { + globalFunc(a: a, b: b) + } + """ + ] + ) + } + +} diff --git a/Tests/JExtractSwiftTests/ThunkNameRegistryTests.swift b/Tests/JExtractSwiftTests/ThunkNameRegistryTests.swift new file mode 100644 index 00000000..58447268 --- /dev/null +++ b/Tests/JExtractSwiftTests/ThunkNameRegistryTests.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwift +import Testing + +final class ThunkNameRegistryTests { + @Test("Thunk names: deduplicate names") + func deduplicate() throws { + var registry = ThunkNameRegistry() + #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello") + #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello$1") + #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello$2") + #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello$3") + #expect(registry.deduplicate(name: "swiftjava_other") == "swiftjava_other") + #expect(registry.deduplicate(name: "swiftjava_other") == "swiftjava_other$1") + } +} \ No newline at end of file diff --git a/Tests/JExtractSwiftTests/VariableImportTests.swift b/Tests/JExtractSwiftTests/VariableImportTests.swift index d776b42a..ac3b003e 100644 --- a/Tests/JExtractSwiftTests/VariableImportTests.swift +++ b/Tests/JExtractSwiftTests/VariableImportTests.swift @@ -34,138 +34,164 @@ final class VariableImportTests { """ @Test("Import: var counter: Int") - func variable_() throws { + func variable_int() throws { let st = Swift2JavaTranslator( javaPackage: "com.example.swift", - swiftModuleName: "__FakeModule" + swiftModuleName: "FakeModule" ) st.log.logLevel = .error - try st.analyze(swiftInterfacePath: "/fake/Fake.swiftinterface", text: class_interfaceFile) - - let identifier = "counterInt" - let varDecl: ImportedVariable? = - st.importedTypes.values.compactMap { - $0.variables.first { - $0.identifier == identifier - } - }.first - guard let varDecl else { - fatalError("Cannot find: \(identifier)") - } - - let output = CodePrinter.toString { printer in - st.printVariableDowncallMethods(&printer, varDecl) - } - - assertOutput( - dump: true, - output, - expected: - """ - // ==== -------------------------------------------------- - // counterInt - private static class counterInt { - public static final FunctionDescriptor DESC_GET = FunctionDescriptor.of( - /* -> */SWIFT_INT, - SWIFT_POINTER - ); - public static final MemorySegment ADDR_GET = __FakeModule.findOrThrow("g"); - public static final MethodHandle HANDLE_GET = Linker.nativeLinker().downcallHandle(ADDR_GET, DESC_GET); - public static final FunctionDescriptor DESC_SET = FunctionDescriptor.ofVoid( - SWIFT_INT, - SWIFT_POINTER - ); - public static final MemorySegment ADDR_SET = __FakeModule.findOrThrow("s"); - public static final MethodHandle HANDLE_SET = Linker.nativeLinker().downcallHandle(ADDR_SET, DESC_SET); - } - /** - * Function descriptor for: - * - */ - public static FunctionDescriptor counterInt$get$descriptor() { - return counterInt.DESC_GET; - } - /** - * Downcall method handle for: - * - */ - public static MethodHandle counterInt$get$handle() { - return counterInt.HANDLE_GET; - } - /** - * Address for: - * - */ - public static MemorySegment counterInt$get$address() { - return counterInt.ADDR_GET; - } - /** - * Function descriptor for: - * - */ - public static FunctionDescriptor counterInt$set$descriptor() { - return counterInt.DESC_SET; - } - /** - * Downcall method handle for: - * - */ - public static MethodHandle counterInt$set$handle() { - return counterInt.HANDLE_SET; - } - /** - * Address for: - * - */ - public static MemorySegment counterInt$set$address() { - return counterInt.ADDR_SET; - } - /** - * Downcall to Swift: - * - */ - public static long getCounterInt(java.lang.foreign.MemorySegment self$) { - var mh$ = counterInt.HANDLE_GET; + try assertOutput( + st, input: class_interfaceFile, .java, + detectChunkByInitialLines: 7, + expectedChunks: + [ + """ + private static class counterInt { + public static final FunctionDescriptor DESC_GET = FunctionDescriptor.of( + /* -> */SWIFT_INT, + SWIFT_POINTER + ); + public static final MemorySegment ADDR_GET = + FakeModule.findOrThrow("swiftjava_FakeModule_MySwiftClass_counterInt$1"); + public static final MethodHandle HANDLE_GET = Linker.nativeLinker().downcallHandle(ADDR_GET, DESC_GET); + public static final FunctionDescriptor DESC_SET = FunctionDescriptor.ofVoid( + SWIFT_INT, + SWIFT_POINTER + ); + public static final MemorySegment ADDR_SET = + FakeModule.findOrThrow("swiftjava_FakeModule_MySwiftClass_counterInt__$1"); + public static final MethodHandle HANDLE_SET = Linker.nativeLinker().downcallHandle(ADDR_SET, DESC_SET); + } + """, + """ + /** + * Function descriptor for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static FunctionDescriptor counterInt$get$descriptor() { + return counterInt.DESC_GET; + } + """, + """ + /** + * Downcall method handle for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static MethodHandle counterInt$get$handle() { + return counterInt.HANDLE_GET; + } + """, + """ + /** + * Address for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static MemorySegment counterInt$get$address() { + return counterInt.ADDR_GET; + } + """, + """ + /** + * Function descriptor for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static FunctionDescriptor counterInt$set$descriptor() { + return counterInt.DESC_SET; + } + """ + , + """ + /** + * Downcall method handle for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static MethodHandle counterInt$set$handle() { + return counterInt.HANDLE_SET; + } + """, + """ + /** + * Address for: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static MemorySegment counterInt$set$address() { + return counterInt.ADDR_SET; + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static long getCounterInt(java.lang.foreign.MemorySegment self$) { + var mh$ = counterInt.HANDLE_GET; + try { + if (TRACE_DOWNCALLS) { + traceDowncall(self$); + } + return (long) mh$.invokeExact(self$); + } catch (Throwable ex$) { + throw new AssertionError("should not reach here", ex$); + } + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public long getCounterInt() { + return (long) getCounterInt($memorySegment()); + } + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public static void setCounterInt(long newValue, java.lang.foreign.MemorySegment self$) { + var mh$ = counterInt.HANDLE_SET; try { - if (TRACE_DOWNCALLS) { - traceDowncall(self$); - } - return (long) mh$.invokeExact(self$); + if (TRACE_DOWNCALLS) { + traceDowncall(newValue, self$); + } + mh$.invokeExact(newValue, self$); } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); + throw new AssertionError("should not reach here", ex$); } - } - /** - * Downcall to Swift: - * - */ - public long getCounterInt() { - return (long) getCounterInt($memorySegment()); - } - /** - * Downcall to Swift: - * - */ - public static void setCounterInt(long newValue, java.lang.foreign.MemorySegment self$) { - var mh$ = counterInt.HANDLE_SET; - try { - if (TRACE_DOWNCALLS) { - traceDowncall(newValue, self$); - } - mh$.invokeExact(newValue, self$); - } catch (Throwable ex$) { - throw new AssertionError("should not reach here", ex$); } - } - /** - * Downcall to Swift: - * - */ - public void setCounterInt(long newValue) { - setCounterInt(newValue, $memorySegment()); - } - """ + """, + """ + /** + * Downcall to Swift: + * {@snippet lang=swift : + * public var counterInt: Int + * } + */ + public void setCounterInt(long newValue) { + setCounterInt(newValue, $memorySegment()); + } + """ + ] ) } } From 98594a9289060fd0d0f27a3d31f67bac32dbe16c Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Sat, 9 Nov 2024 10:26:52 +0900 Subject: [PATCH 2/8] Revert "Adapt to swift-syntax changes in generic argument handling" This reverts commit 0f4002d1893befe7ecc79b038a31ac2911c94f28. --- Sources/JExtractSwift/TranslatedType.swift | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Sources/JExtractSwift/TranslatedType.swift b/Sources/JExtractSwift/TranslatedType.swift index ad54de44..9dc12e07 100644 --- a/Sources/JExtractSwift/TranslatedType.swift +++ b/Sources/JExtractSwift/TranslatedType.swift @@ -55,12 +55,7 @@ extension Swift2JavaVisitor { // Translate the generic arguments to the C-compatible types. let genericArgs = try memberType.genericArgumentClause.map { genericArgumentClause in try genericArgumentClause.arguments.map { argument in - switch argument.argument { - case .type(let argumentType): - try cCompatibleType(for: argumentType) - @unknown default: - throw TypeTranslationError.unimplementedType(TypeSyntax(memberType)) - } + try cCompatibleType(for: argument.argument) } } @@ -76,12 +71,7 @@ extension Swift2JavaVisitor { // Translate the generic arguments to the C-compatible types. let genericArgs = try identifierType.genericArgumentClause.map { genericArgumentClause in try genericArgumentClause.arguments.map { argument in - switch argument.argument { - case .type(let argumentType): - try cCompatibleType(for: argumentType) - @unknown default: - throw TypeTranslationError.unimplementedType(TypeSyntax(identifierType)) - } + try cCompatibleType(for: argument.argument) } } From 371f4e2178078b4bd362e9201894c63ff1caf860 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Sat, 9 Nov 2024 16:10:54 +0900 Subject: [PATCH 3/8] gradle build now causes proper emission of the swift/java sources --- .github/workflows/pull_request.yml | 1 + Makefile | 3 + Package.swift | 18 ++- .../JExtractSwiftCommandPlugin.swift | 135 ++++++++++++++++++ .../PluginsShared}/Configuration.swift | 2 +- .../PluginsShared/PluginUtils.swift | 48 +++++++ .../JExtractSwiftPlugin.swift | 24 +--- .../PluginsShared/Configuration.swift | 36 +++++ .../PluginsShared/PluginUtils.swift | 37 +++++ Samples/JExtractPluginSampleApp/Package.swift | 55 +++---- ...JExtractSwift.config => swift-java.config} | 0 Samples/JExtractPluginSampleApp/build.gradle | 6 +- .../swift/JExtractPluginSampleMain.java | 12 +- Samples/SwiftKitSampleApp/build.gradle | 2 +- Sources/JExtractSwift/CodePrinter.swift | 30 ++-- Sources/JExtractSwift/Logger.swift | 16 +-- Sources/JExtractSwift/Swift2Java.swift | 21 +-- .../Swift2JavaTranslator+Printing.swift | 30 ++-- .../JExtractSwift/Swift2JavaTranslator.swift | 2 +- Sources/JExtractSwift/Swift2JavaVisitor.swift | 6 +- Sources/JExtractSwift/TerminalColors.swift | 17 +++ Sources/JExtractSwift/ThunkNameRegistry.swift | 6 +- .../Asserts/TextAssertions.swift | 2 +- 23 files changed, 403 insertions(+), 106 deletions(-) create mode 100644 Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift rename Plugins/{JExtractSwiftPlugin => JExtractSwiftCommandPlugin/PluginsShared}/Configuration.swift (92%) create mode 100644 Plugins/JExtractSwiftCommandPlugin/PluginsShared/PluginUtils.swift create mode 100644 Plugins/JExtractSwiftPlugin/PluginsShared/Configuration.swift create mode 100644 Plugins/JExtractSwiftPlugin/PluginsShared/PluginUtils.swift rename Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/{JExtractSwift.config => swift-java.config} (100%) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 3e9ba36e..25f07eed 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -8,6 +8,7 @@ jobs: soundness: uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main with: + # Not API stable package (yet) api_breakage_check_enabled: false # FIXME: Something is off with the format task and it gets "stuck", need to investigate format_check_enabled: false diff --git a/Makefile b/Makefile index 74d7bbda..d8f8717a 100644 --- a/Makefile +++ b/Makefile @@ -94,6 +94,9 @@ javakit-generate: generate-JavaKit generate-JavaKitReflection generate-JavaKitJa clean: rm -rf .build; \ + rm -rf build; \ + rm -rf Samples/JExtractPluginSampleApp/.build; \ + rm -rf Samples/JExtractPluginSampleApp/build; \ rm -rf Samples/SwiftKitExampleApp/src/generated/java/* format: diff --git a/Package.swift b/Package.swift index 523e7c58..02e4abc0 100644 --- a/Package.swift +++ b/Package.swift @@ -133,7 +133,13 @@ let package = Package( "JExtractSwiftPlugin" ] ), - + .plugin( + name: "JExtractSwiftCommandPlugin", + targets: [ + "JExtractSwiftCommandPlugin" + ] + ), + // ==== Examples .library( @@ -347,6 +353,16 @@ let package = Package( "JExtractSwiftTool" ] ), + .plugin( + name: "JExtractSwiftCommandPlugin", + capability: .command( + intent: .custom(verb: "jextract", description: "Extract Java accessors from Swift module"), + permissions: [ + ]), + dependencies: [ + "JExtractSwiftTool" + ] + ), .testTarget( name: "JavaKitTests", diff --git a/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift b/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift new file mode 100644 index 00000000..9672f499 --- /dev/null +++ b/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift @@ -0,0 +1,135 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +@main +final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { + + var verbose: Bool = false + + /// Build the target before attempting to extract from it. + /// This avoids trying to extract from broken sources. + /// + /// You may disable this if confident that input targets sources are correct and there's no need to kick off a pre-build for some reason. + var buildInputs: Bool = true + + /// Build the target once swift-java sources have been generated. + /// This helps verify that the generated output is correct, and won't miscompile on the next build. + var buildOutputs: Bool = true + + func createBuildCommands(context: PackagePlugin.PluginContext, target: any PackagePlugin.Target) async throws -> [PackagePlugin.Command] { + // FIXME: This is not a build plugin but SwiftPM forces us to impleme the protocol anyway? rdar://139556637 + return [] + } + + func performCommand(context: PluginContext, arguments: [String]) throws { + self.verbose = arguments.contains("-v") || arguments.contains("--verbose") + + let selectedTargets: [String] = + if let last = arguments.lastIndex(where: { $0.starts(with: "-")}), + last < arguments.endIndex { + Array(arguments[.. String) { + if self.verbose { + print("[swift-java] \(message())") + } + } +} diff --git a/Plugins/JExtractSwiftPlugin/Configuration.swift b/Plugins/JExtractSwiftCommandPlugin/PluginsShared/Configuration.swift similarity index 92% rename from Plugins/JExtractSwiftPlugin/Configuration.swift rename to Plugins/JExtractSwiftCommandPlugin/PluginsShared/Configuration.swift index 042c4952..f917290c 100644 --- a/Plugins/JExtractSwiftPlugin/Configuration.swift +++ b/Plugins/JExtractSwiftCommandPlugin/PluginsShared/Configuration.swift @@ -21,7 +21,7 @@ struct Configuration: Codable { } func readConfiguration(sourceDir: String) throws -> Configuration { - let configFile = URL(filePath: sourceDir).appending(path: "JExtractSwift.config") + let configFile = URL(filePath: sourceDir).appending(path: "swift-java.config") do { let configData = try Data(contentsOf: configFile) return try JSONDecoder().decode(Configuration.self, from: configData) diff --git a/Plugins/JExtractSwiftCommandPlugin/PluginsShared/PluginUtils.swift b/Plugins/JExtractSwiftCommandPlugin/PluginsShared/PluginUtils.swift new file mode 100644 index 00000000..320dfc19 --- /dev/null +++ b/Plugins/JExtractSwiftCommandPlugin/PluginsShared/PluginUtils.swift @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation +import PackagePlugin + +// Note: the JAVA_HOME environment variable must be set to point to where +// Java is installed, e.g., +// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. +func findJavaHome() -> String { + if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { + return home + } + + // This is a workaround for envs (some IDEs) which have trouble with + // picking up env variables during the build process + let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" + if let home = try? String(contentsOfFile: path, encoding: .utf8) { + if let lastChar = home.last, lastChar.isNewline { + return String(home.dropLast()) + } + + return home + } + + fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") +} + +func getSwiftJavaConfig(target: Target) -> String? { + let configPath = URL(fileURLWithPath: target.directory.string).appending(component: "swift-java.config").path() + + if FileManager.default.fileExists(atPath: configPath) { + return configPath + } else { + return nil + } +} diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index 9e175fe0..2d1be58c 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -24,6 +24,7 @@ struct JExtractSwiftBuildToolPlugin: BuildToolPlugin { // so we cannot eliminate this deprecation warning. let sourceDir = target.directory.string + let toolURL = try context.tool(named: "JExtractSwiftTool").url let configuration = try readConfiguration(sourceDir: "\(sourceDir)") // We use the the usual maven-style structure of "src/[generated|main|test]/java/..." @@ -52,7 +53,7 @@ struct JExtractSwiftBuildToolPlugin: BuildToolPlugin { return [ .prebuildCommand( displayName: "Generate Java wrappers for Swift types", - executable: try context.tool(named: "JExtractSwiftTool").url, + executable: toolURL, arguments: arguments, // inputFiles: [ configFile ] + swiftFiles, // outputFiles: outputJavaFiles @@ -62,24 +63,3 @@ struct JExtractSwiftBuildToolPlugin: BuildToolPlugin { } } -// Note: the JAVA_HOME environment variable must be set to point to where -// Java is installed, e.g., -// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. -func findJavaHome() -> String { - if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { - return home - } - - // This is a workaround for envs (some IDEs) which have trouble with - // picking up env variables during the build process - let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" - if let home = try? String(contentsOfFile: path, encoding: .utf8) { - if let lastChar = home.last, lastChar.isNewline { - return String(home.dropLast()) - } - - return home - } - - fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") -} diff --git a/Plugins/JExtractSwiftPlugin/PluginsShared/Configuration.swift b/Plugins/JExtractSwiftPlugin/PluginsShared/Configuration.swift new file mode 100644 index 00000000..f917290c --- /dev/null +++ b/Plugins/JExtractSwiftPlugin/PluginsShared/Configuration.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// Configuration for the JExtractSwift translation tool, provided on a per-target +/// basis. +struct Configuration: Codable { + var javaPackage: String +} + +func readConfiguration(sourceDir: String) throws -> Configuration { + let configFile = URL(filePath: sourceDir).appending(path: "swift-java.config") + do { + let configData = try Data(contentsOf: configFile) + return try JSONDecoder().decode(Configuration.self, from: configData) + } catch { + throw ConfigurationError(message: "Failed to parse JExtractSwift configuration at '\(configFile)!'", error: error) + } +} + +struct ConfigurationError: Error { + let message: String + let error: any Error +} diff --git a/Plugins/JExtractSwiftPlugin/PluginsShared/PluginUtils.swift b/Plugins/JExtractSwiftPlugin/PluginsShared/PluginUtils.swift new file mode 100644 index 00000000..6ec58147 --- /dev/null +++ b/Plugins/JExtractSwiftPlugin/PluginsShared/PluginUtils.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +// Note: the JAVA_HOME environment variable must be set to point to where +// Java is installed, e.g., +// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. +func findJavaHome() -> String { + if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { + return home + } + + // This is a workaround for envs (some IDEs) which have trouble with + // picking up env variables during the build process + let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" + if let home = try? String(contentsOfFile: path, encoding: .utf8) { + if let lastChar = home.last, lastChar.isNewline { + return String(home.dropLast()) + } + + return home + } + + fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") +} diff --git a/Samples/JExtractPluginSampleApp/Package.swift b/Samples/JExtractPluginSampleApp/Package.swift index 26215e55..654d7ad1 100644 --- a/Samples/JExtractPluginSampleApp/Package.swift +++ b/Samples/JExtractPluginSampleApp/Package.swift @@ -40,31 +40,32 @@ let javaIncludePath = "\(javaHome)/include" #endif let package = Package( - name: "JExtractPluginSampleApp", - platforms: [ - .macOS(.v10_15), - ], - products: [ - .library( - name: "JExtractPluginSampleLib", - type: .dynamic, - targets: ["JExtractPluginSampleLib"] - ), - ], - dependencies: [ - .package(name: "swift-java", path: "../../"), - ], - targets: [ - .target( - name: "JExtractPluginSampleLib", - dependencies: [ - ], - swiftSettings: [ - .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) - ], - plugins: [ - .plugin(name: "JExtractSwiftPlugin", package: "swift-java"), - ] - ), - ] + name: "JExtractPluginSampleApp", + platforms: [ + .macOS(.v10_15) + ], + products: [ + .library( + name: "JExtractPluginSampleLib", + type: .dynamic, + targets: ["JExtractPluginSampleLib"] + ) + ], + dependencies: [ + .package(name: "swift-java", path: "../../") + ], + targets: [ + .target( + name: "JExtractPluginSampleLib", + dependencies: [], + swiftSettings: [ + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) + ], + plugins: [ + .plugin(name: "JExtractSwiftPlugin", package: "swift-java") + , + .plugin(name: "JExtractSwiftCommandPlugin", package: "swift-java") + ] + ) + ] ) diff --git a/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/JExtractSwift.config b/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/swift-java.config similarity index 100% rename from Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/JExtractSwift.config rename to Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/swift-java.config diff --git a/Samples/JExtractPluginSampleApp/build.gradle b/Samples/JExtractPluginSampleApp/build.gradle index 576a9436..d777b34d 100644 --- a/Samples/JExtractPluginSampleApp/build.gradle +++ b/Samples/JExtractPluginSampleApp/build.gradle @@ -49,7 +49,7 @@ def buildJExtractPlugin = tasks.register("swiftBuildJExtractPlugin", Exec) { args "build" } -def buildSwift = tasks.register("swiftBuildProject", Exec) { +def jextract = tasks.register("jextract", Exec) { description = "Builds swift sources, including swift-java source generation" dependsOn buildJExtractPlugin @@ -76,14 +76,14 @@ def buildSwift = tasks.register("swiftBuildProject", Exec) { workingDir = layout.projectDirectory commandLine "swift" - args "build" + args("package", "jextract") } // Add the java-swift generated Java sources sourceSets { main { java { - srcDir(buildSwift) + srcDir(jextract) } } } diff --git a/Samples/JExtractPluginSampleApp/src/main/java/com/example/swift/JExtractPluginSampleMain.java b/Samples/JExtractPluginSampleApp/src/main/java/com/example/swift/JExtractPluginSampleMain.java index d1ac8320..43a7f38a 100644 --- a/Samples/JExtractPluginSampleApp/src/main/java/com/example/swift/JExtractPluginSampleMain.java +++ b/Samples/JExtractPluginSampleApp/src/main/java/com/example/swift/JExtractPluginSampleMain.java @@ -22,7 +22,15 @@ public static void main(String[] args) { System.out.println("java.library.path = " + SwiftKit.getJavaLibraryPath()); System.out.println("jextract.trace.downcalls = " + SwiftKit.getJextractTraceDowncalls()); - var o = new MyCoolSwiftClass(12); - o.exposedToJava(); + try { + var o = new MyCoolSwiftClass(12); + o.exposedToJava(); + } catch (UnsatisfiedLinkError linkError) { + if (linkError.getMessage().startsWith("unresolved symbol: ")) { + var unresolvedSymbolName = linkError.getMessage().substring("unresolved symbol: ".length()); + + + } + } } } diff --git a/Samples/SwiftKitSampleApp/build.gradle b/Samples/SwiftKitSampleApp/build.gradle index f8da2f6d..fca33d4c 100644 --- a/Samples/SwiftKitSampleApp/build.gradle +++ b/Samples/SwiftKitSampleApp/build.gradle @@ -75,7 +75,7 @@ def buildSwift = tasks.register("swiftBuildProject", Exec) { workingDir = layout.projectDirectory commandLine "swift" - args "build" + args ["package", "jextract"] } // Add the java-swift generated Java sources diff --git a/Sources/JExtractSwift/CodePrinter.swift b/Sources/JExtractSwift/CodePrinter.swift index 24fe0313..6e26b960 100644 --- a/Sources/JExtractSwift/CodePrinter.swift +++ b/Sources/JExtractSwift/CodePrinter.swift @@ -40,13 +40,13 @@ public struct CodePrinter { return printer.finalize() } - var ioMode: PrintMode + var mode: PrintMode public enum PrintMode { case accumulateAll case flushToFileOnWrite } - public init(io: PrintMode = .flushToFileOnWrite) { - self.ioMode = .accumulateAll + public init(mode: PrintMode = .flushToFileOnWrite) { + self.mode = mode } internal mutating func append(_ text: String) { @@ -208,16 +208,18 @@ public enum PrinterTerminator: String { } extension CodePrinter { + + /// - Returns: the output path of the generated file, if any (i.e. not in accumulate in memory mode) package mutating func writeContents( outputDirectory: String, javaPackagePath: String?, filename: String - ) throws { - guard self.ioMode != .accumulateAll else { + ) throws -> URL? { + guard self.mode != .accumulateAll else { // if we're accumulating everything, we don't want to finalize/flush any contents // let's mark that this is where a write would have happened though: print("// ^^^^ Contents of: \(outputDirectory)/\(filename)") - return + return nil } let contents = finalize() @@ -231,24 +233,22 @@ extension CodePrinter { print("// \(filename)") } print(contents) - return + return nil } - let targetDirectory = [outputDirectory, javaPackagePath].compactMap { $0 }.joined( - separator: PATH_SEPARATOR) + let targetDirectory = [outputDirectory, javaPackagePath].compactMap { $0 }.joined(separator: PATH_SEPARATOR) log.trace("Prepare target directory: \(targetDirectory)") try FileManager.default.createDirectory( atPath: targetDirectory, withIntermediateDirectories: true) - let targetFilePath = [javaPackagePath, filename].compactMap { $0 }.joined( - separator: PATH_SEPARATOR) - Swift.print("Writing '\(targetFilePath)'...", terminator: "") + let outputPath = Foundation.URL(fileURLWithPath: targetDirectory).appendingPathComponent(filename) try contents.write( - to: Foundation.URL(fileURLWithPath: targetDirectory).appendingPathComponent(filename), + to: outputPath, atomically: true, encoding: .utf8 ) - Swift.print(" done.".green) + + return outputPath } -} \ No newline at end of file +} diff --git a/Sources/JExtractSwift/Logger.swift b/Sources/JExtractSwift/Logger.swift index 6a3dcc1b..9aceb201 100644 --- a/Sources/JExtractSwift/Logger.swift +++ b/Sources/JExtractSwift/Logger.swift @@ -95,14 +95,14 @@ public struct Logger { } extension Logger { - public enum Level: Int, Hashable { - case trace = 0 - case debug = 1 - case info = 2 - case notice = 3 - case warning = 4 - case error = 5 - case critical = 6 + public enum Level: String, Hashable { + case trace = "trace" + case debug = "debug" + case info = "info" + case notice = "notice" + case warning = "warning" + case error = "error" + case critical = "critical" } } diff --git a/Sources/JExtractSwift/Swift2Java.swift b/Sources/JExtractSwift/Swift2Java.swift index 4848d051..6d864449 100644 --- a/Sources/JExtractSwift/Swift2Java.swift +++ b/Sources/JExtractSwift/Swift2Java.swift @@ -50,7 +50,6 @@ public struct SwiftToJava: ParsableCommand { public func run() throws { let inputPaths = self.input.dropFirst().map { URL(string: $0)! } - print("Input \(inputPaths)") let translator = Swift2JavaTranslator( javaPackage: packageName, @@ -60,8 +59,10 @@ public struct SwiftToJava: ParsableCommand { var allFiles: [URL] = [] let fileManager = FileManager.default - + let log = translator.log + for path in inputPaths { + log.debug("Input path: \(path)") if isDirectory(url: path) { if let enumerator = fileManager.enumerator(at: path, includingPropertiesForKeys: nil) { for case let fileURL as URL in enumerator { @@ -73,20 +74,24 @@ public struct SwiftToJava: ParsableCommand { } } - for file in allFiles { - print("Importing module '\(swiftModule)', interface file: \(file)") + for file in allFiles where canExtract(from: file) { + translator.log.debug("Importing module '\(swiftModule)', file: \(file)") try translator.analyze(file: file.path) try translator.writeExportedJavaSources(outputDirectory: outputDirectoryJava) try translator.writeSwiftThunkSources(outputDirectory: outputDirectorySwift) - print("Imported interface file: \(file) " + "done.".green) + log.debug("[swift-java] Imported interface file: \(file.path)") } try translator.writeExportedJavaModule(outputDirectory: outputDirectoryJava) - print("") - print("Generated Java sources in package '\(packageName)' in: \(outputDirectoryJava)/") - print("Swift module '\(swiftModule)' import: " + "done.".green) + print("[swift-java] Generated Java sources (\(packageName)) in: \(outputDirectoryJava)/") + print("[swift-java] Imported Swift module '\(swiftModule)': " + "done.".green) + } + + func canExtract(from file: URL) -> Bool { + file.lastPathComponent.hasSuffix(".swift") || + file.lastPathComponent.hasSuffix(".swiftinterface") } } diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift index 67f900df..bb26f93d 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift @@ -36,11 +36,13 @@ extension Swift2JavaTranslator { log.info("Printing contents: \(filename)") printImportedClass(&printer, ty) - try printer.writeContents( + if let outputFile = try printer.writeContents( outputDirectory: outputDirectory, javaPackagePath: javaPackagePath, filename: filename - ) + ) { + print("[swift-java] Generated: \(ty.javaClassName.bold).java (at \(outputFile))") + } } } @@ -56,29 +58,35 @@ extension Swift2JavaTranslator { printSwiftThunkSources(&printer, decl: decl) } - let moduleFilename = "\(self.swiftModuleName)Module+SwiftJava.swift" + let moduleFilenameBase = "\(self.swiftModuleName)Module+SwiftJava" + let moduleFilename = "\(moduleFilenameBase).swift" log.info("Printing contents: \(moduleFilename)") do { - try printer.writeContents( + if let outputFile = try printer.writeContents( outputDirectory: outputDirectory, javaPackagePath: nil, - filename: moduleFilename) + filename: moduleFilename) { + print("[swift-java] Generated: \(moduleFilenameBase.bold).java (at \(outputFile)") + } } catch { log.warning("Failed to write to Swift thunks: \(moduleFilename)") } // === All types for (_, ty) in importedTypes.sorted(by: { (lhs, rhs) in lhs.key < rhs.key }) { - let filename = "\(ty.swiftTypeName)+SwiftJava.swift" + let fileNameBase = "\(ty.swiftTypeName)+SwiftJava" + let filename = "\(fileNameBase).swift" log.info("Printing contents: \(filename)") do { try printSwiftThunkSources(&printer, ty: ty) - try printer.writeContents( + if let outputFile = try printer.writeContents( outputDirectory: outputDirectory, javaPackagePath: nil, - filename: filename) + filename: filename) { + print("[swift-java] Generated: \(fileNameBase.bold).swift (at \(outputFile)") + } } catch { log.warning("Failed to write to Swift thunks: \(filename)") } @@ -113,11 +121,13 @@ extension Swift2JavaTranslator { public func writeExportedJavaModule(outputDirectory: String, printer: inout CodePrinter) throws { printModule(&printer) - try printer.writeContents( + if let file = try printer.writeContents( outputDirectory: outputDirectory, javaPackagePath: javaPackagePath, filename: "\(swiftModuleName).java" - ) + ) { + self.log.info("Generated: \(file): \("done".green).") + } } } diff --git a/Sources/JExtractSwift/Swift2JavaTranslator.swift b/Sources/JExtractSwift/Swift2JavaTranslator.swift index 51cb0335..9894244b 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator.swift @@ -81,7 +81,7 @@ extension Swift2JavaTranslator { try analyzeSwiftInterface(interfaceFilePath: file, text: text) - log.info("Done processing: \(file)") + log.debug("Done processing: \(file)") } package func analyzeSwiftInterface(interfaceFilePath: String, text: String) throws { diff --git a/Sources/JExtractSwift/Swift2JavaVisitor.swift b/Sources/JExtractSwift/Swift2JavaVisitor.swift index b3445c05..c1e63998 100644 --- a/Sources/JExtractSwift/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwift/Swift2JavaVisitor.swift @@ -50,7 +50,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { override func visitPost(_ node: ClassDeclSyntax) { if currentTypeName != nil { - log.info("Completed import: \(node.kind) \(node.name)") + log.debug("Completed import: \(node.kind) \(node.name)") currentTypeName = nil } } @@ -186,7 +186,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { return .skipChildren } - self.log.info("Import initializer: \(node.kind) \(currentType.javaType.description)") + self.log.debug("Import initializer: \(node.kind) \(currentType.javaType.description)") let params: [ImportedParam] do { params = try node.signature.parameterClause.parameters.map { param in @@ -215,7 +215,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { ) funcDecl.isInit = true - log.info( + log.debug( "Record initializer method in \(currentType.javaType.description): \(funcDecl.identifier)") translator.importedTypes[currentTypeName]!.initializers.append(funcDecl) diff --git a/Sources/JExtractSwift/TerminalColors.swift b/Sources/JExtractSwift/TerminalColors.swift index fcef5a3c..6170e2bc 100644 --- a/Sources/JExtractSwift/TerminalColors.swift +++ b/Sources/JExtractSwift/TerminalColors.swift @@ -21,6 +21,7 @@ package enum Rainbow: String { case magenta = "\u{001B}[0;35m" case cyan = "\u{001B}[0;36m" case white = "\u{001B}[0;37m" + case bold = "\u{001B}[1m" case `default` = "\u{001B}[0;0m" func name() -> String { @@ -33,6 +34,7 @@ package enum Rainbow: String { case .magenta: return "Magenta" case .cyan: return "Cyan" case .white: return "White" + case .bold: return "Bold" case .default: return "Default" } } @@ -127,6 +129,17 @@ package extension String { } } + var bold: String { + self.colored(as: .bold) + } + func bold(if condition: Bool) -> String { + if condition { + self.colored(as: .bold) + } else { + self + } + } + var `default`: String { self.colored(as: .default) } @@ -169,6 +182,10 @@ package extension Substring { self.colored(as: .white) } + var bold: String { + self.colored(as: .bold) + } + var `default`: String { self.colored(as: .default) } diff --git a/Sources/JExtractSwift/ThunkNameRegistry.swift b/Sources/JExtractSwift/ThunkNameRegistry.swift index 2a41ce4a..5cf49dae 100644 --- a/Sources/JExtractSwift/ThunkNameRegistry.swift +++ b/Sources/JExtractSwift/ThunkNameRegistry.swift @@ -23,12 +23,12 @@ package struct ThunkNameRegistry { package mutating func deduplicate(name: String) -> String { var emittedCount = self.baseNames[name, default: 0] - self.baseNames[name] = emittedCount + 1 + defer { self.baseNames[name] = emittedCount + 1 } if emittedCount == 0 { - return name // first occurrence of a name we keep as-is + return name // first occurrence of a name we keep as-is } else { return "\(name)$\(emittedCount)" } } -} \ No newline at end of file +} diff --git a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift index e0f84237..0efff0c7 100644 --- a/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift +++ b/Tests/JExtractSwiftTests/Asserts/TextAssertions.swift @@ -36,7 +36,7 @@ func assertOutput( try! translator.analyze(file: "/fake/Fake.swiftinterface", text: input) let output: String - var printer: CodePrinter = CodePrinter(io: .accumulateAll) + var printer: CodePrinter = CodePrinter(mode: .accumulateAll) switch renderKind { case .swift: try translator.writeSwiftThunkSources(outputDirectory: "/fake", printer: &printer) From f10bdeae63af0956a5c7500dcb900b12ae801dcc Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Mon, 11 Nov 2024 11:05:22 +0900 Subject: [PATCH 4/8] cleanup install jdk script working on samples validation fixed name deduplication logic --- .github/scripts/validate_samples.sh | 36 ++++++++++ .github/workflows/pull_request.yml | 3 +- Package.swift | 2 +- .../JExtractSwiftCommandPlugin.swift | 65 ++++++++++-------- ...PluginSampleLibModule+SwiftJava.swiftdeps~ | Bin 0 -> 2100 bytes .../MyCoolSwiftClass+SwiftJava.swiftdeps~ | Bin 0 -> 2712 bytes Samples/JExtractPluginSampleApp/Package.swift | 6 +- Samples/JExtractPluginSampleApp/build.gradle | 9 ++- .../JExtractPluginSampleApp/ci-validate.sh | 3 + .../swift/JExtractPluginSampleMain.java | 12 +--- Samples/JavaSieve/ci-validate.sh | 3 + Samples/SwiftKitSampleApp/build.gradle | 4 +- Samples/SwiftKitSampleApp/gradlew | 1 + Samples/SwiftKitSampleApp/gradlew.bat | 1 + Sources/JExtractSwift/ImportedDecls.swift | 13 +++- Sources/JExtractSwift/Swift2Java.swift | 3 +- .../Swift2JavaTranslator+Printing.swift | 8 +-- Sources/JExtractSwift/SwiftKit+Printing.swift | 20 ------ .../JExtractSwift/SwiftThunkTranslator.swift | 19 +++-- Sources/JExtractSwift/ThunkNameRegistry.swift | 43 +++++++++--- docker/install_jdk.sh | 4 -- 21 files changed, 158 insertions(+), 97 deletions(-) create mode 100755 .github/scripts/validate_samples.sh create mode 100644 Samples/JExtractPluginSampleApp/JExtractPluginSampleLibModule+SwiftJava.swiftdeps~ create mode 100644 Samples/JExtractPluginSampleApp/MyCoolSwiftClass+SwiftJava.swiftdeps~ create mode 100644 Samples/JExtractPluginSampleApp/ci-validate.sh create mode 100755 Samples/JavaSieve/ci-validate.sh create mode 120000 Samples/SwiftKitSampleApp/gradlew create mode 120000 Samples/SwiftKitSampleApp/gradlew.bat diff --git a/.github/scripts/validate_samples.sh b/.github/scripts/validate_samples.sh new file mode 100755 index 00000000..c7a1223f --- /dev/null +++ b/.github/scripts/validate_samples.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# shellcheck disable=SC2034 +declare -r GREEN='\033[0;31m' +declare -r BOLD='\033[1m' +declare -r RESET='\033[0m' + +# shellcheck disable=SC2155 +declare -r SAMPLE_PACKAGES=$(find Samples -name Package.swift -maxdepth 2) +declare -r CI_VALIDATE_SCRIPT='ci-validate.sh' + +for samplePackage in ${SAMPLE_PACKAGES} ; do + sampleDir=$(dirname "$samplePackage") + + echo "" + echo "" + echo "========================================================================" + printf "Validate sample: '${BOLD}%s${RESET}' using: " "$sampleDir" + cd "$sampleDir" || exit + if [[ $(find . -name ${CI_VALIDATE_SCRIPT} -maxdepth 1) ]]; then + echo -e "Custom ${BOLD}${CI_VALIDATE_SCRIPT}${RESET} script..." + ./${CI_VALIDATE_SCRIPT} || exit + elif [[ $(find . -name 'build.gradle*' -maxdepth 1) ]]; then + echo -e "${BOLD}Gradle${RESET} build..." + ./gradlew build --info || exit + else + echo -e "${BOLD}SwiftPM${RESET} build..." + swift build || exit + fi + + cd - || exit +done + + +printf "Done validating samples: " +echo "${GREEN}done${RESET}." diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 25f07eed..44e0b0a2 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -134,8 +134,7 @@ jobs: - name: Test Swift run: "swift test" - name: Build (Swift) Sample Apps - run: | - find Samples/ -name Package.swift -maxdepth 2 -exec swift build --package-path $(dirname {}) \;; + run: .github/scripts/validate_samples.sh # TODO: Benchmark compile crashes in CI, enable when nightly toolchains in better shape. # - name: Build (Swift) Benchmarks # run: "swift package --package-path Benchmarks/ benchmark list" diff --git a/Package.swift b/Package.swift index 02e4abc0..0d5a5dab 100644 --- a/Package.swift +++ b/Package.swift @@ -150,7 +150,7 @@ let package = Package( ], dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax.git", branch: "main"), + .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.1"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"), .package(url: "https://github.com/ordo-one/package-benchmark", .upToNextMajor(from: "1.4.0")), ], diff --git a/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift b/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift index 9672f499..ee22f36d 100644 --- a/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift +++ b/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift @@ -17,27 +17,31 @@ import PackagePlugin @main final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { - + var verbose: Bool = false - + /// Build the target before attempting to extract from it. /// This avoids trying to extract from broken sources. /// /// You may disable this if confident that input targets sources are correct and there's no need to kick off a pre-build for some reason. var buildInputs: Bool = true - + /// Build the target once swift-java sources have been generated. /// This helps verify that the generated output is correct, and won't miscompile on the next build. var buildOutputs: Bool = true - + func createBuildCommands(context: PackagePlugin.PluginContext, target: any PackagePlugin.Target) async throws -> [PackagePlugin.Command] { // FIXME: This is not a build plugin but SwiftPM forces us to impleme the protocol anyway? rdar://139556637 return [] } - + func performCommand(context: PluginContext, arguments: [String]) throws { + // Plugin can't have dependencies, so we have some naive argument parsing instead: self.verbose = arguments.contains("-v") || arguments.contains("--verbose") - + if !self.verbose { + fatalError("Plugin should be verbose") + } + let selectedTargets: [String] = if let last = arguments.lastIndex(where: { $0.starts(with: "-")}), last < arguments.endIndex { @@ -45,13 +49,13 @@ final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { } else { [] } - + for target in context.package.targets { guard let configPath = getSwiftJavaConfig(target: target) else { log("Skipping target '\(target.name), has no 'swift-java.config' file") continue } - + do { print("[swift-java] Extracting Java wrappers from target: '\(target.name)'...") try performCommand(context: context, target: target, arguments: arguments) @@ -60,24 +64,19 @@ final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { } } } - + /// Perform the command on a specific target. func performCommand(context: PluginContext, target: Target, arguments: [String]) throws { // Make sure the target can builds properly try self.packageManager.build(.target(target.name), parameters: .init()) - + guard let sourceModule = target.sourceModule else { return } if self.buildInputs { log("Pre-building target '\(target.name)' before extracting sources...") try self.packageManager.build(.target(target.name), parameters: .init()) } - - if self.buildOutputs { - log("Post-building target '\(target.name)' to verify generated sources...") - try self.packageManager.build(.target(target.name), parameters: .init()) - } - + // Note: Target doesn't have a directoryURL counterpart to directory, // so we cannot eliminate this deprecation warning. let sourceDir = target.directory.string @@ -91,8 +90,6 @@ final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { .appending(path: "generated") .appending(path: "java") let outputDirectorySwift = context.pluginWorkDirectoryURL - .appending(path: "src") - .appending(path: "generated") .appending(path: "Sources") var arguments: [String] = [ @@ -108,28 +105,42 @@ final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { arguments.append(sourceDir) try runExtract(context: context, target: target, arguments: arguments) + + if self.buildOutputs { + // Building the *products* since we need to build the dylib that contains our newly generated sources, + // so just building the target again would not be enough. We build all products which we affected using + // our source generation, which usually would be just a product dylib with our library. + // + // In practice, we'll always want to build after generating; either here, + // or via some other task before we run any Java code, calling into Swift. + log("Post-extract building products with target '\(target.name)'...") + for product in context.package.products where product.targets.contains(where: { $0.id == target.id }) { + log("Post-extract building product '\(product.name)'...") + try self.packageManager.build(.product(product.name), parameters: .init()) + } + } } - + func runExtract(context: PluginContext, target: Target, arguments: [String]) throws { let process = Process() process.executableURL = try context.tool(named: "JExtractSwiftTool").url process.arguments = arguments - + do { - log("Execute: \(process.executableURL) \(arguments)") - + log("Execute: \(process.executableURL!.absoluteURL.relativePath) \(arguments.joined(separator: " "))") + try process.run() process.waitUntilExit() - + assert(process.terminationStatus == 0, "Process failed with exit code: \(process.terminationStatus)") } catch { - print("[swift-java][command] Failed to extract Java sources for target: '\(target.name); Error: \(error)") + print("[swift-java-command] Failed to extract Java sources for target: '\(target.name); Error: \(error)") } } - - func log(_ message: @autoclosure () -> String) { + + func log(_ message: @autoclosure () -> String, terminator: String = "\n") { if self.verbose { - print("[swift-java] \(message())") + print("[swift-java-command] \(message())", terminator: terminator) } } } diff --git a/Samples/JExtractPluginSampleApp/JExtractPluginSampleLibModule+SwiftJava.swiftdeps~ b/Samples/JExtractPluginSampleApp/JExtractPluginSampleLibModule+SwiftJava.swiftdeps~ new file mode 100644 index 0000000000000000000000000000000000000000..49d7039f1b0a00afae8c748dd8207121b6b15552 GIT binary patch literal 2100 zcmc(gzfaph6vtlLA{wEAQI|R|HQZYh8mi*(rci;E9ukY;IN?l3u03rZzsqg(k zRX7gC0TQl)?AK5{4rdoupc!RfZ+e+M>bSaPW=fKr$+}U2+-O#YQP1Tm=d+zmE_rUZ>2P>BEsMN)qad#ySE-Tx5+r_%68b0gQ zj`5`7rA=(c)M8ALA-YODW`v73BC3K!Sifb<^r*@D)aV{HESoy`y=58dHql3gHQFS; z;Yl6U8)dNXI@r(~Lpt0hs^xaD?a+wc%MY8ocy3;jaf~`NX&A@EX_tyL&)OR_ACJAA z2A(G4tsBH^S`PDIji~F&hGbs{Zp}%ly&a_^D9P1Yf6d|Um^yRJX%dx2?|VNP>jQhc zsd$cVZq*%&tqPaj-8S;SKJO-De;A+dhx~`69bmg6Ncn}3wGeLv(I!`b&FJprgPCPP`HGI{k{E|#@&abaF= z7Mlf?YQ0?tfPonf!Zw|0noK-3o#AF=c>x7kQY4u` zP9BFNhaNcQ;C49l*!ILB2Up2~1IPX${R24kNe&F5?G!W2#GYC0zMtQI?@7C*#d3uc z03-k)-0$7Pg18$)PW>84yTRoA#DcWPXLfjT2gl&`*GGZ$XFI)HKofDFm-Yhs zV?pc&^kqA}!>2m|JqRR}%$Ht>`)%oEK|JKshlR-lK6BibUh(2y@cNt3qaYse;!!|f zwWVW}Vz9Qf&u8`n>EJYmx;TlZ=(>+;jC7)adn*8&JOE#Bb(6|3EiqZ-r=Q%6z2R<+ z4he(YZ|V?gLFI5*a2%5*E6xB*bqJm0X^$}ZDcomo1=ItV5}?*4!5 zE1N%0NKv>3@D8vr23ECV;Bye5&O$^u9{~8)@3iztudIxKPMCv!l5 z@UZ%H!?!(~>UN#cs9Kq@T(nmhkNz7xY#=Jvnub}Y`a07$?56L%_4T$bU)!P;yXor8 zqtz&1B@z}Z>qf()hNYWL%+;{#O_RyJM?-TE(*|M+o72!WADhyn%OR?qtYN;QZTZZ? zJSDctOxs~DUBV93*EQqBu*6oirs>ODx__dfP0yvKQR|_j4NbSbE$X-|MDJ&M$vv8# zmK7R7?R9TYPBpiYpJEwIZ?a6}dsYns>kn_))cjT3MGhWm`s2#7?7S76*;A$T-HAE` zC42Rab9=ZrOamEnSG8WRJ?jta6?0py_-@qxHg<)Fv$I70FV9c=!+te9Uk>s+oV)$u zzG}|#mC>87i(i}Ve(1*UQh&Iwi0^*ko>DEm-`(g_yIYo(IznC3~0pWoNK!$)M zfD8i}0dft}{6F|NKJpE$b0ci0t;8!?kaeo1Hfa}6CKnvypQYxr$`yrJpN z;Y#p0#DBsp?G?O&Enuon2h>=(3@V>Wfx?ZAA(BJ_mLML7bgRw79d&_&k Swift.Bool { + lhs.parent?.originalSwiftType.id == rhs.parent?.originalSwiftType.id && + lhs.swiftDecl.id == rhs.swiftDecl.id + } +} + public enum VariableAccessorKind { case get case set @@ -300,7 +311,7 @@ public struct ImportedVariable: ImportedDecl, CustomStringConvertible { case .set: let newValueParam: FunctionParameterSyntax = "_ newValue: \(self.returnType.cCompatibleSwiftType)" - var funcDecl = ImportedFunc( + let funcDecl = ImportedFunc( module: self.module, decl: self.syntax!, parent: self.parentName, diff --git a/Sources/JExtractSwift/Swift2Java.swift b/Sources/JExtractSwift/Swift2Java.swift index 6d864449..315633b8 100644 --- a/Sources/JExtractSwift/Swift2Java.swift +++ b/Sources/JExtractSwift/Swift2Java.swift @@ -41,9 +41,8 @@ public struct SwiftToJava: ParsableCommand { help: "Name of the Swift module to import (and the swift interface files belong to)") var swiftModule: String - // TODO: Once we ship this, make this `.warning` by default @Option(name: .shortAndLong, help: "Configure the level of lots that should be printed") - var logLevel: Logger.Level = .notice + var logLevel: Logger.Level = .info @Argument(help: "The Swift files or directories to recursively export to Java.") var input: [String] diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift index bb26f93d..e5aea10c 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift @@ -659,10 +659,10 @@ extension Swift2JavaTranslator { _ printer: inout CodePrinter, _ decl: ImportedFunc, accessorKind: VariableAccessorKind? = nil ) { - var thunkName = SwiftKitPrinting.Names.functionThunk( - thunkNameRegistry: &self.thunkNameRegistry, - module: self.swiftModuleName, function: decl) - thunkName = thunkNameRegistry.deduplicate(name: thunkName) +// var thunkName = SwiftKitPrinting.Names.functionThunk( +// thunkNameRegistry: &self.thunkNameRegistry, +// module: self.swiftModuleName, function: decl) + let thunkName = thunkNameRegistry.functionThunkName(module: self.swiftModuleName, decl: decl) printer.print( """ public static final MemorySegment \(accessorKind.renderAddrFieldName) = diff --git a/Sources/JExtractSwift/SwiftKit+Printing.swift b/Sources/JExtractSwift/SwiftKit+Printing.swift index 1f9a4ecd..ed7d05d6 100644 --- a/Sources/JExtractSwift/SwiftKit+Printing.swift +++ b/Sources/JExtractSwift/SwiftKit+Printing.swift @@ -41,24 +41,4 @@ extension SwiftKitPrinting.Names { "swiftjava_getType_\(module)_\(nominal.swiftTypeName)" } - static func functionThunk( - thunkNameRegistry: inout ThunkNameRegistry, - module: String, function: ImportedFunc) -> String { - let params = function.effectiveParameters(paramPassingStyle: .swiftThunkSelf) - var paramsPart = "" - if !params.isEmpty { - paramsPart = "_" + params.map { param in - param.firstName ?? "_" - }.joined(separator: "_") - } - - let name = - if let parent = function.parent { - "swiftjava_\(module)_\(parent.swiftTypeName)_\(function.baseIdentifier)\(paramsPart)" - } else { - "swiftjava_\(module)_\(function.baseIdentifier)\(paramsPart)" - } - - return thunkNameRegistry.deduplicate(name: name) - } } diff --git a/Sources/JExtractSwift/SwiftThunkTranslator.swift b/Sources/JExtractSwift/SwiftThunkTranslator.swift index a3010490..57628f09 100644 --- a/Sources/JExtractSwift/SwiftThunkTranslator.swift +++ b/Sources/JExtractSwift/SwiftThunkTranslator.swift @@ -76,10 +76,8 @@ struct SwiftThunkTranslator { fatalError("Cannot render initializer accessor if init function has no parent! Was: \(function)") } - let funcName = SwiftKitPrinting.Names.functionThunk( - thunkNameRegistry: &self.st.thunkNameRegistry, - module: st.swiftModuleName, - function: function) + let thunkName = self.st.thunkNameRegistry.functionThunkName( + module: st.swiftModuleName, decl: function) // FIXME: handle in thunk: return types // FIXME: handle in thunk: parameters @@ -87,8 +85,8 @@ struct SwiftThunkTranslator { return [ """ - @_cdecl("\(raw: funcName)") - public func \(raw: funcName)(\(raw: st.renderSwiftParamDecls(function, paramPassingStyle: nil))) -> Any /* \(raw: parent.swiftTypeName) */ { + @_cdecl("\(raw: thunkName)") + public func \(raw: thunkName)(\(raw: st.renderSwiftParamDecls(function, paramPassingStyle: nil))) -> Any /* \(raw: parent.swiftTypeName) */ { \(raw: parent.swiftTypeName)(\(raw: st.renderForwardSwiftParams(function, paramPassingStyle: nil))) } """ @@ -98,10 +96,11 @@ struct SwiftThunkTranslator { func render(forFunc decl: ImportedFunc) -> [DeclSyntax] { st.log.trace("Rendering thunks for: \(decl.baseIdentifier)") - let funcName = SwiftKitPrinting.Names.functionThunk( - thunkNameRegistry: &st.thunkNameRegistry, - module: st.swiftModuleName, - function: decl) +// let funcName = SwiftKitPrinting.Names.functionThunk( +// thunkNameRegistry: &st.thunkNameRegistry, +// module: st.swiftModuleName, +// function: decl) + let funcName = st.thunkNameRegistry.functionThunkName(module: st.swiftModuleName, decl: decl) // Do we need to pass a self parameter? let paramPassingStyle: SelfParameterVariant? diff --git a/Sources/JExtractSwift/ThunkNameRegistry.swift b/Sources/JExtractSwift/ThunkNameRegistry.swift index 5cf49dae..2e716ff7 100644 --- a/Sources/JExtractSwift/ThunkNameRegistry.swift +++ b/Sources/JExtractSwift/ThunkNameRegistry.swift @@ -17,18 +17,45 @@ package struct ThunkNameRegistry { /// Maps base names such as "swiftjava_Module_Type_method_a_b_c" to the number of times we've seen them. /// This is used to de-duplicate symbols as we emit them. - private var baseNames: [String: Int] = [:] + private var registry: [ImportedFunc: String] = [:] + private var duplicateNames: [String: Int] = [:] package init() {} - package mutating func deduplicate(name: String) -> String { - var emittedCount = self.baseNames[name, default: 0] - defer { self.baseNames[name] = emittedCount + 1 } + package mutating func functionThunkName( + module: String, decl: ImportedFunc, + file: String = #fileID, line: UInt = #line) -> String { + if let existingName = self.registry[decl] { + return existingName + } - if emittedCount == 0 { - return name // first occurrence of a name we keep as-is - } else { - return "\(name)$\(emittedCount)" + let params = decl.effectiveParameters(paramPassingStyle: .swiftThunkSelf) + var paramsPart = "" + if !params.isEmpty { + paramsPart = "_" + params.map { param in + param.firstName ?? "_" + }.joined(separator: "_") } + + let name = + if let parent = decl.parent { + "swiftjava_\(module)_\(parent.swiftTypeName)_\(decl.baseIdentifier)\(paramsPart)" + } else { + "swiftjava_\(module)_\(decl.baseIdentifier)\(paramsPart)" + } + + let emittedCount = self.duplicateNames[name, default: 0] + defer { self.duplicateNames[name] = emittedCount + 1 } + + let deduplicatedName = + if emittedCount == 0 { + name // first occurrence of a name we keep as-is + } else { + "\(name)$\(emittedCount)" + } + + // Store the name we assigned to this specific decl. + self.registry[decl] = deduplicatedName + return deduplicatedName } } diff --git a/docker/install_jdk.sh b/docker/install_jdk.sh index 0d63eaf9..0a0a7de2 100755 --- a/docker/install_jdk.sh +++ b/docker/install_jdk.sh @@ -75,11 +75,7 @@ fi mkdir -p /usr/lib/jvm/ mv jdk.tar.gz /usr/lib/jvm/ -cd /usr/lib/jvm/ -ls tar xzvf jdk.tar.gz && rm jdk.tar.gz -ls -find . -depth -maxdepth 1 -type d mv "$(find . -depth -maxdepth 1 -type d | head -n1)" default-jdk echo "JAVA_HOME = /usr/lib/jvm/default-jdk" From 7f9a9865cb8730ce588fdf47ae2c99ce7068f98a Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Mon, 11 Nov 2024 22:05:36 +0900 Subject: [PATCH 5/8] lots of fixing around how we deal with config and library loading Change how we do static initialization, to automatically load library as we first use a class from it! rename verify step fix variable import test for new scheme gitignore .d and similar files split out samples into separate job ci: reorganize jobs include objc workaround for linux dont rebuild twice to get better output build dependency fixes in gradle: depend on SwiftKit as well lib paths: workaround for swiftly installed 6.0.2 lib path ignore .idea directory --- .github/actions/prepare_env/action.yml | 44 ++++++ .github/scripts/validate_samples.sh | 9 +- .github/workflows/pull_request.yml | 125 +++++------------- .gitignore | 6 + .licenseignore | 1 + ...d-logic.java-common-conventions.gradle.kts | 35 +++-- .../JExtractSwiftCommandPlugin.swift | 54 +++++--- .../JExtractSwiftPlugin.swift | 2 - ...PluginSampleLibModule+SwiftJava.swiftdeps~ | Bin 2100 -> 0 bytes .../MyCoolSwiftClass+SwiftJava.swiftdeps~ | Bin 2712 -> 0 bytes .../MyCoolSwiftClass.swift | 9 ++ Samples/JExtractPluginSampleApp/build.gradle | 38 +++++- .../JExtractPluginSampleApp/ci-validate.sh | 0 .../swift/JExtractPluginSampleMain.java | 1 + Samples/JavaProbablyPrime/ci-validate.sh | 3 + Samples/SwiftKitSampleApp/Package.swift | 3 + .../MySwiftLibrary/MySwiftLibrary.swift | 17 +-- ...JExtractSwift.config => swift-java.config} | 0 Samples/SwiftKitSampleApp/build.gradle | 53 ++++++-- .../swift/swiftkit/JavaToSwiftBenchmark.java | 3 +- .../com/example/swift/HelloJava2Swift.java | 25 ++-- .../{generated => }/MySwiftClassTest.java | 38 +++--- ...oduleTest.java => MySwiftLibraryTest.java} | 30 ++--- .../org/swift/swiftkit/MySwiftClassTest.java | 10 +- .../org/swift/swiftkit/SwiftArenaTest.java | 11 +- .../ExampleSwiftLibrary/MySwiftLibrary.swift | 9 ++ Sources/JExtractSwift/Swift2Java.swift | 2 +- .../Swift2JavaTranslator+Printing.swift | 86 +++++++----- Sources/JExtractSwift/Swift2JavaVisitor.swift | 8 +- .../JExtractSwift/SwiftThunkTranslator.swift | 44 +++--- Sources/JExtractSwift/ThunkNameRegistry.swift | 4 + .../Java2SwiftLib/JavaClassTranslator.swift | 4 +- SwiftKit/build.gradle | 29 ++++ .../java/org/swift/swiftkit/SwiftKit.java | 13 +- .../swiftkit/SwiftRuntimeMetadataTest.java | 1 + .../JExtractSwiftTests/MethodThunkTests.swift | 4 +- .../ThunkNameRegistryTests.swift | 29 ---- .../VariableImportTests.swift | 16 ++- buildSrc/build.gradle | 9 ++ .../swift/swiftkit/gradle/BuildUtils.groovy | 20 ++- docker/Dockerfile | 4 +- docker/install_jdk.sh | 1 + settings.gradle | 1 - 43 files changed, 477 insertions(+), 324 deletions(-) create mode 100644 .github/actions/prepare_env/action.yml delete mode 100644 Samples/JExtractPluginSampleApp/JExtractPluginSampleLibModule+SwiftJava.swiftdeps~ delete mode 100644 Samples/JExtractPluginSampleApp/MyCoolSwiftClass+SwiftJava.swiftdeps~ mode change 100644 => 100755 Samples/JExtractPluginSampleApp/ci-validate.sh create mode 100755 Samples/JavaProbablyPrime/ci-validate.sh rename Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/{JExtractSwift.config => swift-java.config} (100%) rename Samples/SwiftKitSampleApp/src/test/java/com/example/swift/{generated => }/MySwiftClassTest.java (56%) rename Samples/SwiftKitSampleApp/src/test/java/com/example/swift/{generated/GeneratedJavaKitExampleModuleTest.java => MySwiftLibraryTest.java} (59%) delete mode 100644 Tests/JExtractSwiftTests/ThunkNameRegistryTests.swift diff --git a/.github/actions/prepare_env/action.yml b/.github/actions/prepare_env/action.yml new file mode 100644 index 00000000..ebcc8aed --- /dev/null +++ b/.github/actions/prepare_env/action.yml @@ -0,0 +1,44 @@ +name: 'Swift Java CI Env' +description: 'Prepare the CI environment by installing Swift and selected JDK etc.' + +runs: + using: composite + steps: + - name: Install System Dependencies + run: apt-get -qq update && apt-get -qq install -y make curl wget libjemalloc2 libjemalloc-dev + shell: bash + - name: Cache JDK + id: cache-jdk + uses: actions/cache@v4 + continue-on-error: true + with: + path: /usr/lib/jvm/default-jdk/ + key: ${{ runner.os }}-jdk-${{ matrix.jdk_vendor }}-${{ hashFiles('/usr/lib/jvm/default-jdk/*') }} + restore-keys: | + ${{ runner.os }}-jdk- + - name: Install JDK + if: steps.cache-jdk.outputs.cache-hit != 'true' + run: "bash -xc 'JDK_VENDOR=${{ matrix.jdk_vendor }} ./docker/install_jdk.sh'" + shell: bash + # TODO: not using setup-java since incompatible with the swiftlang/swift base image + # - name: Install Untested Nightly Swift + # run: "bash -xc './docker/install_untested_nightly_swift.sh'" + - name: Cache local Gradle repository + uses: actions/cache@v4 + continue-on-error: true + with: + path: | + /root/.gradle/caches + /root/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('*/*.gradle*', 'settings.gradle') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Cache local SwiftPM repository + uses: actions/cache@v4 + continue-on-error: true + with: + path: /__w/swift-java/swift-java/.build/checkouts + key: ${{ runner.os }}-swiftpm-cache-${{ hashFiles('Package.swift') }} + restore-keys: | + ${{ runner.os }}-swiftpm-cache + ${{ runner.os }}-swiftpm- diff --git a/.github/scripts/validate_samples.sh b/.github/scripts/validate_samples.sh index c7a1223f..70f917eb 100755 --- a/.github/scripts/validate_samples.sh +++ b/.github/scripts/validate_samples.sh @@ -1,7 +1,7 @@ #!/bin/bash # shellcheck disable=SC2034 -declare -r GREEN='\033[0;31m' +declare -r GREEN='\033[0;32m' declare -r BOLD='\033[1m' declare -r RESET='\033[0m' @@ -15,7 +15,7 @@ for samplePackage in ${SAMPLE_PACKAGES} ; do echo "" echo "" echo "========================================================================" - printf "Validate sample: '${BOLD}%s${RESET}' using: " "$sampleDir" + printf "Validate sample '${BOLD}%s${RESET}' using: " "$sampleDir" cd "$sampleDir" || exit if [[ $(find . -name ${CI_VALIDATE_SCRIPT} -maxdepth 1) ]]; then echo -e "Custom ${BOLD}${CI_VALIDATE_SCRIPT}${RESET} script..." @@ -28,9 +28,10 @@ for samplePackage in ${SAMPLE_PACKAGES} ; do swift build || exit fi + echo -e "Validated sample '${BOLD}${sampleDir}${RESET}': ${BOLD}passed${RESET}." cd - || exit done - +echo printf "Done validating samples: " -echo "${GREEN}done${RESET}." +echo -e "${GREEN}done${RESET}." diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 44e0b0a2..8039b572 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -30,50 +30,14 @@ jobs: JAVA_HOME: "/usr/lib/jvm/default-jdk" steps: - uses: actions/checkout@v4 - - name: Install System Dependencies - run: apt-get -qq update && apt-get -qq install -y make curl wget libjemalloc2 libjemalloc-dev - - name: Cache JDK - id: cache-jdk - uses: actions/cache@v4 - continue-on-error: true - with: - path: /usr/lib/jvm/default-jdk/ - key: ${{ runner.os }}-jdk-${{ matrix.jdk_vendor }}-${{ hashFiles('/usr/lib/jvm/default-jdk/*') }} - restore-keys: | - ${{ runner.os }}-jdk- - - name: Install JDK - if: steps.cache-jdk.outputs.cache-hit != 'true' - run: "bash -xc 'JDK_VENDOR=${{ matrix.jdk_vendor }} ./docker/install_jdk.sh'" - # TODO: not using setup-java since incompatible with the swiftlang/swift base image - # - name: Install Untested Nightly Swift - # run: "bash -xc './docker/install_untested_nightly_swift.sh'" - - name: Cache local Gradle repository - uses: actions/cache@v4 - continue-on-error: true - with: - path: | - /root/.gradle/caches - /root/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('*/*.gradle*', 'settings.gradle') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Cache local SwiftPM repository - uses: actions/cache@v4 - continue-on-error: true - with: - path: /__w/swift-java/swift-java/.build/checkouts - key: ${{ runner.os }}-swiftpm-cache-${{ hashFiles('Package.swift') }} - restore-keys: | - ${{ runner.os }}-swiftpm-cache - ${{ runner.os }}-swiftpm- - # run the actual build - - name: Gradle build - run: | - ./gradlew build -x test --no-daemon # just build - ./gradlew build --info --no-daemon - - name: Gradle build (benchmarks) - run: | - ./gradlew compileJmh --info --no-daemon + - name: Prepare CI Environment + uses: ./.github/actions/prepare_env + - name: Gradle :SwiftKit:build + run: ./gradlew build -x test + - name: Gradle :SwiftKit:check + run: ./gradlew :SwiftKit:check --info + - name: Gradle compile JMH benchmarks + run: ./gradlew compileJmh --info test-swift: name: Swift tests (swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}} os:${{ matrix.os_version }}) @@ -91,50 +55,33 @@ jobs: JAVA_HOME: "/usr/lib/jvm/default-jdk" steps: - uses: actions/checkout@v4 - - name: Install System Dependencies - run: apt-get -qq update && apt-get -qq install -y make curl wget libjemalloc2 libjemalloc-dev - - name: Cache JDK - id: cache-jdk - uses: actions/cache@v4 - continue-on-error: true - with: - path: /usr/lib/jvm/default-jdk/ - key: ${{ runner.os }}-jdk-${{ matrix.jdk_vendor }}-${{ hashFiles('/usr/lib/jvm/default-jdk/*') }} - restore-keys: | - ${{ runner.os }}-jdk- - - name: Install JDK - if: steps.cache-jdk.outputs.cache-hit != 'true' - run: "bash -xc 'JDK_VENDOR=${{ matrix.jdk_vendor }} ./docker/install_jdk.sh'" - # TODO: not using setup-java since incompatible with the swiftlang/swift base image - # - name: Install Untested Nightly Swift - # run: "bash -xc './docker/install_untested_nightly_swift.sh'" - - name: Cache local Gradle repository - uses: actions/cache@v4 - continue-on-error: true - with: - path: | - /root/.gradle/caches - /root/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('*/*.gradle*', 'settings.gradle') }} - restore-keys: | - ${{ runner.os }}-gradle- - - name: Cache local SwiftPM repository - uses: actions/cache@v4 - continue-on-error: true - with: - path: /__w/swift-java/swift-java/.build/checkouts - key: ${{ runner.os }}-swiftpm-cache-${{ hashFiles('Package.swift') }} - restore-keys: | - ${{ runner.os }}-swiftpm-cache - ${{ runner.os }}-swiftpm- - # run the actual build - # - name: Generate sources (make) (Temporary) - # # TODO: this should be triggered by the respective builds - # run: "make jextract-generate" - - name: Test Swift + - name: Prepare CI Environment + uses: ./.github/actions/prepare_env + - name: Swift Build + run: "swift build --build-tests" + - name: Swift Test run: "swift test" - - name: Build (Swift) Sample Apps + + verify-samples: + name: Verify Samples (swift:${{ matrix.swift_version }} jdk:${{matrix.jdk_vendor}} os:${{ matrix.os_version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # swift_version: ['nightly-main'] + swift_version: ['6.0.2'] + os_version: ['jammy'] + jdk_vendor: ['Corretto'] + container: + image: ${{ (contains(matrix.swift_version, 'nightly') && 'swiftlang/swift') || 'swift' }}:${{ matrix.swift_version }}-${{ matrix.os_version }} + env: + JAVA_HOME: "/usr/lib/jvm/default-jdk" + steps: + - uses: actions/checkout@v4 + - name: Prepare CI Environment + uses: ./.github/actions/prepare_env + - name: Verify Samples (All) run: .github/scripts/validate_samples.sh - # TODO: Benchmark compile crashes in CI, enable when nightly toolchains in better shape. - # - name: Build (Swift) Benchmarks - # run: "swift package --package-path Benchmarks/ benchmark list" + # TODO: Benchmark compile crashes in CI, enable when nightly toolchains in better shape. + # - name: Build (Swift) Benchmarks + # run: "swift package --package-path Benchmarks/ benchmark list" diff --git a/.gitignore b/.gitignore index 5055eec2..11bbeceb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .build +.idea Packages xcuserdata/ DerivedData/ @@ -33,3 +34,8 @@ Package.resolved # Ignore files generated by jextract, we always can re-generate them */**/src/generated/java/**/* + +*/**/*.d +*/**/*.o +*/**/*.swiftdeps +*/**/*.swiftdeps~ diff --git a/.licenseignore b/.licenseignore index 0825283e..c5dfe3d2 100644 --- a/.licenseignore +++ b/.licenseignore @@ -39,3 +39,4 @@ gradlew gradlew.bat **/gradlew **/gradlew.bat +**/ci-validate.sh diff --git a/BuildLogic/src/main/kotlin/build-logic.java-common-conventions.gradle.kts b/BuildLogic/src/main/kotlin/build-logic.java-common-conventions.gradle.kts index e3fc499f..136da7d3 100644 --- a/BuildLogic/src/main/kotlin/build-logic.java-common-conventions.gradle.kts +++ b/BuildLogic/src/main/kotlin/build-logic.java-common-conventions.gradle.kts @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import java.util.* +import java.io.* plugins { java @@ -44,7 +45,7 @@ tasks.withType(JavaCompile::class).forEach { // FIXME: cannot share definition with 'buildSrc' so we duplicated the impl here -fun javaLibraryPaths(): List { +fun javaLibraryPaths(dir: File): List { val osName = System.getProperty("os.name") val osArch = System.getProperty("os.arch") val isLinux = osName.lowercase(Locale.getDefault()).contains("linux") @@ -52,15 +53,15 @@ fun javaLibraryPaths(): List { return listOf( if (isLinux) { if (osArch.equals("x86_64") || osArch.equals("amd64")) { - "$rootDir/.build/x86_64-unknown-linux-gnu/debug/" + "$dir/.build/x86_64-unknown-linux-gnu/debug/" } else { - "$rootDir/.build/$osArch-unknown-linux-gnu/debug/" + "$dir/.build/$osArch-unknown-linux-gnu/debug/" } } else { if (osArch.equals("aarch64")) { - "$rootDir/.build/arm64-apple-macosx/debug/" + "$dir/.build/arm64-apple-macosx/debug/" } else { - "$rootDir/.build/$osArch-apple-macosx/debug/" + "$dir/.build/$osArch-apple-macosx/debug/" } }, if (isLinux) { @@ -68,6 +69,12 @@ fun javaLibraryPaths(): List { } else { // assume macOS "/usr/lib/swift/" + }, + if (isLinux) { + System.getProperty("user.home") + "/.local/share/swiftly/toolchains/6.0.2/usr/lib/swift/linux" + } else { + // assume macOS + "/usr/lib/swift/" } ) } @@ -79,7 +86,9 @@ tasks.test { "--enable-native-access=ALL-UNNAMED", // Include the library paths where our dylibs are that we want to load and call - "-Djava.library.path=" + javaLibraryPaths().joinToString(File.pathSeparator) + "-Djava.library.path=" + + (javaLibraryPaths(rootDir) + javaLibraryPaths(project.projectDir)) + .joinToString(File.pathSeparator) ) } @@ -88,17 +97,3 @@ tasks.withType { this.showStandardStreams = true } } - - -// TODO: This is a crude workaround, we'll remove 'make' soon and properly track build dependencies -// val buildSwiftJExtract = tasks.register("buildMake") { -// description = "Triggers 'make' build" -// -// workingDir(rootDir) -// commandLine("make") -// } -// -// tasks.build { -// dependsOn(buildSwiftJExtract) -// } - diff --git a/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift b/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift index ee22f36d..f09c13e2 100644 --- a/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift +++ b/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift @@ -30,7 +30,7 @@ final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { /// This helps verify that the generated output is correct, and won't miscompile on the next build. var buildOutputs: Bool = true - func createBuildCommands(context: PackagePlugin.PluginContext, target: any PackagePlugin.Target) async throws -> [PackagePlugin.Command] { + func createBuildCommands(context: PluginContext, target: any Target) async throws -> [Command] { // FIXME: This is not a build plugin but SwiftPM forces us to impleme the protocol anyway? rdar://139556637 return [] } @@ -38,9 +38,6 @@ final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { func performCommand(context: PluginContext, arguments: [String]) throws { // Plugin can't have dependencies, so we have some naive argument parsing instead: self.verbose = arguments.contains("-v") || arguments.contains("--verbose") - if !self.verbose { - fatalError("Plugin should be verbose") - } let selectedTargets: [String] = if let last = arguments.lastIndex(where: { $0.starts(with: "-")}), @@ -52,30 +49,23 @@ final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { for target in context.package.targets { guard let configPath = getSwiftJavaConfig(target: target) else { - log("Skipping target '\(target.name), has no 'swift-java.config' file") + log("Skipping target '\(target.name)', has no 'swift-java.config' file") continue } do { - print("[swift-java] Extracting Java wrappers from target: '\(target.name)'...") - try performCommand(context: context, target: target, arguments: arguments) + print("[swift-java-command] Extracting Java wrappers from target: '\(target.name)'...") + try performCommand(context: context, target: target, extraArguments: arguments) } catch { - print("[swift-java] error: Failed to extract from target '\(target.name)': \(error)") + print("[swift-java-command] error: Failed to extract from target '\(target.name)': \(error)") } + } + print("[swift-java-command] Generating sources: " + "done".green + ".") } - /// Perform the command on a specific target. - func performCommand(context: PluginContext, target: Target, arguments: [String]) throws { - // Make sure the target can builds properly - try self.packageManager.build(.target(target.name), parameters: .init()) - - guard let sourceModule = target.sourceModule else { return } - - if self.buildInputs { - log("Pre-building target '\(target.name)' before extracting sources...") - try self.packageManager.build(.target(target.name), parameters: .init()) - } + func prepareJExtractArguments(context: PluginContext, target: Target) throws -> [String] { + guard let sourceModule = target.sourceModule else { return [] } // Note: Target doesn't have a directoryURL counterpart to directory, // so we cannot eliminate this deprecation warning. @@ -104,8 +94,25 @@ final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { ] arguments.append(sourceDir) + return arguments + } + + /// Perform the command on a specific target. + func performCommand(context: PluginContext, target: Target, extraArguments _: [String]) throws { + // Make sure the target can builds properly + try self.packageManager.build(.target(target.name), parameters: .init()) + + guard let sourceModule = target.sourceModule else { return } + + if self.buildInputs { + log("Pre-building target '\(target.name)' before extracting sources...") + try self.packageManager.build(.target(target.name), parameters: .init()) + } + + let arguments = try prepareJExtractArguments(context: context, target: target) + try runExtract(context: context, target: target, arguments: arguments) - + if self.buildOutputs { // Building the *products* since we need to build the dylib that contains our newly generated sources, // so just building the target again would not be enough. We build all products which we affected using @@ -144,3 +151,10 @@ final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { } } } + +// Mini coloring helper, since we cannot have dependencies we keep it minimal here +extension String { + var green: String { + "\u{001B}[0;32m" + "\(self)" + "\u{001B}[0;0m" + } +} \ No newline at end of file diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index 2d1be58c..f23a1dbf 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -34,8 +34,6 @@ struct JExtractSwiftBuildToolPlugin: BuildToolPlugin { .appending(path: "generated") .appending(path: "java") let outputDirectorySwift = context.pluginWorkDirectoryURL - .appending(path: "src") - .appending(path: "generated") .appending(path: "Sources") var arguments: [String] = [ diff --git a/Samples/JExtractPluginSampleApp/JExtractPluginSampleLibModule+SwiftJava.swiftdeps~ b/Samples/JExtractPluginSampleApp/JExtractPluginSampleLibModule+SwiftJava.swiftdeps~ deleted file mode 100644 index 49d7039f1b0a00afae8c748dd8207121b6b15552..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2100 zcmc(gzfaph6vtlLA{wEAQI|R|HQZYh8mi*(rci;E9ukY;IN?l3u03rZzsqg(k zRX7gC0TQl)?AK5{4rdoupc!RfZ+e+M>bSaPW=fKr$+}U2+-O#YQP1Tm=d+zmE_rUZ>2P>BEsMN)qad#ySE-Tx5+r_%68b0gQ zj`5`7rA=(c)M8ALA-YODW`v73BC3K!Sifb<^r*@D)aV{HESoy`y=58dHql3gHQFS; z;Yl6U8)dNXI@r(~Lpt0hs^xaD?a+wc%MY8ocy3;jaf~`NX&A@EX_tyL&)OR_ACJAA z2A(G4tsBH^S`PDIji~F&hGbs{Zp}%ly&a_^D9P1Yf6d|Um^yRJX%dx2?|VNP>jQhc zsd$cVZq*%&tqPaj-8S;SKJO-De;A+dhx~`69bmg6Ncn}3wGeLv(I!`b&FJprgPCPP`HGI{k{E|#@&abaF= z7Mlf?YQ0?tfPonf!Zw|0noK-3o#AF=c>x7kQY4u` zP9BFNhaNcQ;C49l*!ILB2Up2~1IPX${R24kNe&F5?G!W2#GYC0zMtQI?@7C*#d3uc z03-k)-0$7Pg18$)PW>84yTRoA#DcWPXLfjT2gl&`*GGZ$XFI)HKofDFm-Yhs zV?pc&^kqA}!>2m|JqRR}%$Ht>`)%oEK|JKshlR-lK6BibUh(2y@cNt3qaYse;!!|f zwWVW}Vz9Qf&u8`n>EJYmx;TlZ=(>+;jC7)adn*8&JOE#Bb(6|3EiqZ-r=Q%6z2R<+ z4he(YZ|V?gLFI5*a2%5*E6xB*bqJm0X^$}ZDcomo1=ItV5}?*4!5 zE1N%0NKv>3@D8vr23ECV;Bye5&O$^u9{~8)@3iztudIxKPMCv!l5 z@UZ%H!?!(~>UN#cs9Kq@T(nmhkNz7xY#=Jvnub}Y`a07$?56L%_4T$bU)!P;yXor8 zqtz&1B@z}Z>qf()hNYWL%+;{#O_RyJM?-TE(*|M+o72!WADhyn%OR?qtYN;QZTZZ? zJSDctOxs~DUBV93*EQqBu*6oirs>ODx__dfP0yvKQR|_j4NbSbE$X-|MDJ&M$vv8# zmK7R7?R9TYPBpiYpJEwIZ?a6}dsYns>kn_))cjT3MGhWm`s2#7?7S76*;A$T-HAE` zC42Rab9=ZrOamEnSG8WRJ?jta6?0py_-@qxHg<)Fv$I70FV9c=!+te9Uk>s+oV)$u zzG}|#mC>87i(i}Ve(1*UQh&Iwi0^*ko>DEm-`(g_yIYo(IznC3~0pWoNK!$)M zfD8i}0dft}{6F|NKJpE$b0ci0t;8!?kaeo1Hfa}6CKnvypQYxr$`yrJpN z;Y#p0#DBsp?G?O&Enuon2h>=(3@V>Wfx?ZAA(BJ_mLML7bgRw79d&_&k, -// _ nameLength: UInt, -// genericEnvironment: UnsafeRawPointer?, -// genericArguments: UnsafeRawPointer?) -// -> Any.Type? - // ==== Internal helpers private func p(_ msg: String, file: String = #fileID, line: UInt = #line, function: String = #function) { print("[swift][\(file):\(line)](\(function)) \(msg)") fflush(stdout) } + +#if os(Linux) +// FIXME: why do we need this workaround? +@_silgen_name("_objc_autoreleaseReturnValue") +public func _objc_autoreleaseReturnValue(a: Any) {} + +@_silgen_name("objc_autoreleaseReturnValue") +public func objc_autoreleaseReturnValue(a: Any) {} +#endif diff --git a/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/JExtractSwift.config b/Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/swift-java.config similarity index 100% rename from Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/JExtractSwift.config rename to Samples/SwiftKitSampleApp/Sources/MySwiftLibrary/swift-java.config diff --git a/Samples/SwiftKitSampleApp/build.gradle b/Samples/SwiftKitSampleApp/build.gradle index 3ded4929..ff9e1837 100644 --- a/Samples/SwiftKitSampleApp/build.gradle +++ b/Samples/SwiftKitSampleApp/build.gradle @@ -34,9 +34,10 @@ java { } } + // This is for development, when we edit the Swift swift-java project, the outputs of the generated sources may change. // Thus, we also need to watch and re-build the top level project. -def buildJExtractPlugin = tasks.register("swiftBuildJExtractPlugin", Exec) { +def compileSwiftJExtractPlugin = tasks.register("compileSwiftJExtractPlugin", Exec) { description = "Rebuild the swift-java root project" inputs.file(new File(rootDir, "Package.swift")) @@ -45,22 +46,26 @@ def buildJExtractPlugin = tasks.register("swiftBuildJExtractPlugin", Exec) { workingDir = rootDir commandLine "swift" - args "build" + args("build", + "--product", "SwiftKitSwift", + "--product", "JExtractSwiftPlugin", + "--product", "JExtractSwiftCommandPlugin") } -def buildSwift = tasks.register("swiftBuildProject", Exec) { +def jextract = tasks.register("jextract", Exec) { description = "Builds swift sources, including swift-java source generation" - dependsOn buildJExtractPlugin + dependsOn compileSwiftJExtractPlugin // only because we depend on "live developing" the plugin while using this project to test it inputs.file(new File(rootDir, "Package.swift")) inputs.dir(new File(rootDir, "Sources")) - inputs.file(layout.projectDirectory.file("Package.swift")) - inputs.dir(layout.projectDirectory.dir("Sources")) + inputs.file(new File(projectDir, "Package.swift")) + inputs.dir(new File(projectDir, "Sources")) // TODO: we can use package describe --type json to figure out which targets depend on JExtractSwiftPlugin and will produce outputs - // Avoid adding this directory, but create the expected one specifically for all targets which WILL produce sources because they have the plugin + // Avoid adding this directory, but create the expected one specifically for all targets + // which WILL produce sources because they have the plugin outputs.dir(layout.buildDirectory.dir("../.build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}")) File baseSwiftPluginOutputsDir = layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile @@ -68,6 +73,7 @@ def buildSwift = tasks.register("swiftBuildProject", Exec) { baseSwiftPluginOutputsDir.mkdirs() } Files.walk(layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile.toPath()).each { + // Add any Java sources generated by the plugin to our sourceSet if (it.endsWith("JExtractSwiftPlugin/src/generated/java")) { outputs.dir(it) } @@ -75,20 +81,40 @@ def buildSwift = tasks.register("swiftBuildProject", Exec) { workingDir = layout.projectDirectory commandLine "swift" - args ["package", "jextract"] + args("package", "jextract", "-v", "--log-level", "info") // TODO: pass log level from Gradle build } // Add the java-swift generated Java sources sourceSets { main { java { - srcDir(buildSwift) + srcDir(jextract) + } + } + test { + java { + srcDir(jextract) + } + } + jmh { + java { + srcDir(jextract) } } } tasks.build { - dependsOn(buildSwift) + dependsOn("jextract") +} + + +def cleanSwift = tasks.register("cleanSwift", Exec) { + workingDir = layout.projectDirectory + commandLine "swift" + args("package", "clean") +} +tasks.clean { + dependsOn("cleanSwift") } dependencies { @@ -111,7 +137,8 @@ application { // Include the library paths where our dylibs are that we want to load and call "-Djava.library.path=" + (BuildUtils.javaLibraryPaths(rootDir) + - BuildUtils.javaLibraryPaths(rootDir)).join(":"), + BuildUtils.javaLibraryPaths(project.projectDir)).join(":"), + // Enable tracing downcalls (to Swift) "-Djextract.trace.downcalls=true" @@ -120,6 +147,8 @@ application { jmh { jvmArgsAppend = [ - "-Djava.library.path=" + BuildUtils.javaLibraryPaths(rootDir).join(":"), + "-Djava.library.path=" + + (BuildUtils.javaLibraryPaths(rootDir) + + BuildUtils.javaLibraryPaths(project.projectDir)).join(":"), ] } diff --git a/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java b/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java index 5ce9ee6b..614697a3 100644 --- a/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java +++ b/Samples/SwiftKitSampleApp/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java @@ -26,8 +26,9 @@ import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.infra.Blackhole; -import com.example.swift.generated.MySwiftClass; +import com.example.swift.MySwiftClass; +@SuppressWarnings("unused") public class JavaToSwiftBenchmark { @State(Scope.Benchmark) diff --git a/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java b/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java index 9789eda5..af7345ff 100644 --- a/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java +++ b/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java @@ -15,7 +15,8 @@ package com.example.swift; // Import swift-extract generated sources -import com.example.swift.ExampleSwiftLibrary; + +import com.example.swift.MySwiftLibrary; import com.example.swift.MySwiftClass; // Import javakit/swiftkit support libraries @@ -23,31 +24,33 @@ import org.swift.swiftkit.SwiftKit; import org.swift.swiftkit.SwiftValueWitnessTable; -import java.lang.foreign.*; - public class HelloJava2Swift { public static void main(String[] args) { boolean traceDowncalls = Boolean.getBoolean("jextract.trace.downcalls"); System.out.println("Property: jextract.trace.downcalls = " + traceDowncalls); - System.out.printf("java.library.path = %s\n", System.getProperty("java.library.path")); + System.out.print("java.library.path = \n"); + for (var path : SwiftKit.getJavaLibraryPath().split(":")) { + System.out.println(" " + path); + } examples(); + } static void examples() { - ExampleSwiftLibrary.helloWorld(); + MySwiftLibrary.helloWorld(); - ExampleSwiftLibrary.globalTakeInt(1337); + MySwiftLibrary.globalTakeInt(1337); - MySwiftClass obj = new MySwiftClass(2222, 7777); + MySwiftClass obj = new MySwiftClass(2222, 7777); - SwiftKit.retain(obj.$memorySegment()); - System.out.println("[java] obj ref count = " + SwiftKit.retainCount(obj.$memorySegment())); + SwiftKit.retain(obj.$memorySegment()); + System.out.println("[java] obj ref count = " + SwiftKit.retainCount(obj.$memorySegment())); - obj.voidMethod(); - obj.takeIntMethod(42); + obj.voidMethod(); + obj.takeIntMethod(42); System.out.println("DONE."); } diff --git a/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/generated/MySwiftClassTest.java b/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java similarity index 56% rename from Samples/SwiftKitSampleApp/src/test/java/com/example/swift/generated/MySwiftClassTest.java rename to Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java index 7495ff19..fa17ef1a 100644 --- a/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/generated/MySwiftClassTest.java +++ b/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftClassTest.java @@ -12,33 +12,41 @@ // //===----------------------------------------------------------------------===// -package com.example.swift.generated; +package com.example.swift; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.junit.jupiter.api.condition.OS; +import org.swift.swiftkit.SwiftKit; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.io.File; +import java.util.stream.Stream; -public class MySwiftClassTest { +import static org.junit.jupiter.api.Assertions.*; - @BeforeAll - static void beforeAll() { - System.out.printf("java.library.path = %s\n", System.getProperty("java.library.path")); +public class MySwiftClassTest { - System.loadLibrary("swiftCore"); - System.loadLibrary("ExampleSwiftLibrary"); + void checkPaths(Throwable throwable) { + var paths = SwiftKit.getJavaLibraryPath().split(":"); + for (var path : paths) { + System.out.println("CHECKING PATH: " + path); + Stream.of(new File(path).listFiles()) + .filter(file -> !file.isDirectory()) + .forEach((file) -> { + System.out.println(" - " + file.getPath()); + }); + } - System.setProperty("jextract.trace.downcalls", "true"); + throw new RuntimeException(throwable); } @Test void test_MySwiftClass_voidMethod() { - MySwiftClass o = new MySwiftClass(12, 42); - o.voidMethod(); + try { + MySwiftClass o = new MySwiftClass(12, 42); + o.voidMethod(); + } catch (Throwable throwable) { + checkPaths(throwable); + } } @Test diff --git a/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/generated/GeneratedJavaKitExampleModuleTest.java b/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java similarity index 59% rename from Samples/SwiftKitSampleApp/src/test/java/com/example/swift/generated/GeneratedJavaKitExampleModuleTest.java rename to Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java index 9cceacf5..ffa90359 100644 --- a/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/generated/GeneratedJavaKitExampleModuleTest.java +++ b/Samples/SwiftKitSampleApp/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -12,46 +12,42 @@ // //===----------------------------------------------------------------------===// -package com.example.swift.generated; +package com.example.swift; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.DisabledOnOs; -import org.junit.jupiter.api.condition.OS; import org.swift.swiftkit.SwiftKit; +import java.util.Arrays; import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; -public class GeneratedJavaKitExampleModuleTest { - - @BeforeAll - static void beforeAll() { - System.out.println("java.library.path = " + SwiftKit.getJavaLibraryPath()); - System.out.println("java.library.path = " + SwiftKit.getJextractTraceDowncalls()); - } +public class MySwiftLibraryTest { @Test void call_helloWorld() { - ExampleSwiftLibrary.helloWorld(); + MySwiftLibrary.helloWorld(); - assertNotNull(ExampleSwiftLibrary.helloWorld$address()); + assertNotNull(MySwiftLibrary.helloWorld$address()); } @Test void call_globalTakeInt() { - ExampleSwiftLibrary.globalTakeInt(12); + MySwiftLibrary.globalTakeInt(12); - assertNotNull(ExampleSwiftLibrary.globalTakeInt$address()); + assertNotNull(MySwiftLibrary.globalTakeInt$address()); } @Test + @Disabled("Upcalls not yet implemented in new scheme") @SuppressWarnings({"Convert2Lambda", "Convert2MethodRef"}) void call_globalCallMeRunnable() { CountDownLatch countDownLatch = new CountDownLatch(3); - ExampleSwiftLibrary.globalCallMeRunnable(new Runnable() { + MySwiftLibrary.globalCallMeRunnable(new Runnable() { @Override public void run() { countDownLatch.countDown(); @@ -59,10 +55,10 @@ public void run() { }); assertEquals(2, countDownLatch.getCount()); - ExampleSwiftLibrary.globalCallMeRunnable(() -> countDownLatch.countDown()); + MySwiftLibrary.globalCallMeRunnable(() -> countDownLatch.countDown()); assertEquals(1, countDownLatch.getCount()); - ExampleSwiftLibrary.globalCallMeRunnable(countDownLatch::countDown); + MySwiftLibrary.globalCallMeRunnable(countDownLatch::countDown); assertEquals(0, countDownLatch.getCount()); } diff --git a/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/MySwiftClassTest.java b/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/MySwiftClassTest.java index 069a4e41..633d5f1c 100644 --- a/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/MySwiftClassTest.java +++ b/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/MySwiftClassTest.java @@ -15,19 +15,13 @@ package org.swift.swiftkit; import static org.junit.jupiter.api.Assertions.assertEquals; -import org.junit.jupiter.api.BeforeAll; + import org.junit.jupiter.api.Test; -import com.example.swift.generated.MySwiftClass; +import com.example.swift.MySwiftClass; public class MySwiftClassTest { - @BeforeAll - static void beforeAll() { - System.out.printf("java.library.path = %s\n", SwiftKit.getJavaLibraryPath()); - System.out.printf("jextract.trace.downcalls = %s\n", SwiftKit.getJextractTraceDowncalls()); - } - @Test void call_retain_retainCount_release() { var arena = SwiftArena.ofConfined(); diff --git a/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/SwiftArenaTest.java b/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/SwiftArenaTest.java index 4f4ab9df..ad514e1b 100644 --- a/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/SwiftArenaTest.java +++ b/Samples/SwiftKitSampleApp/src/test/java/org/swift/swiftkit/SwiftArenaTest.java @@ -14,24 +14,21 @@ package org.swift.swiftkit; -import com.example.swift.generated.MySwiftClass; +import com.example.swift.MySwiftClass; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; import org.swift.swiftkit.util.PlatformUtils; +import java.util.Arrays; +import java.util.stream.Collectors; + import static org.junit.jupiter.api.Assertions.*; import static org.swift.swiftkit.SwiftKit.*; import static org.swift.swiftkit.SwiftKit.retainCount; public class SwiftArenaTest { - @BeforeAll - static void beforeAll() { - System.out.printf("java.library.path = %s\n", SwiftKit.getJavaLibraryPath()); - System.out.printf("jextract.trace.downcalls = %s\n", SwiftKit.getJextractTraceDowncalls()); - } - static boolean isAmd64() { return PlatformUtils.isAmd64(); } diff --git a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift index 36e09b14..8376d9b6 100644 --- a/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift +++ b/Sources/ExampleSwiftLibrary/MySwiftLibrary.swift @@ -101,3 +101,12 @@ private func p(_ msg: String, file: String = #fileID, line: UInt = #line, functi print("[swift][\(file):\(line)](\(function)) \(msg)") fflush(stdout) } + +#if os(Linux) +// FIXME: why do we need this workaround? +@_silgen_name("_objc_autoreleaseReturnValue") +public func _objc_autoreleaseReturnValue(a: Any) {} + +@_silgen_name("objc_autoreleaseReturnValue") +public func objc_autoreleaseReturnValue(a: Any) {} +#endif diff --git a/Sources/JExtractSwift/Swift2Java.swift b/Sources/JExtractSwift/Swift2Java.swift index 315633b8..2167c391 100644 --- a/Sources/JExtractSwift/Swift2Java.swift +++ b/Sources/JExtractSwift/Swift2Java.swift @@ -107,6 +107,6 @@ extension Logger.Level: ExpressibleByArgument { func isDirectory(url: URL) -> Bool { var isDirectory: ObjCBool = false - FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) + _ = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) return isDirectory.boolValue } diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift index e5aea10c..7600753a 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift @@ -53,20 +53,18 @@ extension Swift2JavaTranslator { } public func writeSwiftThunkSources(outputDirectory: String, printer: inout CodePrinter) throws { - // ==== Globals - for decl in self.importedGlobalFuncs { - printSwiftThunkSources(&printer, decl: decl) - } - let moduleFilenameBase = "\(self.swiftModuleName)Module+SwiftJava" let moduleFilename = "\(moduleFilenameBase).swift" - log.info("Printing contents: \(moduleFilename)") do { + log.info("Printing contents: \(moduleFilename)") + + try printGlobalSwiftThunkSources(&printer) + if let outputFile = try printer.writeContents( outputDirectory: outputDirectory, javaPackagePath: nil, filename: moduleFilename) { - print("[swift-java] Generated: \(moduleFilenameBase.bold).java (at \(outputFile)") + print("[swift-java] Generated: \(moduleFilenameBase.bold).swift (at \(outputFile)") } } catch { log.warning("Failed to write to Swift thunks: \(moduleFilename)") @@ -93,6 +91,15 @@ extension Swift2JavaTranslator { } } + public func printGlobalSwiftThunkSources(_ printer: inout CodePrinter) throws { + let stt = SwiftThunkTranslator(self) + + for thunk in stt.renderGlobalThunks() { + printer.print(thunk) + printer.println() + } + } + public func printSwiftThunkSources(_ printer: inout CodePrinter, decl: ImportedFunc) { let stt = SwiftThunkTranslator(self) @@ -145,9 +152,6 @@ extension Swift2JavaTranslator { printImports(&printer) printModuleClass(&printer) { printer in - - printStaticLibraryLoad(&printer) - // TODO: print all "static" methods for decl in importedGlobalFuncs { printFunctionDowncallMethods(&printer, decl) @@ -161,9 +165,6 @@ extension Swift2JavaTranslator { printImports(&printer) printClass(&printer, decl) { printer in - // Ensure we have loaded the library where the Swift type was declared before we attempt to resolve types in Swift - printStaticLibraryLoad(&printer) - // Prepare type metadata, we're going to need these when invoking e.g. initializers so cache them in a static. // We call into source swift-java source generated accessors which give us the type of the Swift object: // TODO: seems we no longer need the mangled name per se, so avoiding such constant and downcall @@ -172,8 +173,20 @@ extension Swift2JavaTranslator { // SwiftKitPrinting.renderCallGetSwiftTypeMangledName(module: self.swiftModuleName, nominal: decl), // ";" // ) + + // We use a static field to abuse the initialization order such that by the time we get type metadata, + // we already have loaded the library where it will be obtained from. printer.printParts( """ + @SuppressWarnings("unused") + private static final boolean INITIALIZED_LIBS = initializeLibs(); + static boolean initializeLibs() { + System.loadLibrary(SwiftKit.STDLIB_DYLIB_NAME); + System.loadLibrary("SwiftKitSwift"); + System.loadLibrary(LIB_NAME); + return true; + } + public static final SwiftAnyType TYPE_METADATA = new SwiftAnyType(\(SwiftKitPrinting.renderCallGetSwiftType(module: self.swiftModuleName, nominal: decl))); public final SwiftAnyType $swiftType() { @@ -308,6 +321,11 @@ extension Swift2JavaTranslator { """ static final SymbolLookup SYMBOL_LOOKUP = getSymbolLookup(); private static SymbolLookup getSymbolLookup() { + // Ensure Swift and our Lib are loaded during static initialization of the class. + System.loadLibrary("swiftCore"); + System.loadLibrary("SwiftKitSwift"); + System.loadLibrary(LIB_NAME); + if (PlatformUtils.isMacOS()) { return SymbolLookup.libraryLookup(System.mapLibraryName(LIB_NAME), LIBRARY_ARENA) .or(SymbolLookup.loaderLookup()) @@ -339,6 +357,11 @@ extension Swift2JavaTranslator { private \(typeName)() { // Should not be called directly } + + // Static enum to force initialization + private static enum Initializer { + FORCE; // Refer to this to force outer Class initialization (and static{} blocks to trigger) + } """ ) } @@ -423,9 +446,9 @@ extension Swift2JavaTranslator { .map(Object::toString) .collect(Collectors.joining(", ")); System.out.printf("[java][%s:%d] Downcall: %s(%s)\\n", - ex.getStackTrace()[1].getFileName(), - ex.getStackTrace()[1].getLineNumber(), - ex.getStackTrace()[1].getMethodName(), + ex.getStackTrace()[2].getFileName(), + ex.getStackTrace()[2].getLineNumber(), + ex.getStackTrace()[2].getMethodName(), traceArgs); } @@ -436,9 +459,9 @@ extension Swift2JavaTranslator { .map(Object::toString) .collect(Collectors.joining(", ")); System.out.printf("[java][%s:%d] %s: %s\\n", - ex.getStackTrace()[1].getFileName(), - ex.getStackTrace()[1].getLineNumber(), - ex.getStackTrace()[1].getMethodName(), + ex.getStackTrace()[2].getFileName(), + ex.getStackTrace()[2].getLineNumber(), + ex.getStackTrace()[2].getMethodName(), traceArgs); } """ @@ -508,17 +531,6 @@ extension Swift2JavaTranslator { ) } - public func printStaticLibraryLoad(_ printer: inout CodePrinter) { - printer.print( - """ - static { - System.loadLibrary("swiftCore"); - System.loadLibrary(LIB_NAME); - } - """ - ) - } - public func printFunctionDowncallMethods(_ printer: inout CodePrinter, _ decl: ImportedFunc) { printer.printSeparator(decl.identifier) @@ -872,18 +884,26 @@ extension Swift2JavaTranslator { let firstName = p.firstName ?? "_" let secondName = p.secondName ?? p.firstName ?? nextUniqueParamName() + let paramTy: String = + if paramPassingStyle == .swiftThunkSelf { + "\(p.type.cCompatibleSwiftType)" + } else { + p.type.swiftTypeName.description + } + let param = if firstName == secondName { // We have to do this to avoid a 'extraneous duplicate parameter name; 'number' already has an argument label' warning - "\(firstName): \(p.type.swiftTypeName.description)" + "\(firstName): \(paramTy)" } else { - "\(firstName) \(secondName): \(p.type.swiftTypeName.description)" + "\(firstName) \(secondName): \(paramTy)" } ps.append(param) } if paramPassingStyle == .swiftThunkSelf { - ps.append("_self: Any") + ps.append("_self: UnsafeMutableRawPointer") +// ps.append("_self: Any") } let res = ps.joined(separator: ", ") diff --git a/Sources/JExtractSwift/Swift2JavaVisitor.swift b/Sources/JExtractSwift/Swift2JavaVisitor.swift index c1e63998..3feceec3 100644 --- a/Sources/JExtractSwift/Swift2JavaVisitor.swift +++ b/Sources/JExtractSwift/Swift2JavaVisitor.swift @@ -136,7 +136,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { // TODO: filter out kinds of variables we cannot import - self.log.info("Import variable: \(node.kind) \(fullName)") + self.log.debug("Import variable: \(node.kind) \(fullName)") let returnTy: TypeSyntax if let typeAnnotation = binding.typeAnnotation { @@ -167,7 +167,7 @@ final class Swift2JavaVisitor: SyntaxVisitor { } if let currentTypeName { - log.info("Record variable in \(currentTypeName)") + log.debug("Record variable in \(currentTypeName)") translator.importedTypes[currentTypeName]!.variables.append(varDecl) } else { fatalError("Global variables are not supported yet: \(node.debugDescription)") @@ -221,6 +221,10 @@ final class Swift2JavaVisitor: SyntaxVisitor { return .skipChildren } + + override func visit(_ node: DeinitializerDeclSyntax) -> SyntaxVisitorContinueKind { + return .skipChildren + } } extension DeclGroupSyntax where Self: NamedDeclSyntax { diff --git a/Sources/JExtractSwift/SwiftThunkTranslator.swift b/Sources/JExtractSwift/SwiftThunkTranslator.swift index 57628f09..197cf226 100644 --- a/Sources/JExtractSwift/SwiftThunkTranslator.swift +++ b/Sources/JExtractSwift/SwiftThunkTranslator.swift @@ -25,6 +25,16 @@ struct SwiftThunkTranslator { self.st = st } + func renderGlobalThunks() -> [DeclSyntax] { + var decls: [DeclSyntax] = [] + + for decl in st.importedGlobalFuncs { + decls.append(contentsOf: render(forFunc: decl)) + } + + return decls + } + /// Render all the thunks that make Swift methods accessible to Java. func renderThunks(forType nominal: ImportedNominalType) -> [DeclSyntax] { var decls: [DeclSyntax] = [] @@ -32,10 +42,6 @@ struct SwiftThunkTranslator { decls.append(renderSwiftTypeAccessor(nominal)) - for decl in st.importedGlobalFuncs { - decls.append(contentsOf: render(forFunc: decl)) - } - for decl in nominal.initializers { decls.append(contentsOf: renderSwiftInitAccessor(decl)) } @@ -79,46 +85,48 @@ struct SwiftThunkTranslator { let thunkName = self.st.thunkNameRegistry.functionThunkName( module: st.swiftModuleName, decl: function) - // FIXME: handle in thunk: return types - // FIXME: handle in thunk: parameters - // FIXME: handle in thunk: errors return [ """ @_cdecl("\(raw: thunkName)") public func \(raw: thunkName)(\(raw: st.renderSwiftParamDecls(function, paramPassingStyle: nil))) -> Any /* \(raw: parent.swiftTypeName) */ { - \(raw: parent.swiftTypeName)(\(raw: st.renderForwardSwiftParams(function, paramPassingStyle: nil))) + print("[swift] init class \(raw: parent.swiftTypeName)") + return \(raw: parent.swiftTypeName)(\(raw: st.renderForwardSwiftParams(function, paramPassingStyle: nil))) } """ ] } - func render(forFunc decl: ImportedFunc) -> [DeclSyntax] { st.log.trace("Rendering thunks for: \(decl.baseIdentifier)") -// let funcName = SwiftKitPrinting.Names.functionThunk( -// thunkNameRegistry: &st.thunkNameRegistry, -// module: st.swiftModuleName, -// function: decl) - let funcName = st.thunkNameRegistry.functionThunkName(module: st.swiftModuleName, decl: decl) + let thunkName = st.thunkNameRegistry.functionThunkName(module: st.swiftModuleName, decl: decl) // Do we need to pass a self parameter? let paramPassingStyle: SelfParameterVariant? let callBaseDot: String if let parent = decl.parent { paramPassingStyle = .swiftThunkSelf - // TODO: unsafe bitcast - callBaseDot = "(_self as! \(parent.originalSwiftType))." + callBaseDot = "unsafeBitCast(_self, to: \(parent.originalSwiftType).self)." + // callBaseDot = "(_self as! \(parent.originalSwiftType))." } else { paramPassingStyle = nil callBaseDot = "" } + let returnArrowTy = + if decl.returnType.cCompatibleJavaMemoryLayout == .primitive(.void) { + "/* \(decl.returnType.swiftTypeName) */" + } else { + "-> \(decl.returnType.cCompatibleSwiftType) /* \(decl.returnType.swiftTypeName) */" + } + + // FIXME: handle in thunk: errors + return [ """ - @_cdecl("\(raw: funcName)") - public func \(raw: funcName)(\(raw: st.renderSwiftParamDecls(decl, paramPassingStyle: paramPassingStyle))) -> \(decl.returnType.cCompatibleSwiftType) /* \(raw: decl.returnType.swiftTypeName) */ { + @_cdecl("\(raw: thunkName)") + public func \(raw: thunkName)(\(raw: st.renderSwiftParamDecls(decl, paramPassingStyle: paramPassingStyle))) \(raw: returnArrowTy) { \(raw: callBaseDot)\(raw: decl.baseIdentifier)(\(raw: st.renderForwardSwiftParams(decl, paramPassingStyle: paramPassingStyle))) } """ diff --git a/Sources/JExtractSwift/ThunkNameRegistry.swift b/Sources/JExtractSwift/ThunkNameRegistry.swift index 2e716ff7..92f1397d 100644 --- a/Sources/JExtractSwift/ThunkNameRegistry.swift +++ b/Sources/JExtractSwift/ThunkNameRegistry.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +import SwiftSyntax + /// Registry of names we've already emitted as @_cdecl and must be kept unique. /// In order to avoid duplicate symbols, the registry can append some unique identifier to duplicated names package struct ThunkNameRegistry { @@ -36,6 +38,8 @@ package struct ThunkNameRegistry { param.firstName ?? "_" }.joined(separator: "_") } + + let name = if let parent = decl.parent { diff --git a/Sources/Java2SwiftLib/JavaClassTranslator.swift b/Sources/Java2SwiftLib/JavaClassTranslator.swift index b90a538a..b0b0628f 100644 --- a/Sources/Java2SwiftLib/JavaClassTranslator.swift +++ b/Sources/Java2SwiftLib/JavaClassTranslator.swift @@ -518,7 +518,9 @@ extension JavaClassTranslator { preferValueTypes: true, outerOptional: .implicitlyUnwrappedOptional ) - if resultType != "Void" { + + // FIXME: cleanup the checking here + if resultType != "Void" && resultType != "Swift.Void" { resultTypeStr = " -> \(resultType)" } else { resultTypeStr = "" diff --git a/SwiftKit/build.gradle b/SwiftKit/build.gradle index 3479e6c8..e0896e66 100644 --- a/SwiftKit/build.gradle +++ b/SwiftKit/build.gradle @@ -40,3 +40,32 @@ tasks.test { events("passed", "skipped", "failed") } } + +// SwiftKit depends on SwiftKitSwift (Swift library that this Java library calls into) + +def compileSwift = tasks.register("compileSwift", Exec) { + description "Compile the swift-java SwiftKitSwift dynamic library that SwiftKit (Java) calls into" + + inputs.file(new File(rootDir, "Package.swift")) + inputs.dir(new File(rootDir, "Sources/")) // a bit generous, but better safe than sorry, and include all Swift source changes + outputs.dir(new File(rootDir, ".build")) + + workingDir = rootDir + commandLine "swift" + args("build", "--target", "SwiftKitSwift") +} +tasks.build { + dependsOn("compileSwift") +} + + + +def cleanSwift = tasks.register("cleanSwift", Exec) { + workingDir = rootDir + commandLine "swift" + args("package", "clean") +} +tasks.clean { + dependsOn("cleanSwift") +} + diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java b/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java index c1344375..83ee9f6d 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java +++ b/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java @@ -27,15 +27,22 @@ public class SwiftKit { - private static final String STDLIB_DYLIB_NAME = "swiftCore"; + public static final String STDLIB_DYLIB_NAME = "swiftCore"; + public static final String SWIFTKITSWIFT_DYLIB_NAME = "SwiftKitSwift"; private static final String STDLIB_MACOS_DYLIB_PATH = "/usr/lib/swift/libswiftCore.dylib"; private static final Arena LIBRARY_ARENA = Arena.ofAuto(); static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls"); - static { + @SuppressWarnings("unused") + private static final boolean INITIALIZED_LIBS = loadLibraries(false); + + public static boolean loadLibraries(boolean loadSwiftKit) { System.loadLibrary(STDLIB_DYLIB_NAME); - System.loadLibrary("SwiftKitSwift"); + if (loadSwiftKit) { + System.loadLibrary(SWIFTKITSWIFT_DYLIB_NAME); + } + return true; } static final SymbolLookup SYMBOL_LOOKUP = getSymbolLookup(); diff --git a/SwiftKit/src/test/java/org/swift/swiftkit/SwiftRuntimeMetadataTest.java b/SwiftKit/src/test/java/org/swift/swiftkit/SwiftRuntimeMetadataTest.java index 5354cc4f..ffefe72e 100644 --- a/SwiftKit/src/test/java/org/swift/swiftkit/SwiftRuntimeMetadataTest.java +++ b/SwiftKit/src/test/java/org/swift/swiftkit/SwiftRuntimeMetadataTest.java @@ -15,6 +15,7 @@ package org.swift.swiftkit; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; public class SwiftRuntimeMetadataTest { diff --git a/Tests/JExtractSwiftTests/MethodThunkTests.swift b/Tests/JExtractSwiftTests/MethodThunkTests.swift index 705a4c7c..e03add8c 100644 --- a/Tests/JExtractSwiftTests/MethodThunkTests.swift +++ b/Tests/JExtractSwiftTests/MethodThunkTests.swift @@ -39,13 +39,13 @@ final class MethodThunkTests { [ """ @_cdecl("swiftjava_FakeModule_globalFunc_a_b") - public func swiftjava_FakeModule_globalFunc_a_b(a: Int32, b: Int64) -> Swift.Void /* Void */ { + public func swiftjava_FakeModule_globalFunc_a_b(a: Int32, b: Int64) /* Void */ { globalFunc(a: a, b: b) } """, """ @_cdecl("swiftjava_FakeModule_globalFunc_a_b$1") - public func swiftjava_FakeModule_globalFunc_a_b$1(a: Double, b: Int64) -> Swift.Void /* Void */ { + public func swiftjava_FakeModule_globalFunc_a_b$1(a: Double, b: Int64) /* Void */ { globalFunc(a: a, b: b) } """ diff --git a/Tests/JExtractSwiftTests/ThunkNameRegistryTests.swift b/Tests/JExtractSwiftTests/ThunkNameRegistryTests.swift deleted file mode 100644 index 58447268..00000000 --- a/Tests/JExtractSwiftTests/ThunkNameRegistryTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import JExtractSwift -import Testing - -final class ThunkNameRegistryTests { - @Test("Thunk names: deduplicate names") - func deduplicate() throws { - var registry = ThunkNameRegistry() - #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello") - #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello$1") - #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello$2") - #expect(registry.deduplicate(name: "swiftjava_hello") == "swiftjava_hello$3") - #expect(registry.deduplicate(name: "swiftjava_other") == "swiftjava_other") - #expect(registry.deduplicate(name: "swiftjava_other") == "swiftjava_other$1") - } -} \ No newline at end of file diff --git a/Tests/JExtractSwiftTests/VariableImportTests.swift b/Tests/JExtractSwiftTests/VariableImportTests.swift index ac3b003e..47e44be2 100644 --- a/Tests/JExtractSwiftTests/VariableImportTests.swift +++ b/Tests/JExtractSwiftTests/VariableImportTests.swift @@ -49,18 +49,20 @@ final class VariableImportTests { """ private static class counterInt { public static final FunctionDescriptor DESC_GET = FunctionDescriptor.of( - /* -> */SWIFT_INT, + /* -> */SWIFT_INT, SWIFT_POINTER - ); - public static final MemorySegment ADDR_GET = - FakeModule.findOrThrow("swiftjava_FakeModule_MySwiftClass_counterInt$1"); + ); + public static final MemorySegment ADDR_GET = + FakeModule.findOrThrow("swiftjava_FakeModule_MySwiftClass_counterInt"); + public static final MethodHandle HANDLE_GET = Linker.nativeLinker().downcallHandle(ADDR_GET, DESC_GET); public static final FunctionDescriptor DESC_SET = FunctionDescriptor.ofVoid( - SWIFT_INT, + SWIFT_INT, SWIFT_POINTER - ); + ); public static final MemorySegment ADDR_SET = - FakeModule.findOrThrow("swiftjava_FakeModule_MySwiftClass_counterInt__$1"); + FakeModule.findOrThrow("swiftjava_FakeModule_MySwiftClass_counterInt"); + public static final MethodHandle HANDLE_SET = Linker.nativeLinker().downcallHandle(ADDR_SET, DESC_SET); } """, diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index a61e5793..c18f30c1 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -15,3 +15,12 @@ repositories { mavenCentral() } + +def cleanSwift = tasks.register("cleanSwift", Exec) { + workingDir = layout.projectDirectory + commandLine "swift" + args("package", "clean") +} +tasks.clean { + dependsOn("cleanSwift") +} diff --git a/buildSrc/src/main/groovy/org/swift/swiftkit/gradle/BuildUtils.groovy b/buildSrc/src/main/groovy/org/swift/swiftkit/gradle/BuildUtils.groovy index 2e09d11b..e05dcf94 100644 --- a/buildSrc/src/main/groovy/org/swift/swiftkit/gradle/BuildUtils.groovy +++ b/buildSrc/src/main/groovy/org/swift/swiftkit/gradle/BuildUtils.groovy @@ -23,7 +23,7 @@ final class BuildUtils { def isLinux = osName.toLowerCase(Locale.getDefault()).contains("linux") def base = rootDir == null ? "" : "${rootDir}/" - return [ + def debugPaths = [ isLinux ? /* Linux */ (osArch == "amd64" || osArch == "x86_64" ? "${base}.build/x86_64-unknown-linux-gnu/debug/" : @@ -38,10 +38,20 @@ final class BuildUtils { /* macOS */ (osArch == "aarch64" ? "${base}../../.build/arm64-apple-macosx/debug/" : "${base}../../.build/${osArch}-apple-macosx/debug/"), - isLinux ? - "/usr/lib/swift/linux" : - // assume macOS - "/usr/lib/swift/" ] + def releasePaths = debugPaths.collect { it.replaceAll("debug", "release") } + def systemPaths = + isLinux ? + [ + "/usr/lib/swift/linux", + // TODO: should we be Swiftly aware and then use the currently used path? + System.getProperty("user.home") + "/.local/share/swiftly/toolchains/6.0.2/usr/lib/swift/linux" + ] : + [ + // assume macOS + "/usr/lib/swift/" + ] + + return releasePaths + debugPaths + systemPaths } } diff --git a/docker/Dockerfile b/docker/Dockerfile index c4711253..c68ccc3c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,5 +26,5 @@ ENV PATH="$PATH:/usr/lib/jvm/default-jdk/bin" # Install "untested" nightly 'main' Swift # TODO: Only do this if the released Swift is older than what we require -COPY install_untested_nightly_swift.sh . -RUN bash -xc './install_untested_nightly_swift.sh' +#COPY install_untested_nightly_swift.sh . +RUN #bash -xc './install_untested_nightly_swift.sh' diff --git a/docker/install_jdk.sh b/docker/install_jdk.sh index 0a0a7de2..8aa295ce 100755 --- a/docker/install_jdk.sh +++ b/docker/install_jdk.sh @@ -75,6 +75,7 @@ fi mkdir -p /usr/lib/jvm/ mv jdk.tar.gz /usr/lib/jvm/ +cd /usr/lib/jvm/ tar xzvf jdk.tar.gz && rm jdk.tar.gz mv "$(find . -depth -maxdepth 1 -type d | head -n1)" default-jdk diff --git a/settings.gradle b/settings.gradle index 3a0bd106..fa0fa5bd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,4 +26,3 @@ new File(rootDir, "Samples").listFiles().each { include ":Samples:${it.name}" } } - From 50750e036774d3db7806e348e9d2be391cb2ae9f Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Thu, 14 Nov 2024 15:56:17 +0900 Subject: [PATCH 6/8] Properly avoid Any in thunks we're emitting Correct reference counting in init thunk lots of fixing around how we deal with config and library loading move trace functions into SwiftKit; avoid regenerating adjust swift source gen tests --- .github/scripts/validate_samples.sh | 2 +- .../JExtractSwiftCommandPlugin.swift | 1 + .../com/example/swift/HelloJava2Swift.java | 22 ++++--- .../Swift2JavaTranslator+Printing.swift | 61 +++++-------------- .../JExtractSwift/SwiftThunkTranslator.swift | 13 ++-- Sources/SwiftKitSwift/SwiftKit.swift | 34 +++++++---- .../java/org/swift/swiftkit/SwiftKit.java | 34 ++++++++--- .../FuncCallbackImportTests.swift | 4 +- .../MethodImportTests.swift | 28 ++++----- .../JExtractSwiftTests/MethodThunkTests.swift | 4 +- .../VariableImportTests.swift | 8 +-- .../swift/swiftkit/gradle/BuildUtils.groovy | 1 + 12 files changed, 108 insertions(+), 104 deletions(-) diff --git a/.github/scripts/validate_samples.sh b/.github/scripts/validate_samples.sh index 70f917eb..26273575 100755 --- a/.github/scripts/validate_samples.sh +++ b/.github/scripts/validate_samples.sh @@ -22,7 +22,7 @@ for samplePackage in ${SAMPLE_PACKAGES} ; do ./${CI_VALIDATE_SCRIPT} || exit elif [[ $(find . -name 'build.gradle*' -maxdepth 1) ]]; then echo -e "${BOLD}Gradle${RESET} build..." - ./gradlew build --info || exit + ./gradlew build || ./gradlew build --info # re-run to get better failure output else echo -e "${BOLD}SwiftPM${RESET} build..." swift build || exit diff --git a/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift b/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift index f09c13e2..008bc7d7 100644 --- a/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift +++ b/Plugins/JExtractSwiftCommandPlugin/JExtractSwiftCommandPlugin.swift @@ -60,6 +60,7 @@ final class JExtractSwiftCommandPlugin: BuildToolPlugin, CommandPlugin { print("[swift-java-command] error: Failed to extract from target '\(target.name)': \(error)") } + print("[swift-java-command] Done.") } print("[swift-java-command] Generating sources: " + "done".green + ".") } diff --git a/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java b/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java index af7345ff..2a86e403 100644 --- a/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java +++ b/Samples/SwiftKitSampleApp/src/main/java/com/example/swift/HelloJava2Swift.java @@ -24,19 +24,17 @@ import org.swift.swiftkit.SwiftKit; import org.swift.swiftkit.SwiftValueWitnessTable; +import java.util.Arrays; + public class HelloJava2Swift { public static void main(String[] args) { boolean traceDowncalls = Boolean.getBoolean("jextract.trace.downcalls"); System.out.println("Property: jextract.trace.downcalls = " + traceDowncalls); - System.out.print("java.library.path = \n"); - for (var path : SwiftKit.getJavaLibraryPath().split(":")) { - System.out.println(" " + path); - } + System.out.print("Property: java.library.path = " +SwiftKit.getJavaLibraryPath()); examples(); - } static void examples() { @@ -44,13 +42,17 @@ static void examples() { MySwiftLibrary.globalTakeInt(1337); - MySwiftClass obj = new MySwiftClass(2222, 7777); + // Example of using an arena; MyClass.deinit is run at end of scope + try (var arena = SwiftArena.ofConfined()) { + MySwiftClass obj = new MySwiftClass(arena, 2222, 7777); - SwiftKit.retain(obj.$memorySegment()); - System.out.println("[java] obj ref count = " + SwiftKit.retainCount(obj.$memorySegment())); + // just checking retains/releases work + SwiftKit.retain(obj.$memorySegment()); + SwiftKit.release(obj.$memorySegment()); - obj.voidMethod(); - obj.takeIntMethod(42); + obj.voidMethod(); + obj.takeIntMethod(42); + } System.out.println("DONE."); } diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift index 7600753a..cb9fd7ed 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift @@ -112,6 +112,15 @@ extension Swift2JavaTranslator { public func printSwiftThunkSources(_ printer: inout CodePrinter, ty: ImportedNominalType) throws { let stt = SwiftThunkTranslator(self) + printer.print( + """ + // Generated by swift-java + + import SwiftKitSwift + + """ + ) + for thunk in stt.renderThunks(forType: ty) { printer.print("\(thunk)") printer.print("") @@ -258,9 +267,6 @@ extension Swift2JavaTranslator { // Layout of the class printClassMemoryLayout(&printer, decl) - // Render the 'trace' functions etc - printTraceFunctionDecls(&printer) - body(&printer) } } @@ -273,9 +279,6 @@ extension Swift2JavaTranslator { printClassConstants(printer: &printer) printTypeMappingDecls(&printer) - // Render the 'trace' functions etc - printTraceFunctionDecls(&printer) - printer.print( """ static MemorySegment findOrThrow(String symbol) { @@ -434,40 +437,6 @@ extension Swift2JavaTranslator { ) } - public func printTraceFunctionDecls(_ printer: inout CodePrinter) { - printer.print( - """ - static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls"); - - static void traceDowncall(Object... args) { - var ex = new RuntimeException(); - - String traceArgs = Arrays.stream(args) - .map(Object::toString) - .collect(Collectors.joining(", ")); - System.out.printf("[java][%s:%d] Downcall: %s(%s)\\n", - ex.getStackTrace()[2].getFileName(), - ex.getStackTrace()[2].getLineNumber(), - ex.getStackTrace()[2].getMethodName(), - traceArgs); - } - - static void trace(Object... args) { - var ex = new RuntimeException(); - - String traceArgs = Arrays.stream(args) - .map(Object::toString) - .collect(Collectors.joining(", ")); - System.out.printf("[java][%s:%d] %s: %s\\n", - ex.getStackTrace()[2].getFileName(), - ex.getStackTrace()[2].getLineNumber(), - ex.getStackTrace()[2].getMethodName(), - traceArgs); - } - """ - ) - } - public func printClassConstructors(_ printer: inout CodePrinter, _ decl: ImportedFunc) { guard let parentName = decl.parent else { fatalError("init must be inside a parent type! Was: \(decl)") @@ -515,8 +484,8 @@ extension Swift2JavaTranslator { public \(parentName.unqualifiedJavaTypeName)(SwiftArena arena, \(renderJavaParamDecls(decl, paramPassingStyle: .wrapper))) { var mh$ = \(descClassIdentifier).HANDLE; try { - if (TRACE_DOWNCALLS) { - traceDowncall(\(renderForwardJavaParams(decl, paramPassingStyle: nil))); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(\(renderForwardJavaParams(decl, paramPassingStyle: nil))); } this.selfMemorySegment = (MemorySegment) mh$.invokeExact(\(renderForwardJavaParams(decl, paramPassingStyle: nil)), TYPE_METADATA.$memorySegment()); @@ -747,8 +716,8 @@ extension Swift2JavaTranslator { \(renderUpcallHandles(decl)) """, """ - if (TRACE_DOWNCALLS) { - traceDowncall(\(renderForwardJavaParams(decl, paramPassingStyle: .memorySegment))); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(\(renderForwardJavaParams(decl, paramPassingStyle: .memorySegment))); } \(maybeReturnCast) mh$.invokeExact(\(renderForwardJavaParams(decl, paramPassingStyle: paramPassingStyle))); } catch (Throwable ex$) { @@ -800,8 +769,8 @@ extension Swift2JavaTranslator { public static \(returnTy) \(decl.baseIdentifier)(\(renderJavaParamDecls(decl, paramPassingStyle: paramPassingStyle))) { var mh$ = \(decl.baseIdentifier).HANDLE; try { - if (TRACE_DOWNCALLS) { - traceDowncall(\(renderForwardJavaParams(decl, paramPassingStyle: .memorySegment))); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(\(renderForwardJavaParams(decl, paramPassingStyle: .memorySegment))); } \(maybeReturnCast) mh$.invokeExact(\(renderForwardJavaParams(decl, paramPassingStyle: paramPassingStyle))); } catch (Throwable ex$) { diff --git a/Sources/JExtractSwift/SwiftThunkTranslator.swift b/Sources/JExtractSwift/SwiftThunkTranslator.swift index 197cf226..5fc01279 100644 --- a/Sources/JExtractSwift/SwiftThunkTranslator.swift +++ b/Sources/JExtractSwift/SwiftThunkTranslator.swift @@ -71,8 +71,8 @@ struct SwiftThunkTranslator { return """ @_cdecl("\(raw: funcName)") - public func \(raw: funcName)() -> Any /* Any.Type */ { - return \(raw: nominal.swiftTypeName).self + public func \(raw: funcName)() -> UnsafeMutableRawPointer /* Any.Type */ { + return unsafeBitCast(\(raw: nominal.swiftTypeName).self, to: UnsafeMutableRawPointer.self) } """ } @@ -89,9 +89,10 @@ struct SwiftThunkTranslator { [ """ @_cdecl("\(raw: thunkName)") - public func \(raw: thunkName)(\(raw: st.renderSwiftParamDecls(function, paramPassingStyle: nil))) -> Any /* \(raw: parent.swiftTypeName) */ { - print("[swift] init class \(raw: parent.swiftTypeName)") - return \(raw: parent.swiftTypeName)(\(raw: st.renderForwardSwiftParams(function, paramPassingStyle: nil))) + public func \(raw: thunkName)(\(raw: st.renderSwiftParamDecls(function, paramPassingStyle: nil))) -> UnsafeMutableRawPointer /* \(raw: parent.swiftTypeName) */ { + let _self = \(raw: parent.swiftTypeName)(\(raw: st.renderForwardSwiftParams(function, paramPassingStyle: nil))) + let self$ = unsafeBitCast(_self, to: UnsafeMutableRawPointer.self) + return _swiftjava_swift_retain(object: self$) } """ ] @@ -127,7 +128,7 @@ struct SwiftThunkTranslator { """ @_cdecl("\(raw: thunkName)") public func \(raw: thunkName)(\(raw: st.renderSwiftParamDecls(decl, paramPassingStyle: paramPassingStyle))) \(raw: returnArrowTy) { - \(raw: callBaseDot)\(raw: decl.baseIdentifier)(\(raw: st.renderForwardSwiftParams(decl, paramPassingStyle: paramPassingStyle))) + return \(raw: callBaseDot)\(raw: decl.baseIdentifier)(\(raw: st.renderForwardSwiftParams(decl, paramPassingStyle: paramPassingStyle))) } """ ] diff --git a/Sources/SwiftKitSwift/SwiftKit.swift b/Sources/SwiftKitSwift/SwiftKit.swift index 9fdcdd80..38b3c1c8 100644 --- a/Sources/SwiftKitSwift/SwiftKit.swift +++ b/Sources/SwiftKitSwift/SwiftKit.swift @@ -12,12 +12,6 @@ // //===----------------------------------------------------------------------===// -// This is a "plain Swift" file containing various types of declarations, -// that is exported to Java by using the `jextract-swift` tool. -// -// No annotations are necessary on the Swift side to perform the export. - -// FIXME: this is a workaround until we can pass String to Swift directly @_silgen_name("getTypeByStringByteArray") public func getTypeByStringByteArray(_ name: UnsafePointer) -> Any.Type? { let string = String(cString: name) @@ -26,9 +20,25 @@ public func getTypeByStringByteArray(_ name: UnsafePointer) -> Any.Type? return type } -//// FIXME: this is internal in stdlib, it would make things easier here -//@_silgen_name("swift_stdlib_getTypeByMangledNameUntrusted") -//public func _getTypeByMangledNameUntrusted( -// _ name: UnsafePointer, -// _ nameLength: UInt) -// -> Any.Type? +@_silgen_name("swift_retain") +public func _swiftjava_swift_retain(object: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer + +@_silgen_name("swift_release") +public func _swiftjava_swift_release(object: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer + +@_silgen_name("swift_retainCount") +public func _swiftjava_swift_retainCount(object: UnsafeMutableRawPointer) -> Int + +@_silgen_name("swift_isUniquelyReferenced") +public func _swiftjava_swift_isUniquelyReferenced(object: UnsafeMutableRawPointer) -> Bool + + + @_alwaysEmitIntoClient @_transparent + internal func _swiftjava_withHeapObject( + of object: AnyObject, + _ body: (UnsafeMutableRawPointer) -> R + ) -> R { + defer { _fixLifetime(object) } + let unmanaged = Unmanaged.passUnretained(object) + return body(unmanaged.toOpaque()) + } diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java b/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java index 83ee9f6d..1951c23e 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java +++ b/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java @@ -29,10 +29,11 @@ public class SwiftKit { public static final String STDLIB_DYLIB_NAME = "swiftCore"; public static final String SWIFTKITSWIFT_DYLIB_NAME = "SwiftKitSwift"; + public static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls"); + private static final String STDLIB_MACOS_DYLIB_PATH = "/usr/lib/swift/libswiftCore.dylib"; private static final Arena LIBRARY_ARENA = Arena.ofAuto(); - static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls"); @SuppressWarnings("unused") private static final boolean INITIALIZED_LIBS = loadLibraries(false); @@ -62,12 +63,31 @@ private static SymbolLookup getSymbolLookup() { public SwiftKit() { } - static void traceDowncall(String name, Object... args) { - String traceArgs = Arrays.stream(args) - .map(Object::toString) - .collect(Collectors.joining(", ")); - System.out.printf("[java] Downcall: %s(%s)\n", name, traceArgs); - } + public static void traceDowncall(Object... args) { + var ex = new RuntimeException(); + + String traceArgs = Arrays.stream(args) + .map(Object::toString) + .collect(Collectors.joining(", ")); + System.out.printf("[java][%s:%d] Downcall: %s(%s)\n", + ex.getStackTrace()[1].getFileName(), + ex.getStackTrace()[1].getLineNumber(), + ex.getStackTrace()[1].getMethodName(), + traceArgs); + } + + public static void trace(Object... args) { + var ex = new RuntimeException(); + + String traceArgs = Arrays.stream(args) + .map(Object::toString) + .collect(Collectors.joining(", ")); + System.out.printf("[java][%s:%d] %s: %s\n", + ex.getStackTrace()[1].getFileName(), + ex.getStackTrace()[1].getLineNumber(), + ex.getStackTrace()[1].getMethodName(), + traceArgs); + } static MemorySegment findOrThrow(String symbol) { return SYMBOL_LOOKUP.find(symbol) diff --git a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift index 41440f8e..2f2c8c8c 100644 --- a/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift +++ b/Tests/JExtractSwiftTests/FuncCallbackImportTests.swift @@ -69,8 +69,8 @@ final class FuncCallbackImportTests { callMe_callback_handle$ = callMe_callback_handle$.bindTo(callback); Linker linker = Linker.nativeLinker(); MemorySegment callback$ = linker.upcallStub(callMe_callback_handle$, callMe_callback_desc$, arena); - if (TRACE_DOWNCALLS) { - traceDowncall(callback$); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(callback$); } mh$.invokeExact(callback$); } catch (Throwable ex$) { diff --git a/Tests/JExtractSwiftTests/MethodImportTests.swift b/Tests/JExtractSwiftTests/MethodImportTests.swift index bc5b33f8..c866cb60 100644 --- a/Tests/JExtractSwiftTests/MethodImportTests.swift +++ b/Tests/JExtractSwiftTests/MethodImportTests.swift @@ -91,8 +91,8 @@ final class MethodImportTests { public static void helloWorld() { var mh$ = helloWorld.HANDLE; try { - if (TRACE_DOWNCALLS) { - traceDowncall(); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(); } mh$.invokeExact(); } catch (Throwable ex$) { @@ -134,8 +134,8 @@ final class MethodImportTests { public static void globalTakeInt(long i) { var mh$ = globalTakeInt.HANDLE; try { - if (TRACE_DOWNCALLS) { - traceDowncall(i); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(i); } mh$.invokeExact(i); } catch (Throwable ex$) { @@ -177,8 +177,8 @@ final class MethodImportTests { public static void globalTakeIntLongString(int i32, long l, com.example.swift.String s) { var mh$ = globalTakeIntLongString.HANDLE; try { - if (TRACE_DOWNCALLS) { - traceDowncall(i32, l, s.$memorySegment()); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(i32, l, s.$memorySegment()); } mh$.invokeExact(i32, l, s.$memorySegment()); } catch (Throwable ex$) { @@ -220,8 +220,8 @@ final class MethodImportTests { public static void helloMemberFunction(java.lang.foreign.MemorySegment self$) { var mh$ = helloMemberFunction.HANDLE; try { - if (TRACE_DOWNCALLS) { - traceDowncall(self$); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(self$); } mh$.invokeExact(self$); } catch (Throwable ex$) { @@ -263,8 +263,8 @@ final class MethodImportTests { public static void helloMemberInExtension(java.lang.foreign.MemorySegment self$) { var mh$ = helloMemberInExtension.HANDLE; try { - if (TRACE_DOWNCALLS) { - traceDowncall(self$); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(self$); } mh$.invokeExact(self$); } catch (Throwable ex$) { @@ -306,8 +306,8 @@ final class MethodImportTests { public static void helloMemberFunction(java.lang.foreign.MemorySegment self$) { var mh$ = helloMemberFunction.HANDLE; try { - if (TRACE_DOWNCALLS) { - traceDowncall(self$); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(self$); } mh$.invokeExact(self$); } catch (Throwable ex$) { @@ -431,8 +431,8 @@ final class MethodImportTests { public MySwiftClass(SwiftArena arena, long len, long cap) { var mh$ = init_len_cap.HANDLE; try { - if (TRACE_DOWNCALLS) { - traceDowncall(len, cap); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(len, cap); } this.selfMemorySegment = (MemorySegment) mh$.invokeExact(len, cap, TYPE_METADATA.$memorySegment()); if (arena != null) { diff --git a/Tests/JExtractSwiftTests/MethodThunkTests.swift b/Tests/JExtractSwiftTests/MethodThunkTests.swift index e03add8c..56eedfa5 100644 --- a/Tests/JExtractSwiftTests/MethodThunkTests.swift +++ b/Tests/JExtractSwiftTests/MethodThunkTests.swift @@ -40,13 +40,13 @@ final class MethodThunkTests { """ @_cdecl("swiftjava_FakeModule_globalFunc_a_b") public func swiftjava_FakeModule_globalFunc_a_b(a: Int32, b: Int64) /* Void */ { - globalFunc(a: a, b: b) + return globalFunc(a: a, b: b) } """, """ @_cdecl("swiftjava_FakeModule_globalFunc_a_b$1") public func swiftjava_FakeModule_globalFunc_a_b$1(a: Double, b: Int64) /* Void */ { - globalFunc(a: a, b: b) + return globalFunc(a: a, b: b) } """ ] diff --git a/Tests/JExtractSwiftTests/VariableImportTests.swift b/Tests/JExtractSwiftTests/VariableImportTests.swift index 47e44be2..ee562cbd 100644 --- a/Tests/JExtractSwiftTests/VariableImportTests.swift +++ b/Tests/JExtractSwiftTests/VariableImportTests.swift @@ -143,8 +143,8 @@ final class VariableImportTests { public static long getCounterInt(java.lang.foreign.MemorySegment self$) { var mh$ = counterInt.HANDLE_GET; try { - if (TRACE_DOWNCALLS) { - traceDowncall(self$); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(self$); } return (long) mh$.invokeExact(self$); } catch (Throwable ex$) { @@ -173,8 +173,8 @@ final class VariableImportTests { public static void setCounterInt(long newValue, java.lang.foreign.MemorySegment self$) { var mh$ = counterInt.HANDLE_SET; try { - if (TRACE_DOWNCALLS) { - traceDowncall(newValue, self$); + if (SwiftKit.TRACE_DOWNCALLS) { + SwiftKit.traceDowncall(newValue, self$); } mh$.invokeExact(newValue, self$); } catch (Throwable ex$) { diff --git a/buildSrc/src/main/groovy/org/swift/swiftkit/gradle/BuildUtils.groovy b/buildSrc/src/main/groovy/org/swift/swiftkit/gradle/BuildUtils.groovy index e05dcf94..4fcddbee 100644 --- a/buildSrc/src/main/groovy/org/swift/swiftkit/gradle/BuildUtils.groovy +++ b/buildSrc/src/main/groovy/org/swift/swiftkit/gradle/BuildUtils.groovy @@ -41,6 +41,7 @@ final class BuildUtils { ] def releasePaths = debugPaths.collect { it.replaceAll("debug", "release") } def systemPaths = + // system paths isLinux ? [ "/usr/lib/swift/linux", From 3b738bc7c5806f7efd596ee30be11ac5c60ef651 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Thu, 14 Nov 2024 22:21:34 +0900 Subject: [PATCH 7/8] remove jextract todo list from WIP, we use github issues now --- WIP.md | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/WIP.md b/WIP.md index c8a6255d..e1f58612 100644 --- a/WIP.md +++ b/WIP.md @@ -32,50 +32,3 @@ improve: - [ ] Investigate "unbridged" variants of String, Array, etc. - [ ] Investigate the new [Foreign Function & Memory API](https://bugs.openjdk.org/browse/JDK-8312523) (aka Project Panama) for exposing Swift APIs to Java. - -### jextract-swift - -Separate todo list for the jextract / panama side of the project: - -Calling convention: -- [x] Call swift methods, take parameters, return values -- [ ] How to call a **throwing** Swift function from Java -- [ ] How to call a **generic** Swift function from Java - - [ ] How to pass "call me back" (Callable, Runnable) to Swift, and make an **up-call** -- [ ] How to support passing a struct **inout** `SwiftValue` to Swift so that Java side sees change - -Bridges: -- [ ] Java **Optional** / Swift Optional - depends on generics (!) -- [ ] Efficient **String** / SwiftString wrappers and converters -- [ ] Handle byte buffers and pointers properly -- [ ] Converters for **Array** -- [ ] Converters for **List** and common collections? -- [ ] expose Swift collections as some bridged type implementing java.util.Collection? -- [ ] Import Swift **enums** - -Importer: -- [x] import global functions into the `Module.theFunction` on Java side -- [x] import functions with parameters -- [x] import functions return values -- [ ] import instance member functions using "wrapper" pattern -- [ ] handle types like `[any Thing]?`, we can't parse them right now even -- [ ] support nested types in Swift -- [ ] handle types like `any Thing`, importer does not understand `any` or `some` - -Programming model: -- [ ] Which style of ownership for Java class wrapping a Swift Class - - [x] __allocating_init, how to release/destroy from Java - - [x] Offer explicit swift_**release** / swift_**retain** functions - - [ ] Offer some way to make Immortal class instance - - [ ] **SwiftArena** which retains/destroys underlying Swift class? -- [ ] How to create a Swift struct - -Swift Compiler work: -- [x] Expose **mangled names** of types and methods in .swiftinterface -- [ ] Expose **@layout** of class, struct etc. types in .swiftinterface -- [ ] Expose `demangle` function to human-readable text; it'd be good for usability - -Build: -- [x] Gradle build for Java parts of samples and "SwiftKit" utilities -- [x] Build Swift dependencies when building Java samples automatically -- [ ] JMH benchmarks From 7df2a33f6af7953952bd3caef1a669a364951147 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Thu, 14 Nov 2024 22:37:18 +0900 Subject: [PATCH 8/8] remove JExtractPluginSampleApp sample because SwiftKit covers this --- Samples/JExtractPluginSampleApp/.gitignore | 8 - Samples/JExtractPluginSampleApp/Package.swift | 73 --------- .../MyCoolSwiftClass.swift | 35 ----- .../JExtractPluginSampleLib/swift-java.config | 3 - Samples/JExtractPluginSampleApp/build.gradle | 147 ------------------ .../JExtractPluginSampleApp/ci-validate.sh | 3 - Samples/JExtractPluginSampleApp/gradlew | 1 - Samples/JExtractPluginSampleApp/gradlew.bat | 1 - .../swift/JExtractPluginSampleMain.java | 29 ---- 9 files changed, 300 deletions(-) delete mode 100644 Samples/JExtractPluginSampleApp/.gitignore delete mode 100644 Samples/JExtractPluginSampleApp/Package.swift delete mode 100644 Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/MyCoolSwiftClass.swift delete mode 100644 Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/swift-java.config delete mode 100644 Samples/JExtractPluginSampleApp/build.gradle delete mode 100755 Samples/JExtractPluginSampleApp/ci-validate.sh delete mode 120000 Samples/JExtractPluginSampleApp/gradlew delete mode 120000 Samples/JExtractPluginSampleApp/gradlew.bat delete mode 100644 Samples/JExtractPluginSampleApp/src/main/java/com/example/swift/JExtractPluginSampleMain.java diff --git a/Samples/JExtractPluginSampleApp/.gitignore b/Samples/JExtractPluginSampleApp/.gitignore deleted file mode 100644 index 0023a534..00000000 --- a/Samples/JExtractPluginSampleApp/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -/.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc diff --git a/Samples/JExtractPluginSampleApp/Package.swift b/Samples/JExtractPluginSampleApp/Package.swift deleted file mode 100644 index 282cf6fd..00000000 --- a/Samples/JExtractPluginSampleApp/Package.swift +++ /dev/null @@ -1,73 +0,0 @@ -// swift-tools-version: 6.0 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -import PackageDescription - -import class Foundation.FileManager -import class Foundation.ProcessInfo - -// Note: the JAVA_HOME environment variable must be set to point to where -// Java is installed, e.g., -// Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home. -func findJavaHome() -> String { - if let home = ProcessInfo.processInfo.environment["JAVA_HOME"] { - return home - } - - // This is a workaround for envs (some IDEs) which have trouble with - // picking up env variables during the build process - let path = "\(FileManager.default.homeDirectoryForCurrentUser.path()).java_home" - if let home = try? String(contentsOfFile: path, encoding: .utf8) { - if let lastChar = home.last, lastChar.isNewline { - return String(home.dropLast()) - } - - return home - } - - fatalError("Please set the JAVA_HOME environment variable to point to where Java is installed.") -} -let javaHome = findJavaHome() - -let javaIncludePath = "\(javaHome)/include" -#if os(Linux) - let javaPlatformIncludePath = "\(javaIncludePath)/linux" -#elseif os(macOS) - let javaPlatformIncludePath = "\(javaIncludePath)/darwin" -#else - let javaPlatformIncludePath = "\(javaIncludePath)/win32" -#endif - -let package = Package( - name: "JExtractPluginSampleApp", - platforms: [ - .macOS(.v10_15) - ], - products: [ - .library( - name: "JExtractPluginSampleLib", - type: .dynamic, - targets: ["JExtractPluginSampleLib"] - ) - ], - dependencies: [ - .package(name: "swift-java", path: "../../") - ], - targets: [ - .target( - name: "JExtractPluginSampleLib", - dependencies: [], - exclude: [ - "swift-java.config" - ], - swiftSettings: [ - .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) - ], - plugins: [ - .plugin(name: "JExtractSwiftPlugin", package: "swift-java") - , - .plugin(name: "JExtractSwiftCommandPlugin", package: "swift-java") - ] - ) - ] -) diff --git a/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/MyCoolSwiftClass.swift b/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/MyCoolSwiftClass.swift deleted file mode 100644 index ff34e9db..00000000 --- a/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/MyCoolSwiftClass.swift +++ /dev/null @@ -1,35 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -public class MyCoolSwiftClass { - var number: Int - public init(number: Int) { - print("[swift] init(number: \(number))") - self.number = number - } - - public func exposedToJava() { - print("[swift] exposedToJava()") - print("[swift] number = \(number)") - } -} - -#if os(Linux) -// FIXME: why do we need this workaround? -@_silgen_name("_objc_autoreleaseReturnValue") -public func _objc_autoreleaseReturnValue(a: Any) {} - -@_silgen_name("objc_autoreleaseReturnValue") -public func objc_autoreleaseReturnValue(a: Any) {} -#endif diff --git a/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/swift-java.config b/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/swift-java.config deleted file mode 100644 index 6e5bc2af..00000000 --- a/Samples/JExtractPluginSampleApp/Sources/JExtractPluginSampleLib/swift-java.config +++ /dev/null @@ -1,3 +0,0 @@ -{ - "javaPackage": "com.example.swift" -} diff --git a/Samples/JExtractPluginSampleApp/build.gradle b/Samples/JExtractPluginSampleApp/build.gradle deleted file mode 100644 index 1aec8d55..00000000 --- a/Samples/JExtractPluginSampleApp/build.gradle +++ /dev/null @@ -1,147 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import org.swift.swiftkit.gradle.BuildUtils - -import java.nio.file.* - -plugins { - id("build-logic.java-application-conventions") -} - -group = "org.swift.swiftkit" -version = "1.0-SNAPSHOT" - -repositories { - mavenCentral() -} - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(22)) - } -} - - -// This is for development, when we edit the Swift swift-java project, the outputs of the generated sources may change. -// Thus, we also need to watch and re-build the top level project. -def compileSwiftJExtractPlugin = tasks.register("compileSwiftJExtractPlugin", Exec) { - description = "Rebuild the swift-java root project" - - inputs.file(new File(rootDir, "Package.swift")) - inputs.dir(new File(rootDir, "Sources")) - outputs.dir(new File(rootDir, ".build")) - - workingDir = rootDir - commandLine "swift" - args("build", - "--product", "SwiftKitSwift", - "--product", "JExtractSwiftPlugin", - "--product", "JExtractSwiftCommandPlugin") -} -def compileSwiftKitSwift = tasks.register("compileSwiftKitSwift", Exec) { - description = "Rebuild the swift-java root project" - - inputs.file(new File(rootDir, "Package.swift")) - inputs.dir(new File(rootDir, "Sources")) - outputs.dir(new File(rootDir, ".build")) - - workingDir = rootDir - commandLine "swift" - args("build", - "--product", "SwiftKitSwift") -} - -def cleanSwift = tasks.register("cleanSwift", Exec) { - workingDir = layout.projectDirectory - commandLine "swift" - args("package", "clean") -} - -def jextract = tasks.register("jextract", Exec) { - description = "Builds swift sources, including swift-java source generation" - dependsOn compileSwiftJExtractPlugin - dependsOn compileSwiftKitSwift - - // only because we depend on "live developing" the plugin while using this project to test it - inputs.file(new File(rootDir, "Package.swift")) - inputs.dir(new File(rootDir, "Sources")) - - inputs.file(new File(projectDir, "Package.swift")) - inputs.dir(new File(projectDir, "Sources")) - - // TODO: we can use package describe --type json to figure out which targets depend on JExtractSwiftPlugin and will produce outputs - // Avoid adding this directory, but create the expected one specifically for all targets - // which WILL produce sources because they have the plugin - outputs.dir(layout.buildDirectory.dir("../.build/plugins/outputs/${layout.projectDirectory.asFile.getName().toLowerCase()}")) - - File baseSwiftPluginOutputsDir = layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile - if (!baseSwiftPluginOutputsDir.exists()) { - baseSwiftPluginOutputsDir.mkdirs() - } - Files.walk(layout.buildDirectory.dir("../.build/plugins/outputs/").get().asFile.toPath()).each { - // Add any Java sources generated by the plugin to our sourceSet - if (it.endsWith("JExtractSwiftPlugin/src/generated/java")) { - outputs.dir(it) - } - } - - workingDir = layout.projectDirectory - commandLine "swift" - args("package", "jextract", "-v", "--log-level", "info") // TODO: pass log level from Gradle build -} - -// Add the java-swift generated Java sources -sourceSets { - main { - java { - srcDir(jextract) - } - } -} - -tasks.build { - dependsOn("jextract") -} - -tasks.clean { - dependsOn("cleanSwift") -} - -dependencies { - implementation(project(':SwiftKit')) - - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") -} - -tasks.named('test', Test) { - useJUnitPlatform() -} - -application { - mainClass = "com.example.swift.JExtractPluginSampleMain" - - applicationDefaultJvmArgs = [ - "--enable-native-access=ALL-UNNAMED", - - // Include the library paths where our dylibs are that we want to load and call - "-Djava.library.path=" + - (BuildUtils.javaLibraryPaths(rootDir) + - BuildUtils.javaLibraryPaths(project.projectDir)).join(":"), - - // Enable tracing downcalls (to Swift) - "-Djextract.trace.downcalls=true" - ] -} diff --git a/Samples/JExtractPluginSampleApp/ci-validate.sh b/Samples/JExtractPluginSampleApp/ci-validate.sh deleted file mode 100755 index 2a1948c2..00000000 --- a/Samples/JExtractPluginSampleApp/ci-validate.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -./gradlew check && ./gradlew run diff --git a/Samples/JExtractPluginSampleApp/gradlew b/Samples/JExtractPluginSampleApp/gradlew deleted file mode 120000 index 343e0d2c..00000000 --- a/Samples/JExtractPluginSampleApp/gradlew +++ /dev/null @@ -1 +0,0 @@ -../../gradlew \ No newline at end of file diff --git a/Samples/JExtractPluginSampleApp/gradlew.bat b/Samples/JExtractPluginSampleApp/gradlew.bat deleted file mode 120000 index cb5a9464..00000000 --- a/Samples/JExtractPluginSampleApp/gradlew.bat +++ /dev/null @@ -1 +0,0 @@ -../../gradlew.bat \ No newline at end of file diff --git a/Samples/JExtractPluginSampleApp/src/main/java/com/example/swift/JExtractPluginSampleMain.java b/Samples/JExtractPluginSampleApp/src/main/java/com/example/swift/JExtractPluginSampleMain.java deleted file mode 100644 index 3a55f176..00000000 --- a/Samples/JExtractPluginSampleApp/src/main/java/com/example/swift/JExtractPluginSampleMain.java +++ /dev/null @@ -1,29 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2024 Apple Inc. and the Swift.org project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Swift.org project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -package com.example.swift; - -import org.swift.swiftkit.SwiftKit; - -public class JExtractPluginSampleMain { - public static void main(String[] args) { - System.out.println(); - System.out.println("java.library.path = " + SwiftKit.getJavaLibraryPath()); - System.out.println("jextract.trace.downcalls = " + SwiftKit.getJextractTraceDowncalls()); - - SwiftKit.loadLibraries(true); - var o = new MyCoolSwiftClass(12); - o.exposedToJava(); - } -}