diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift index 299626213..3e60db148 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator.swift @@ -278,6 +278,9 @@ extension FFMSwift2JavaGenerator { parentProtocol = "SwiftValue" } + if decl.swiftNominal.isSendable { + printer.print("@ThreadSafe // Sendable") + } printer.printBraceBlock("public final class \(decl.swiftNominal.name) extends FFMSwiftInstance implements \(parentProtocol)") { printer in // Constants diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 63b9dcd1e..b91588df2 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -171,6 +171,9 @@ extension JNISwift2JavaGenerator { private func printNominal( _ printer: inout CodePrinter, _ decl: ImportedNominalType, body: (inout CodePrinter) -> Void ) { + if decl.swiftNominal.isSendable { + printer.print("@ThreadSafe // Sendable") + } printer.printBraceBlock("public final class \(decl.swiftNominal.name) extends JNISwiftInstance") { printer in body(&printer) } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift index b377fd858..cef4e7311 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftNominalTypeDeclaration.swift @@ -85,6 +85,22 @@ package class SwiftNominalTypeDeclaration: SwiftTypeDeclaration { super.init(moduleName: moduleName, name: node.name.text) } + /// Returns true if this type conforms to `Sendable` and therefore is "threadsafe". + lazy var isSendable: Bool = { + // Check if Sendable is in the inheritance list + guard let inheritanceClause = self.syntax?.inheritanceClause else { + return false + } + + for inheritedType in inheritanceClause.inheritedTypes { + if inheritedType.type.trimmedDescription == "Sendable" { + return true + } + } + + return false + }() + /// Determine the known standard library type for this nominal type /// declaration. private func computeKnownStandardLibraryType() -> SwiftKnownTypeDeclKind? { diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/ThreadSafe.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/ThreadSafe.java new file mode 100644 index 000000000..2e62a8b66 --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/annotations/ThreadSafe.java @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// +// 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.core.annotations; + +import jdk.jfr.Description; +import jdk.jfr.Label; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; + +/** + * Used to mark a type as thread-safe, i.e. no additional synchronization is necessary when accessing it + * from multiple threads. + * + *
In SwiftJava specifically, this attribute is applied when an extracted Swift type conforms to the Swift + * {@code Sendable} protocol, which is a compiler enforced mechanism to enforce thread-safety in Swift. + * + * @see Swift Sendable API documentation. + */ +@Documented +@Label("Thread-safe") +@Description("Value should be interpreted as safe to be shared across threads.") +@Target({TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ThreadSafe { +} diff --git a/Tests/JExtractSwiftTests/SendableTests.swift b/Tests/JExtractSwiftTests/SendableTests.swift new file mode 100644 index 000000000..d6d0d2d6e --- /dev/null +++ b/Tests/JExtractSwiftTests/SendableTests.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 JExtractSwiftLib +import Testing + +final class SendableTests { + let source = + """ + public struct SendableStruct: Sendable {} + """ + + + @Test("Import: Sendable struct (ffm)") + func sendableStruct_ffm() throws { + + try assertOutput( + input: source, .ffm, .java, + expectedChunks: [ + """ + @ThreadSafe // Sendable + public final class SendableStruct extends FFMSwiftInstance implements SwiftValue { + static final String LIB_NAME = "SwiftModule"; + static final Arena LIBRARY_ARENA = Arena.ofAuto(); + """, + ] + ) + } + + @Test("Import: Sendable struct (jni)") + func sendableStruct_jni() throws { + + try assertOutput( + input: source, .jni, .java, + expectedChunks: [ + """ + @ThreadSafe // Sendable + public final class SendableStruct extends JNISwiftInstance { + static final String LIB_NAME = "SwiftModule"; + """, + ] + ) + } + +} \ No newline at end of file