diff --git a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift index f23a1dbf..94cd4aa6 100644 --- a/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift +++ b/Plugins/JExtractSwiftPlugin/JExtractSwiftPlugin.swift @@ -23,6 +23,16 @@ struct JExtractSwiftBuildToolPlugin: BuildToolPlugin { // Note: Target doesn't have a directoryURL counterpart to directory, // so we cannot eliminate this deprecation warning. let sourceDir = target.directory.string + + let t = target.dependencies.first! + switch (t) { + case .target(let t): + t.sourceModule + case .product(let p): + p.sourceModules + @unknown default: + fatalError("Unknown target dependency type: \(t)") + } let toolURL = try context.tool(named: "JExtractSwiftTool").url let configuration = try readConfiguration(sourceDir: "\(sourceDir)") diff --git a/Samples/SwiftAndJavaJarSampleLib/Example.java b/Samples/SwiftAndJavaJarSampleLib/Example.java new file mode 100644 index 00000000..b5a2697b --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/Example.java @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// 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 com.example.swift.MySwiftLibrary; + +public class Example { + + public static void main(String[] args) { + MySwiftLibrary.helloWorld(); + } + +} diff --git a/Samples/SwiftAndJavaJarSampleLib/Package.swift b/Samples/SwiftAndJavaJarSampleLib/Package.swift new file mode 100644 index 00000000..5f132239 --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/Package.swift @@ -0,0 +1,77 @@ +// 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: "SwiftAndJavaJarSampleLib", + 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"), + ], + exclude: [ + "swift-java.config", + ], + swiftSettings: [ + .swiftLanguageMode(.v5), + .unsafeFlags(["-I\(javaIncludePath)", "-I\(javaPlatformIncludePath)"]) + ], + plugins: [ + .plugin(name: "JExtractSwiftPlugin", package: "swift-java"), + ] + ), + ] +) diff --git a/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift new file mode 100644 index 00000000..84e4618f --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/MySwiftLibrary.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// 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) + } +} + +// ==== 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/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/swift-java.config b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/swift-java.config new file mode 100644 index 00000000..6e5bc2af --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/Sources/MySwiftLibrary/swift-java.config @@ -0,0 +1,3 @@ +{ + "javaPackage": "com.example.swift" +} diff --git a/Samples/SwiftAndJavaJarSampleLib/build.gradle b/Samples/SwiftAndJavaJarSampleLib/build.gradle new file mode 100644 index 00000000..2125011d --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/build.gradle @@ -0,0 +1,184 @@ +//===----------------------------------------------------------------------===// +// +// 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 groovy.json.JsonSlurper +import org.swift.swiftkit.gradle.BuildUtils + +import java.nio.file.* + +plugins { + id("build-logic.java-library-conventions") + id "com.google.osdetector" version "1.7.3" + id("maven-publish") +} + +group = "org.swift.swiftkit" +version = "1.0-SNAPSHOT" + +def swiftBuildConfiguration() { + "release" +} + +repositories { + mavenLocal() + 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") +} + +// 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", + "-c", swiftBuildConfiguration(), + "--product", "SwiftKitSwift", + "--product", "JExtractSwiftPlugin", + "--product", "JExtractSwiftCommandPlugin") +} + +def jextract = tasks.register("jextract", Exec) { + description = "Builds swift sources, including swift-java source generation" + 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(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) + } + } + test { + java { + srcDir(jextract) + } + } +} + +tasks.build { + dependsOn("jextract") +} + +tasks.named('test', Test) { + useJUnitPlatform() +} + + +// ==== Jar publishing + +List swiftProductDylibPaths() { + + def process = ['swift', 'package', 'describe', '--type', 'json'].execute() + process.waitFor() + + if (process.exitValue() != 0) { + throw new RuntimeException("[swift describe] command failed with exit code: ${process.exitValue()}. Cannot find products! Output: ${process.err.text}") + } + + def json = new JsonSlurper().parseText(process.text) + + // TODO: require that we depend on swift-java + // TODO: all the products where the targets depend on swift-java plugin + def products = + json.targets.collect { target -> + target.product_memberships + }.flatten() + + + + def productDylibPaths = products.collect { + logger.info("[swift-java] Include Swift product: '${it}' in product resource paths.") + "${layout.projectDirectory}/.build/${swiftBuildConfiguration()}/lib${it}.dylib" + } + + return productDylibPaths +} + +processResources { + dependsOn "jextract" + + def dylibs = [ + "${layout.projectDirectory}/.build/${swiftBuildConfiguration()}/libSwiftKitSwift.dylib" + ] + dylibs.addAll(swiftProductDylibPaths()) + from(dylibs) +} + +jar { + archiveClassifier = osdetector.classifier +} + +base { + archivesName = "swift-and-java-jar-sample-lib" +} + +publishing { + publications { + maven(MavenPublication) { + artifactId = "swift-and-java-jar-sample-lib" + from components.java + } + } + repositories { + mavenLocal() + } +} diff --git a/Samples/SwiftAndJavaJarSampleLib/gradlew b/Samples/SwiftAndJavaJarSampleLib/gradlew new file mode 120000 index 00000000..343e0d2c --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/gradlew @@ -0,0 +1 @@ +../../gradlew \ No newline at end of file diff --git a/Samples/SwiftAndJavaJarSampleLib/gradlew.bat b/Samples/SwiftAndJavaJarSampleLib/gradlew.bat new file mode 120000 index 00000000..cb5a9464 --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/gradlew.bat @@ -0,0 +1 @@ +../../gradlew.bat \ No newline at end of file diff --git a/Samples/SwiftAndJavaJarSampleLib/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java b/Samples/SwiftAndJavaJarSampleLib/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java new file mode 100644 index 00000000..614697a3 --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/src/jmh/java/org/swift/swiftkit/JavaToSwiftBenchmark.java @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.swiftkit; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import com.example.swift.MySwiftClass; + +@SuppressWarnings("unused") +public class JavaToSwiftBenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkState { + MySwiftClass obj; + + @Setup(Level.Trial) + public void beforeALl() { + System.loadLibrary("swiftCore"); + System.loadLibrary("ExampleSwiftLibrary"); + + // Tune down debug statements so they don't fill up stdout + System.setProperty("jextract.trace.downcalls", "false"); + + obj = new MySwiftClass(1, 2); + } + } + + @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) + public void simpleSwiftApiCall(BenchmarkState state, Blackhole blackhole) { + blackhole.consume(state.obj.makeRandomIntMethod()); + } +} diff --git a/Samples/SwiftAndJavaJarSampleLib/src/main/java/com/example/swift/HelloJava2Swift.java b/Samples/SwiftAndJavaJarSampleLib/src/main/java/com/example/swift/HelloJava2Swift.java new file mode 100644 index 00000000..2a86e403 --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/src/main/java/com/example/swift/HelloJava2Swift.java @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// 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 swift-extract generated sources + +import com.example.swift.MySwiftLibrary; +import com.example.swift.MySwiftClass; + +// Import javakit/swiftkit support libraries +import org.swift.swiftkit.SwiftArena; +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("Property: java.library.path = " +SwiftKit.getJavaLibraryPath()); + + examples(); + } + + static void examples() { + MySwiftLibrary.helloWorld(); + + MySwiftLibrary.globalTakeInt(1337); + + // 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); + + // just checking retains/releases work + SwiftKit.retain(obj.$memorySegment()); + SwiftKit.release(obj.$memorySegment()); + + obj.voidMethod(); + obj.takeIntMethod(42); + } + + System.out.println("DONE."); + } +} diff --git a/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftClassTest.java b/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftClassTest.java new file mode 100644 index 00000000..fa17ef1a --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftClassTest.java @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.SwiftKit; + +import java.io.File; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +public class MySwiftClassTest { + + 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()); + }); + } + + throw new RuntimeException(throwable); + } + + @Test + void test_MySwiftClass_voidMethod() { + try { + MySwiftClass o = new MySwiftClass(12, 42); + o.voidMethod(); + } catch (Throwable throwable) { + checkPaths(throwable); + } + } + + @Test + void test_MySwiftClass_makeIntMethod() { + MySwiftClass o = new MySwiftClass(12, 42); + var got = o.makeIntMethod(); + assertEquals(12, got); + } + + @Test + @Disabled // TODO: Need var mangled names in interfaces + void test_MySwiftClass_property_len() { + MySwiftClass o = new MySwiftClass(12, 42); + var got = o.getLen(); + assertEquals(12, got); + } + +} diff --git a/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftLibraryTest.java b/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftLibraryTest.java new file mode 100644 index 00000000..ffa90359 --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/src/test/java/com/example/swift/MySwiftLibraryTest.java @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2024 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +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 MySwiftLibraryTest { + + @Test + void call_helloWorld() { + MySwiftLibrary.helloWorld(); + + assertNotNull(MySwiftLibrary.helloWorld$address()); + } + + @Test + void call_globalTakeInt() { + MySwiftLibrary.globalTakeInt(12); + + assertNotNull(MySwiftLibrary.globalTakeInt$address()); + } + + @Test + @Disabled("Upcalls not yet implemented in new scheme") + @SuppressWarnings({"Convert2Lambda", "Convert2MethodRef"}) + void call_globalCallMeRunnable() { + CountDownLatch countDownLatch = new CountDownLatch(3); + + MySwiftLibrary.globalCallMeRunnable(new Runnable() { + @Override + public void run() { + countDownLatch.countDown(); + } + }); + assertEquals(2, countDownLatch.getCount()); + + MySwiftLibrary.globalCallMeRunnable(() -> countDownLatch.countDown()); + assertEquals(1, countDownLatch.getCount()); + + MySwiftLibrary.globalCallMeRunnable(countDownLatch::countDown); + assertEquals(0, countDownLatch.getCount()); + } + +} diff --git a/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/MySwiftClassTest.java b/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/MySwiftClassTest.java new file mode 100644 index 00000000..633d5f1c --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/MySwiftClassTest.java @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.swiftkit; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.example.swift.MySwiftClass; + +public class MySwiftClassTest { + + @Test + void call_retain_retainCount_release() { + var arena = SwiftArena.ofConfined(); + var obj = new MySwiftClass(arena, 1, 2); + + assertEquals(1, SwiftKit.retainCount(obj.$memorySegment())); + // TODO: test directly on SwiftHeapObject inheriting obj + + SwiftKit.retain(obj.$memorySegment()); + assertEquals(2, SwiftKit.retainCount(obj.$memorySegment())); + + SwiftKit.release(obj.$memorySegment()); + assertEquals(1, SwiftKit.retainCount(obj.$memorySegment())); + } +} diff --git a/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/SwiftArenaTest.java b/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/SwiftArenaTest.java new file mode 100644 index 00000000..ad514e1b --- /dev/null +++ b/Samples/SwiftAndJavaJarSampleLib/src/test/java/org/swift/swiftkit/SwiftArenaTest.java @@ -0,0 +1,75 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.swiftkit; + +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 { + + static boolean isAmd64() { + return PlatformUtils.isAmd64(); + } + + // FIXME: The destroy witness table call hangs on x86_64 platforms during the destroy witness table call + // See: https://github.com/swiftlang/swift-java/issues/97 + @Test + @DisabledIf("isAmd64") + public void arena_releaseClassOnClose_class_ok() { + try (var arena = SwiftArena.ofConfined()) { + var obj = new MySwiftClass(arena,1, 2); + + retain(obj.$memorySegment()); + assertEquals(2, retainCount(obj.$memorySegment())); + + release(obj.$memorySegment()); + assertEquals(1, retainCount(obj.$memorySegment())); + } + + // TODO: should we zero out the $memorySegment perhaps? + } + + @Test + public void arena_releaseClassOnClose_class_leaked() { + String memorySegmentDescription = ""; + + try { + try (var arena = SwiftArena.ofConfined()) { + var obj = new MySwiftClass(arena,1, 2); + memorySegmentDescription = obj.$memorySegment().toString(); + + // Pretend that we "leaked" the class, something still holds a reference to it while we try to destroy it + retain(obj.$memorySegment()); + assertEquals(2, retainCount(obj.$memorySegment())); + } + + fail("Expected exception to be thrown while the arena is closed!"); + } catch (Exception ex) { + // The message should point out which objects "leaked": + assertTrue(ex.getMessage().contains(memorySegmentDescription)); + } + + } +} diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift index cb9fd7ed..5a367840 100644 --- a/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift +++ b/Sources/JExtractSwift/Swift2JavaTranslator+Printing.swift @@ -325,9 +325,9 @@ 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); + SwiftKit.loadLibrary("swiftCore"); + SwiftKit.loadLibrary("SwiftKitSwift"); + SwiftKit.loadLibrary(LIB_NAME); if (PlatformUtils.isMacOS()) { return SymbolLookup.libraryLookup(System.mapLibraryName(LIB_NAME), LIBRARY_ARENA) diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java b/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java index 1951c23e..858f5500 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java +++ b/SwiftKit/src/main/java/org/swift/swiftkit/SwiftKit.java @@ -16,8 +16,14 @@ import org.swift.swiftkit.util.PlatformUtils; +import java.io.File; +import java.io.IOException; import java.lang.foreign.*; import java.lang.invoke.MethodHandle; +import java.nio.file.CopyOption; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.Optional; import java.util.stream.Collectors; @@ -63,31 +69,31 @@ private static SymbolLookup getSymbolLookup() { public SwiftKit() { } - 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); - } + 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) @@ -102,6 +108,42 @@ public static boolean getJextractTraceDowncalls() { return Boolean.getBoolean("jextract.trace.downcalls"); } + // ==== ------------------------------------------------------------------------------------------------------------ + // Loading libraries + + public static void loadLibrary(String libname) { + // TODO: avoid concurrent loadResource calls; one load is enough esp since we cause File IO when we do that + try { + // try to load a dylib from our classpath, e.g. when we included it in our jar + loadResourceLibrary(libname); + } catch (UnsatisfiedLinkError | RuntimeException e) { + // fallback to plain system path loading + System.loadLibrary(libname); + } + } + + public static void loadResourceLibrary(String libname) { + var resourceName = PlatformUtils.dynamicLibraryName(libname); + if (SwiftKit.TRACE_DOWNCALLS) { + System.out.println("[swift-java] Loading resource library: " + resourceName); + } + + try (var libInputStream = SwiftKit.class.getResourceAsStream("/" + resourceName)) { + if (libInputStream == null) { + throw new RuntimeException("Expected library '" + libname + "' ('" + resourceName + "') was not found as resource!"); + } + + // TODO: we could do an in memory file system here + var tempFile = File.createTempFile(libname, ""); + tempFile.deleteOnExit(); + Files.copy(libInputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + + System.load(tempFile.getAbsolutePath()); + } catch (IOException e) { + throw new RuntimeException("Failed to load dynamic library '" + libname + "' ('" + resourceName + "') as resource!", e); + } + } + // ==== ------------------------------------------------------------------------------------------------------------ // free @@ -396,7 +438,6 @@ public static long getSwiftInt(MemorySegment memorySegment, long offset) { } - private static class swift_getTypeName { /** diff --git a/SwiftKit/src/main/java/org/swift/swiftkit/util/PlatformUtils.java b/SwiftKit/src/main/java/org/swift/swiftkit/util/PlatformUtils.java index 2c51143f..6addb31d 100644 --- a/SwiftKit/src/main/java/org/swift/swiftkit/util/PlatformUtils.java +++ b/SwiftKit/src/main/java/org/swift/swiftkit/util/PlatformUtils.java @@ -35,4 +35,19 @@ public static boolean isAmd64() { String arch = System.getProperty("os.arch"); return arch.equals("amd64") || arch.equals("x86_64"); } + + public static String dynamicLibraryName(String base) { + if (isLinux()) { + return "lib" + uppercaseFistLetter(base) + ".so"; + } else { + return "lib" + uppercaseFistLetter(base) + ".dylib"; + } + } + + static String uppercaseFistLetter(String base) { + if (base == null || base.isEmpty()) { + return base; + } + return base.substring(0, 1).toUpperCase() + base.substring(1); + } }