diff --git a/Makefile b/Makefile index f7d075c4..d804c41d 100644 --- a/Makefile +++ b/Makefile @@ -63,8 +63,8 @@ all: $(BUILD_DIR)/debug/libJavaKit.$(LIB_SUFFIX) $(BUILD_DIR)/debug/Java2Swift: swift build -javakit-run: $(BUILD_DIR)/debug/libJavaKit.$(LIB_SUFFIX) $(BUILD_DIR)/debug/libExampleSwiftLibrary.$(LIB_SUFFIX) - ./gradlew Samples:JavaKitSampleApp:run +javakit-run: + cd Samples/JavaKitSampleApp && swift build && java -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java -Djava.library.path=.build/debug com.example.swift.JavaKitSampleMain Java2Swift: $(BUILD_DIR)/debug/Java2Swift diff --git a/Package.swift b/Package.swift index 2bd95713..a581e842 100644 --- a/Package.swift +++ b/Package.swift @@ -86,6 +86,14 @@ let package = Package( targets: ["Java2SwiftTool"] ), + // ==== Plugin for building Java code + .plugin( + name: "JavaCompilerPlugin", + targets: [ + "JavaCompilerPlugin" + ] + ), + // ==== jextract-swift (extract Java accessors from Swift interface files) .executable( @@ -107,11 +115,6 @@ let package = Package( // ==== Examples - .library( - name: "JavaKitExample", - type: .dynamic, - targets: ["JavaKitExample"] - ), .library( name: "ExampleSwiftLibrary", type: .dynamic, @@ -197,14 +200,12 @@ let package = Package( .linkedLibrary("jvm"), ] ), - .target( - name: "JavaKitExample", - dependencies: ["JavaKit"], - swiftSettings: [ - .swiftLanguageMode(.v5), - .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) - ] + + .plugin( + name: "JavaCompilerPlugin", + capability: .buildTool() ), + .target( name: "ExampleSwiftLibrary", dependencies: [], diff --git a/Plugins/JavaCompilerPlugin/JavaCompilerPlugin.swift b/Plugins/JavaCompilerPlugin/JavaCompilerPlugin.swift new file mode 100644 index 00000000..3a6c4286 --- /dev/null +++ b/Plugins/JavaCompilerPlugin/JavaCompilerPlugin.swift @@ -0,0 +1,90 @@ +//===----------------------------------------------------------------------===// +// +// 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 JavaCompilerBuildToolPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + guard let sourceModule = target.sourceModule else { return [] } + + // Collect all of the Java source files within this target's sources. + let javaFiles = sourceModule.sourceFiles.map { $0.url }.filter { + $0.pathExtension == "java" + } + if javaFiles.isEmpty { + return [] + } + + // Note: Target doesn't have a directoryURL counterpart to directory, + // so we cannot eliminate this deprecation warning. + let sourceDir = target.directory.string + + // The class files themselves will be generated into the build directory + // for this target. + let classFiles = javaFiles.map { sourceFileURL in + let sourceFilePath = sourceFileURL.path + guard sourceFilePath.starts(with: sourceDir) else { + fatalError("Could not get relative path for source file \(sourceFilePath)") + } + + return URL( + filePath: context.pluginWorkDirectoryURL.path + ).appending(path: "Java") + .appending(path: String(sourceFilePath.dropFirst(sourceDir.count))) + .deletingPathExtension() + .appendingPathExtension("class") + } + + let javaHome = URL(filePath: findJavaHome()) + let javaClassFileURL = context.pluginWorkDirectoryURL + .appending(path: "Java") + return [ + .buildCommand( + displayName: "Compiling \(javaFiles.count) Java files for target \(sourceModule.name) to \(javaClassFileURL)", + executable: javaHome + .appending(path: "bin") + .appending(path: "javac"), + arguments: javaFiles.map { $0.path(percentEncoded: false) } + [ + "-d", javaClassFileURL.path() + ], + inputFiles: javaFiles, + outputFiles: classFiles + ) + ] + } +} + +// 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/README.md b/README.md index b30d905e..6bf9df2f 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,9 @@ Sample apps are located in the `Samples/` directory, and they showcase full "rou To run a simple app showcasing a Swift process calling into a Java library you can run: ```bash -./gradlew Samples:JavaKitSampleApp:run +cd Samples/JavaKitSampleApp +swift build +java -cp .build/plugins/outputs/javakitsampleapp/JavaKitExample/destination/JavaCompilerPlugin/Java -Djava.library.path=.build/debug com.example.swift.JavaKitSampleMain ``` #### jextract (Java -> Swift) diff --git a/Samples/JavaKitSampleApp/Package.swift b/Samples/JavaKitSampleApp/Package.swift new file mode 100644 index 00000000..92f41d14 --- /dev/null +++ b/Samples/JavaKitSampleApp/Package.swift @@ -0,0 +1,43 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import CompilerPluginSupport +import PackageDescription + +let package = Package( + name: "JavaKitSampleApp", + platforms: [ + .macOS(.v13), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6), + .macCatalyst(.v13), + ], + + products: [ + .library( + name: "JavaKitExample", + type: .dynamic, + targets: ["JavaKitExample"] + ), + ], + + dependencies: [ + .package(name: "swift-java", path: "../../") + ], + + targets: [ + .target( + name: "JavaKitExample", + dependencies: [ + .product(name: "JavaKit", package: "swift-java") + ], + swiftSettings: [ + .swiftLanguageMode(.v5) + ], + plugins: [ + .plugin(name: "JavaCompilerPlugin", package: "swift-java") + ] + ), + ] +) \ No newline at end of file diff --git a/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift new file mode 100644 index 00000000..d6eba19d --- /dev/null +++ b/Samples/JavaKitSampleApp/Sources/JavaKitExample/JavaKitExample.swift @@ -0,0 +1,120 @@ +//===----------------------------------------------------------------------===// +// +// 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 JavaKit +import JavaRuntime + +enum SwiftWrappedError: Error { + case message(String) +} + +@JavaClass("com.example.swift.HelloSwift") +struct HelloSwift { + @JavaMethod + init(environment: JNIEnvironment) + + @JavaMethod + func sayHelloBack(_ i: Int32) -> Double + + @JavaMethod + func greet(_ name: String) + + @JavaMethod + func doublesToStrings(doubles: [Double]) -> [String] + + @JavaMethod + func throwMessage(message: String) throws + + @JavaField + var value: Double + + @JavaField + var name: String + + @ImplementsJava + func sayHello(i: Int32, _ j: Int32) -> Int32 { + print("Hello from Swift!") + let answer = self.sayHelloBack(i + j) + print("Swift got back \(answer) from Java") + + print("We expect the above value to be the initial value, \(self.javaClass.initialValue)") + + print("Updating Java field value to something different") + self.value = 2.71828 + + let newAnswer = self.sayHelloBack(17) + print("Swift got back updated \(newAnswer) from Java") + + let newHello = HelloSwift(environment: javaEnvironment) + print("Swift created a new Java instance with the value \(newHello.value)") + + let name = newHello.name + print("Hello to \(name)") + newHello.greet("Swift 👋🏽 How's it going") + + self.name = "a 🗑️-collected language" + _ = self.sayHelloBack(42) + + let strings = doublesToStrings(doubles: [3.14159, 2.71828]) + print("Converting doubles to strings: \(strings)") + + // Try downcasting + if let helloSub = self.as(HelloSubclass.self) { + print("Hello from the subclass!") + helloSub.greetMe() + + assert(helloSub.super.value == 2.71828) + } else { + fatalError("Expected subclass here") + } + + // Check "is" behavior + assert(newHello.is(HelloSwift.self)) + assert(!newHello.is(HelloSubclass.self)) + + // Create a new instance. + let helloSubFromSwift = HelloSubclass(greeting: "Hello from Swift", environment: javaEnvironment) + helloSubFromSwift.greetMe() + + do { + try throwMessage(message: "I am an error") + } catch { + print("Caught Java error: \(error)") + } + + return i * j + } + + @ImplementsJava + func throwMessageFromSwift(message: String) throws -> String { + throw SwiftWrappedError.message(message) + } +} + +extension JavaClass { + @JavaStaticField + var initialValue: Double +} + +@JavaClass("com.example.swift.HelloSubclass", extends: HelloSwift.self) +struct HelloSubclass { + @JavaField + var greeting: String + + @JavaMethod + func greetMe() + + @JavaMethod + init(greeting: String, environment: JNIEnvironment) +} diff --git a/Samples/JavaKitSampleApp/src/main/java/com/example/swift/HelloSubclass.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java similarity index 100% rename from Samples/JavaKitSampleApp/src/main/java/com/example/swift/HelloSubclass.java rename to Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSubclass.java diff --git a/Samples/JavaKitSampleApp/src/main/java/com/example/swift/HelloSwift.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java similarity index 100% rename from Samples/JavaKitSampleApp/src/main/java/com/example/swift/HelloSwift.java rename to Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/HelloSwift.java diff --git a/Samples/JavaKitSampleApp/src/main/java/com/example/swift/JavaKitSampleMain.java b/Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/JavaKitSampleMain.java similarity index 100% rename from Samples/JavaKitSampleApp/src/main/java/com/example/swift/JavaKitSampleMain.java rename to Samples/JavaKitSampleApp/Sources/JavaKitExample/com/example/swift/JavaKitSampleMain.java diff --git a/Samples/JavaKitSampleApp/build.gradle b/Samples/JavaKitSampleApp/build.gradle deleted file mode 100644 index 67295994..00000000 --- a/Samples/JavaKitSampleApp/build.gradle +++ /dev/null @@ -1,60 +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 - -plugins { - id("build-logic.java-application-conventions") -} - -group = "org.swift.javakit" -version = "1.0-SNAPSHOT" - -repositories { - mavenCentral() -} - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(22)) - } -} - -dependencies { - implementation(project(':SwiftKit')) - - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") -} - -tasks.test { - useJUnitPlatform() -} - -application { - mainClass = "com.example.swift.JavaKitSampleMain" - - // In order to silence: - // WARNING: A restricted method in java.lang.foreign.SymbolLookup has been called - // WARNING: java.lang.foreign.SymbolLookup::libraryLookup has been called by org.example.swift.JavaKitExample in an unnamed module - // WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module - // WARNING: Restricted methods will be blocked in a future release unless native access is enabled - // FIXME: Find out the proper solution to this - applicationDefaultJvmArgs = [ - "--enable-native-access=ALL-UNNAMED", - - // Include the library paths where our dylibs are that we want to load and call - "-Djava.library.path=" + BuildUtils.javaLibraryPaths(rootDir).join(":") - ] -} diff --git a/Sources/JavaKitExample/com/example/swift/HelloSubclass.java b/Sources/JavaKitExample/com/example/swift/HelloSubclass.java new file mode 100644 index 00000000..eb3cba32 --- /dev/null +++ b/Sources/JavaKitExample/com/example/swift/HelloSubclass.java @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// 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; + +public class HelloSubclass extends HelloSwift { + private String greeting; + + public HelloSubclass(String greeting) { + this.greeting = greeting; + } + + private void greetMe() { + super.greet(greeting); + } +} diff --git a/Sources/JavaKitExample/com/example/swift/HelloSwift.java b/Sources/JavaKitExample/com/example/swift/HelloSwift.java new file mode 100644 index 00000000..b4c87f71 --- /dev/null +++ b/Sources/JavaKitExample/com/example/swift/HelloSwift.java @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// 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; + +public class HelloSwift { + private double value; + private static double initialValue = 3.14159; + private String name = "Java"; + + static { + System.loadLibrary("JavaKitExample"); + } + + public HelloSwift() { + this.value = initialValue; + } + + public native int sayHello(int x, int y); + public native String throwMessageFromSwift(String message) throws Exception; + + // To be called back by the native code + private double sayHelloBack(int i) { + System.out.println("And hello back from " + name + "! You passed me " + i); + return value; + } + + public void greet(String name) { + System.out.println("Salutations, " + name); + } + + String[] doublesToStrings(double[] doubles) { + int size = doubles.length; + String[] strings = new String[size]; + + for(int i = 0; i < size; i++) { + strings[i] = "" + doubles[i]; + } + + return strings; + } + + public void throwMessage(String message) throws Exception { + throw new Exception(message); + } +} diff --git a/Sources/JavaKitExample/com/example/swift/JavaKitSampleMain.java b/Sources/JavaKitExample/com/example/swift/JavaKitSampleMain.java new file mode 100644 index 00000000..2b565608 --- /dev/null +++ b/Sources/JavaKitExample/com/example/swift/JavaKitSampleMain.java @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// 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; + +/** + * This sample shows off a {@link HelloSwift} type which is partially implemented in Swift. + * For the Swift implementation refer to + */ +public class JavaKitSampleMain { + + public static void main(String[] args) { + int result = new HelloSubclass("Swift").sayHello(17, 25); + System.out.println("sayHello(17, 25) = " + result); + } +}