diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Set.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Set.swift new file mode 100644 index 00000000..41c39186 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/Set.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +public func makeStringSet() -> Set { + ["hello", "world"] +} + +public func stringSet(set: Set) -> Set { + set +} + +public func insertIntoStringSet(set: Set, element: String) -> Set { + var copy = set + copy.insert(element) + return copy +} + +public func makeIntegerSet() -> Set { + [1, 2, 3] +} + +public func integerSet(set: Set) -> Set { + set +} + +public func makeLongSet() -> Set { + [10, 20, 30] +} + +public func longSet(set: Set) -> Set { + set +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftSetTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftSetTest.java new file mode 100644 index 00000000..13490ef6 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/SwiftSetTest.java @@ -0,0 +1,144 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.collections.SwiftSet; +import org.swift.swiftkit.core.SwiftArena; + +import static org.junit.jupiter.api.Assertions.*; + +public class SwiftSetTest { + @Test + void makeStringSet() { + try (var arena = SwiftArena.ofConfined()) { + SwiftSet set = MySwiftLibrary.makeStringSet(arena); + assertEquals(2, set.size()); + assertTrue(set.contains("hello")); + assertTrue(set.contains("world")); + assertFalse(set.contains("missing")); + } + } + + @Test + void stringSetRoundtrip() { + try (var arena = SwiftArena.ofConfined()) { + SwiftSet original = MySwiftLibrary.makeStringSet(arena); + SwiftSet roundtripped = MySwiftLibrary.stringSet(original, arena); + assertEquals(original.size(), roundtripped.size()); + assertTrue(roundtripped.contains("hello")); + assertTrue(roundtripped.contains("world")); + } + } + + @Test + void insertIntoStringSet() { + try (var arena = SwiftArena.ofConfined()) { + SwiftSet original = MySwiftLibrary.makeStringSet(arena); + assertEquals(2, original.size()); + + // Insert a new element by passing the set through Swift + SwiftSet modified = + MySwiftLibrary.insertIntoStringSet(original, "swift", arena); + + // The modified set has the new element + assertEquals(3, modified.size()); + assertTrue(modified.contains("hello")); + assertTrue(modified.contains("world")); + assertTrue(modified.contains("swift")); + + // The original set is unchanged (Swift value semantics — it's a copy) + assertEquals(2, original.size()); + assertFalse(original.contains("swift")); + } + } + + @Test + void toJavaSet() { + Set javaSet; + try (var arena = SwiftArena.ofConfined()) { + SwiftSet set = MySwiftLibrary.makeStringSet(arena); + javaSet = set.toJavaSet(); + + // The copy has the same contents as the original + assertEquals(2, javaSet.size()); + assertTrue(javaSet.contains("hello")); + assertTrue(javaSet.contains("world")); + assertFalse(javaSet.contains("missing")); + + // It's a plain HashSet, not the native-backed set + assertInstanceOf(HashSet.class, javaSet); + } + + // The Java set copy survives arena closure + assertEquals(2, javaSet.size()); + assertTrue(javaSet.contains("hello")); + assertTrue(javaSet.contains("world")); + } + + // ==== Swift Set -> Java Set tests ==== + + @Test + void makeIntegerSet() { + try (var arena = SwiftArena.ofConfined()) { + SwiftSet set = MySwiftLibrary.makeIntegerSet(arena); + assertEquals(3, set.size()); + assertTrue(set.contains(1)); + assertTrue(set.contains(2)); + assertTrue(set.contains(3)); + assertFalse(set.contains(42)); + } + } + + @Test + void integerSetRoundtrip() { + try (var arena = SwiftArena.ofConfined()) { + SwiftSet original = MySwiftLibrary.makeIntegerSet(arena); + SwiftSet roundtripped = MySwiftLibrary.integerSet(original, arena); + assertEquals(original.size(), roundtripped.size()); + assertTrue(roundtripped.contains(1)); + assertTrue(roundtripped.contains(2)); + assertTrue(roundtripped.contains(3)); + } + } + + // ==== Swift Set -> Java Set tests ==== + + @Test + void makeLongSet() { + try (var arena = SwiftArena.ofConfined()) { + SwiftSet set = MySwiftLibrary.makeLongSet(arena); + assertEquals(3, set.size()); + assertTrue(set.contains(10L)); + assertTrue(set.contains(20L)); + assertTrue(set.contains(30L)); + assertFalse(set.contains(99L)); + } + } + + @Test + void longSetRoundtrip() { + try (var arena = SwiftArena.ofConfined()) { + SwiftSet original = MySwiftLibrary.makeLongSet(arena); + SwiftSet roundtripped = MySwiftLibrary.longSet(original, arena); + assertEquals(original.size(), roundtripped.size()); + assertTrue(roundtripped.contains(10L)); + assertTrue(roundtripped.contains(20L)); + assertTrue(roundtripped.contains(30L)); + } + } +} diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift index 278bb418..d2eeb58d 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift @@ -70,7 +70,7 @@ extension CType { case .optional(let wrapped) where wrapped.isPointer: try self.init(cdeclType: wrapped) - case .genericParameter, .metatype, .optional, .tuple, .opaque, .existential, .composite, .array, .dictionary: + case .genericParameter, .metatype, .optional, .tuple, .opaque, .existential, .composite, .array, .dictionary, .set: throw CDeclToCLoweringError.invalidCDeclType(cdeclType) } } diff --git a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift index 1e5536fa..d8406016 100644 --- a/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift +++ b/Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift @@ -438,6 +438,9 @@ struct CdeclLowering { case .dictionary: throw LoweringError.unhandledType(type) + + case .set: + throw LoweringError.unhandledType(type) } } @@ -530,7 +533,7 @@ struct CdeclLowering { } throw LoweringError.unhandledType(.optional(wrappedType)) - case .function, .metatype, .optional, .composite, .array, .dictionary: + case .function, .metatype, .optional, .composite, .array, .dictionary, .set: throw LoweringError.unhandledType(.optional(wrappedType)) } } @@ -632,7 +635,7 @@ struct CdeclLowering { // Custom types are not supported yet. throw LoweringError.unhandledType(type) - case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .array, .dictionary: + case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .array, .dictionary, .set: // TODO: Implement throw LoweringError.unhandledType(type) } @@ -835,7 +838,7 @@ struct CdeclLowering { ) ) - case .genericParameter, .function, .optional, .existential, .opaque, .composite, .array, .dictionary: + case .genericParameter, .function, .optional, .existential, .opaque, .composite, .array, .dictionary, .set: throw LoweringError.unhandledType(type) } } diff --git a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift index 5e99b607..166709d3 100644 --- a/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift @@ -511,6 +511,9 @@ extension FFMSwift2JavaGenerator { case .dictionary: throw JavaTranslationError.unhandledType(swiftType) + + case .set: + throw JavaTranslationError.unhandledType(swiftType) } } @@ -742,7 +745,7 @@ extension FFMSwift2JavaGenerator { ) ) - case .genericParameter, .optional, .function, .existential, .opaque, .composite, .array, .dictionary: + case .genericParameter, .optional, .function, .existential, .opaque, .composite, .array, .dictionary, .set: throw JavaTranslationError.unhandledType(swiftType) } diff --git a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift index d7f8a90b..e20decb6 100644 --- a/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift @@ -50,6 +50,7 @@ enum JNIJavaTypeTranslator { .essentialsData, .essentialsDataProtocol, .array, .dictionary, + .set, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: return nil @@ -75,6 +76,7 @@ enum JNIJavaTypeTranslator { .essentialsData, .essentialsDataProtocol, .array, .dictionary, + .set, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: nil @@ -100,6 +102,7 @@ enum JNIJavaTypeTranslator { .essentialsData, .essentialsDataProtocol, .array, .dictionary, + .set, .foundationDate, .essentialsDate, .foundationUUID, .essentialsUUID: nil diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift index e1f5887c..55db4954 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift @@ -241,6 +241,9 @@ extension JNISwift2JavaGenerator { case .dictionary: throw JavaTranslationError.unsupportedSwiftType(type) + case .set: + throw JavaTranslationError.unsupportedSwiftType(type) + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite: throw JavaTranslationError.unsupportedSwiftType(type) } @@ -259,7 +262,7 @@ extension JNISwift2JavaGenerator { ) ) - case .array, .dictionary, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple: + case .array, .dictionary, .set, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple: throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) } } @@ -330,6 +333,9 @@ extension JNISwift2JavaGenerator { case .dictionary: throw JavaTranslationError.unsupportedSwiftType(type) + case .set: + throw JavaTranslationError.unsupportedSwiftType(type) + case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite: throw JavaTranslationError.unsupportedSwiftType(type) } @@ -348,7 +354,7 @@ extension JNISwift2JavaGenerator { ) ) - case .array, .dictionary, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple: + case .array, .dictionary, .set, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple: throw JavaTranslationError.unsupportedSwiftType(.array(elementType)) } } @@ -484,7 +490,7 @@ extension SwiftType { case .array(let elementType): return elementType.isDirectlyTranslatedToWrapJava - case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .dictionary: + case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .dictionary, .set: return false } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index fe72f2dd..c0befa5e 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -485,6 +485,15 @@ extension JNISwift2JavaGenerator { parameterName: parameterName ) + case .set: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.setRequiresElementType(swiftType) + } + return try translateSetParameter( + elementType: genericArgs[0], + parameterName: parameterName + ) + case .foundationDate, .essentialsDate: break // Handled as wrapped struct @@ -590,6 +599,12 @@ extension JNISwift2JavaGenerator { parameterName: parameterName ) + case .set(let elementType): + return try translateSetParameter( + elementType: elementType, + parameterName: parameterName + ) + case .metatype: return TranslatedParameter( parameter: JavaParameter(name: parameterName, type: .long), @@ -889,6 +904,14 @@ extension JNISwift2JavaGenerator { valueType: genericArgs[1] ) + case .set: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.setRequiresElementType(swiftType) + } + return try translateSetResult( + elementType: genericArgs[0] + ) + case .foundationDate, .essentialsDate: // Handled as wrapped struct break @@ -974,6 +997,11 @@ extension JNISwift2JavaGenerator { valueType: valueType ) + case .set(let elementType): + return try translateSetResult( + elementType: elementType + ) + case .tuple(let elements) where !elements.isEmpty: return try translateTupleResult(elements: elements, resultName: resultName) @@ -1293,6 +1321,36 @@ extension JNISwift2JavaGenerator { conversion: .wrapMemoryAddressUnsafe(.placeholder, dictType) ) } + + func translateSetParameter( + elementType: SwiftType, + parameterName: String + ) throws -> TranslatedParameter { + let elementJavaType = try javaTypeForDictionaryComponent(elementType) + let setType = JavaType.swiftSet(elementJavaType) + + return TranslatedParameter( + parameter: JavaParameter(name: parameterName, type: setType), + conversion: .method( + .requireNonNull(.placeholder, message: "\(parameterName) must not be null"), + function: "$memoryAddress", + arguments: [] + ) + ) + } + + func translateSetResult( + elementType: SwiftType + ) throws -> TranslatedResult { + let elementJavaType = try javaTypeForDictionaryComponent(elementType) + let setType = JavaType.swiftSet(elementJavaType) + + return TranslatedResult( + javaType: setType, + outParameters: [], + conversion: .wrapMemoryAddressUnsafe(.placeholder, setType) + ) + } } struct TranslatedEnumCase { @@ -1788,5 +1846,8 @@ extension JNISwift2JavaGenerator { /// Dictionary type requires exactly two generic type arguments (key and value). case dictionaryRequiresKeyAndValueTypes(SwiftType) + + /// Set type requires exactly one generic type argument (element). + case setRequiresElementType(SwiftType) } } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index dc0ecde9..6c78c289 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -127,6 +127,15 @@ extension JNISwift2JavaGenerator { parameterName: parameterName ) + case .set: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.setRequiresElementType(type) + } + return try translateSetParameter( + elementType: genericArgs[0], + parameterName: parameterName + ) + case .foundationDate, .essentialsDate, .foundationData, .essentialsData: // Handled as wrapped struct break @@ -324,6 +333,12 @@ extension JNISwift2JavaGenerator { parameterName: parameterName ) + case .set(let elementType): + return try translateSetParameter( + elementType: elementType, + parameterName: parameterName + ) + case .metatype: return NativeParameter( parameters: [ @@ -656,7 +671,7 @@ extension JNISwift2JavaGenerator { outParameters: [] ) - case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite, .array, .dictionary: + case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite, .array, .dictionary, .set: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -688,7 +703,7 @@ extension JNISwift2JavaGenerator { // Custom types are not supported yet. throw JavaTranslationError.unsupportedSwiftType(type) - case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite, .array, .dictionary: + case .function, .metatype, .optional, .tuple, .existential, .opaque, .genericParameter, .composite, .array, .dictionary, .set: throw JavaTranslationError.unsupportedSwiftType(type) } } @@ -723,6 +738,15 @@ extension JNISwift2JavaGenerator { resultName: resultName ) + case .set: + guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else { + throw JavaTranslationError.setRequiresElementType(swiftResult.type) + } + return try translateSetResult( + elementType: genericArgs[0], + resultName: resultName + ) + case .foundationDate, .essentialsDate, .foundationData, .essentialsData: // Handled as wrapped struct break @@ -802,6 +826,12 @@ extension JNISwift2JavaGenerator { resultName: resultName ) + case .set(let elementType): + return try translateSetResult( + elementType: elementType, + resultName: resultName + ) + case .tuple(let elements) where !elements.isEmpty: return try translateTupleResult(elements: elements, resultName: resultName) @@ -1010,6 +1040,35 @@ extension JNISwift2JavaGenerator { outParameters: [] ) } + + func translateSetParameter( + elementType: SwiftType, + parameterName: String + ) throws -> NativeParameter { + NativeParameter( + parameters: [ + JavaParameter(name: parameterName, type: .long) + ], + conversion: .initFromJNI(.placeholder, swiftType: .set(element: elementType)), + indirectConversion: nil, + conversionCheck: nil + ) + } + + func translateSetResult( + elementType: SwiftType, + resultName: String + ) throws -> NativeResult { + NativeResult( + javaType: .long, + conversion: .method( + .placeholder, + function: "setGetJNIValue", + arguments: [("in", .constant("environment"))] + ), + outParameters: [] + ) + } } struct NativeFunctionSignature { diff --git a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift index 7216db05..7a5d6988 100644 --- a/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift +++ b/Sources/JExtractSwiftLib/JavaTypes/JavaType+SwiftKit.swift @@ -39,4 +39,9 @@ extension JavaType { .class(package: "org.swift.swiftkit.core.collections", name: "SwiftDictionaryMap", typeParameters: [K.boxedType, V.boxedType]) } + /// The description of the type org.swift.swiftkit.core.collections.SwiftSet + static func swiftSet(_ E: JavaType) -> JavaType { + .class(package: "org.swift.swiftkit.core.collections", name: "SwiftSet", typeParameters: [E.boxedType]) + } + } diff --git a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift index cf14fb02..6012c6fb 100644 --- a/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift +++ b/Sources/JExtractSwiftLib/Swift2JavaTranslator.swift @@ -170,6 +170,8 @@ extension Swift2JavaTranslator { return check(ty) case .dictionary(let key, let value): return check(key) || check(value) + case .set(let element): + return check(element) } } diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift index 9354447d..696b1f1a 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownModules.swift @@ -100,6 +100,8 @@ private let swiftSourceFile: SourceFileSyntax = """ public struct Dictionary {} + public struct Set {} + // FIXME: Support 'typealias Void = ()' public struct Void {} diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift index 347bc910..2b41e12b 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftKnownTypeDecls.swift @@ -42,6 +42,7 @@ enum SwiftKnownTypeDeclKind: String, Hashable { case string = "Swift.String" case array = "Swift.Array" case dictionary = "Swift.Dictionary" + case set = "Swift.Set" // Foundation case foundationDataProtocol = "Foundation.DataProtocol" diff --git a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift index c5ac433f..05dd3be7 100644 --- a/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift +++ b/Sources/JExtractSwiftLib/SwiftTypes/SwiftType.swift @@ -59,6 +59,9 @@ enum SwiftType: Equatable { /// `[key: value]` indirect case dictionary(key: SwiftType, value: SwiftType) + /// `Set` + indirect case set(element: SwiftType) + static var void: Self { .tuple([]) } @@ -67,7 +70,7 @@ enum SwiftType: Equatable { switch self { case .nominal(let nominal): nominal case .tuple(let elements): elements.count == 1 ? elements[0].type.asNominalType : nil - case .genericParameter, .function, .metatype, .optional, .existential, .opaque, .composite, .array, .dictionary: nil + case .genericParameter, .function, .metatype, .optional, .existential, .opaque, .composite, .array, .dictionary, .set: nil } } @@ -111,7 +114,7 @@ enum SwiftType: Equatable { return nominal.nominalTypeDecl.isReferenceType case .metatype, .function: return true - case .genericParameter, .optional, .tuple, .existential, .opaque, .composite, .array, .dictionary: + case .genericParameter, .optional, .tuple, .existential, .opaque, .composite, .array, .dictionary, .set: return false } } @@ -158,7 +161,7 @@ extension SwiftType: CustomStringConvertible { private var postfixRequiresParentheses: Bool { switch self { case .function, .existential, .opaque, .composite: true - case .genericParameter, .metatype, .nominal, .optional, .tuple, .array, .dictionary: false + case .genericParameter, .metatype, .nominal, .optional, .tuple, .array, .dictionary, .set: false } } @@ -187,6 +190,8 @@ extension SwiftType: CustomStringConvertible { return "[\(type)]" case .dictionary(let key, let value): return "[\(key): \(value)]" + case .set(let element): + return "Set<\(element)>" } } } diff --git a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift index 833200a3..00efeb50 100644 --- a/Sources/SwiftJava/BridgedValues/JavaBoxing.swift +++ b/Sources/SwiftJava/BridgedValues/JavaBoxing.swift @@ -336,3 +336,51 @@ final class SwiftDictionaryBox: AnySw return result } } + +// ==== ----------------------------------------------------------------------- +// MARK: SwiftSetBox (type-erased base + generic subclass) + +/// Non-generic base class for set boxes, allowing dispatch +/// from @_cdecl JNI functions without knowing the concrete element type. +/// +/// Note: This must be a class (not a protocol) because instances are stored +/// via `Unmanaged` in a raw pointer passed across the JNI boundary. +class AnySwiftSetBox { + func size() -> Int { fatalError("abstract") } + func contains(element: jobject?, environment: JNIEnvironment) -> Bool { fatalError("abstract") } + func toArray(environment: JNIEnvironment) -> jobject? { fatalError("abstract") } + func setAsAny() -> Any { fatalError("abstract") } +} + +/// Generic subclass that wraps a concrete `Set` Swift set. +final class SwiftSetBox: AnySwiftSetBox { + let set: Set + + init(_ set: Set) { + self.set = set + } + + override func size() -> Int { + set.count + } + + override func setAsAny() -> Any { + set + } + + override func contains(element: jobject?, environment: JNIEnvironment) -> Bool { + let swiftElement = E.fromJavaObject(element, in: environment) + return set.contains(swiftElement) + } + + override func toArray(environment: JNIEnvironment) -> jobject? { + let elements = Array(set) + let objectClass = environment.interface.FindClass(environment, "java/lang/Object") + let result = environment.interface.NewObjectArray(environment, jsize(elements.count), objectClass, nil) + for (i, element) in elements.enumerated() { + let javaElement = element.toJavaObject(in: environment) + environment.interface.SetObjectArrayElement(environment, result, jsize(i), javaElement) + } + return result + } +} diff --git a/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift b/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift new file mode 100644 index 00000000..2adb7d38 --- /dev/null +++ b/Sources/SwiftJava/BridgedValues/JavaValue+Set.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJavaJNICore + +// ==== ----------------------------------------------------------------------- +// MARK: Set extension for JNI bridging + +extension Set where Element: JavaBoxable & Hashable { + /// Box this set and return a jlong pointer for passing across JNI. + /// The set is retained on the Swift heap; Java holds the pointer. + public func setGetJNIValue(in environment: JNIEnvironment) -> jlong { + let box = SwiftSetBox(self) + let unmanaged = Unmanaged.passRetained(box) + let rawPointer = unmanaged.toOpaque() + return jlong(Int(bitPattern: rawPointer)) + } + + /// Reconstruct a Swift set from a JNI jlong pointer to a SwiftSetBox. + public init(fromJNI value: jlong, in environment: JNIEnvironment) { + let rawPointer = UnsafeRawPointer(bitPattern: Int(value))! + let box = Unmanaged>.fromOpaque(rawPointer).takeUnretainedValue() + self = box.set + } +} diff --git a/Sources/SwiftJava/BridgedValues/SwiftSet+JNI.swift b/Sources/SwiftJava/BridgedValues/SwiftSet+JNI.swift new file mode 100644 index 00000000..2aa717a4 --- /dev/null +++ b/Sources/SwiftJava/BridgedValues/SwiftSet+JNI.swift @@ -0,0 +1,59 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJavaJNICore + +@JavaClass("org.swift.swiftkit.core.collections.SwiftSet") +open class SwiftSetJava: JavaObject { +} + +@JavaImplementation("org.swift.swiftkit.core.collections.SwiftSet") +extension SwiftSetJava { + + private static func setBox(from pointer: Int64) -> AnySwiftSetBox { + let rawPointer = UnsafeRawPointer(bitPattern: Int(pointer))! + return Unmanaged.fromOpaque(rawPointer).takeUnretainedValue() + } + + @JavaMethod("$size") + public static func _setSize(environment: UnsafeMutablePointer!, pointer: Int64) -> Int32 { + Int32(setBox(from: pointer).size()) + } + + @JavaMethod("$contains") + public static func _setContains(environment: UnsafeMutablePointer!, pointer: Int64, element: JavaObject?) -> Bool { + let jElement = element?.javaThis + return setBox(from: pointer).contains(element: jElement, environment: environment) + } + + @JavaMethod("$toArray") + public static func _setToArray(environment: UnsafeMutablePointer!, pointer: Int64) -> JavaObject? { + guard let result = setBox(from: pointer).toArray(environment: environment) else { return nil } + return JavaObject(javaThis: result, environment: environment) + } + + @JavaMethod("$destroy") + public static func _setDestroy(environment: UnsafeMutablePointer!, pointer: Int64) { + let rawPointer = UnsafeRawPointer(bitPattern: Int(pointer))! + Unmanaged.fromOpaque(rawPointer).release() + } + + @JavaMethod("$typeMetadataAddress") + public static func _setTypeMetadataAddress(environment: UnsafeMutablePointer!, pointer: Int64) -> Int64 { + let set = setBox(from: pointer).setAsAny() + let metatype = type(of: set) + let metadataPointer = unsafeBitCast(metatype, to: UnsafeRawPointer.self) + return Int64(Int(bitPattern: metadataPointer)) + } +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/collections/SwiftSet.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/collections/SwiftSet.java new file mode 100644 index 00000000..a793530e --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/collections/SwiftSet.java @@ -0,0 +1,128 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.core.collections; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.swift.swiftkit.core.*; + +/** + * A Java {@link java.util.Set} backed by a Swift Set living in Swift's native heap memory. + * This avoids un-necessary copying of the whole collection in case we're interested only in a few of its elements. + *

+ * Methods on this type are implemented as JNI downcalls into the native Swift set, unless specified otherwise. + *

+ * You can use {@link #toJavaSet()} to obtain a copy of the data structure on the Java heap. + * + * @param the element type, must be a value representable in Swift + */ +public class SwiftSet extends AbstractSet implements JNISwiftInstance { + + private final long selfPointer; + private final AtomicBoolean destroyed = new AtomicBoolean(false); + + private SwiftSet(long selfPointer) { + this.selfPointer = selfPointer; + } + + public static SwiftSet wrapMemoryAddressUnsafe(long selfPointer, SwiftArena arena) { + SwiftSet set = new SwiftSet<>(selfPointer); + arena.register(set); + return set; + } + + @Override + public long $memoryAddress() { + $ensureAlive(); + return selfPointer; + } + + @Override + public long $typeMetadataAddress() { + $ensureAlive(); + return $typeMetadataAddress(selfPointer); + } + + @Override + public AtomicBoolean $statusDestroyedFlag() { + return destroyed; + } + + @Override + public Runnable $createDestroyFunction() { + final long p = this.selfPointer; + return () -> $destroy(p); + } + + // === Set interface === + + @Override + public int size() { + $ensureAlive(); + return $size(selfPointer); + } + + @Override + public boolean contains(Object o) { + $ensureAlive(); + return $contains(selfPointer, o); + } + + @Override + @SuppressWarnings("unchecked") + public Iterator iterator() { + $ensureAlive(); + Object[] elements = $toArray(selfPointer); + return new Iterator() { + private int index = 0; + + @Override + public boolean hasNext() { + return index < elements.length; + } + + @Override + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return (E) elements[index++]; + } + }; + } + + /** + * Make a copy of the set into a Java heap {@link java.util.Set}, + * which may be preferable if you are going to perform many operations on the set + * and don't expect the changes to be reflected in Swift. + *

+ * This operation DOES NOT perform a deep copy. I.e. if the set contained reference types, + * the new set will keep pointing at the same objects in the Swift heap. + * + * @return A copy of Swift Set on the Java heap, detached from the Swift Set's lifetime + */ + public Set toJavaSet() { + return new HashSet<>(this); + } + + // ==== Native methods + + private static native int $size(long selfPointer); + private static native boolean $contains(long selfPointer, Object element); + private static native Object[] $toArray(long selfPointer); + private static native void $destroy(long selfPointer); + private static native long $typeMetadataAddress(long selfPointer); +} diff --git a/Tests/JExtractSwiftTests/JNI/JNISetTest.swift b/Tests/JExtractSwiftTests/JNI/JNISetTest.swift new file mode 100644 index 00000000..4a70fdcd --- /dev/null +++ b/Tests/JExtractSwiftTests/JNI/JNISetTest.swift @@ -0,0 +1,270 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import JExtractSwiftLib +import Testing + +@Suite +struct JNISetTest { + + @Test("Import: () -> Set (Java)") + func stringSet_result_java() throws { + try assertOutput( + input: "public func f() -> Set {}", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static org.swift.swiftkit.core.collections.SwiftSet f(SwiftArena swiftArena) { + return SwiftSet.wrapMemoryAddressUnsafe(SwiftModule.$f(), swiftArena); + } + """, + """ + private static native long $f(); + """, + ] + ) + } + + @Test("Import: () -> Set (Swift)") + func stringSet_result_swift() throws { + try assertOutput( + input: "public func f() -> Set {}", + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f__") + public func Java_com_example_swift_SwiftModule__00024f__(environment: UnsafeMutablePointer!, thisClass: jclass) -> jlong { + return SwiftModule.f().setGetJNIValue(in: environment) + } + """ + ] + ) + } + + @Test("Import: (Set) -> Void (Java)") + func stringSet_param_java() throws { + try assertOutput( + input: "public func f(set: Set) {}", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static void f(org.swift.swiftkit.core.collections.SwiftSet set) { + SwiftModule.$f(Objects.requireNonNull(set, "set must not be null").$memoryAddress()); + } + """, + """ + private static native void $f(long set); + """, + ] + ) + } + + @Test("Import: (Set) -> Void (Swift)") + func stringSet_param_swift() throws { + try assertOutput( + input: "public func f(set: Set) {}", + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f__J") + public func Java_com_example_swift_SwiftModule__00024f__J(environment: UnsafeMutablePointer!, thisClass: jclass, set: jlong) { + SwiftModule.f(set: Set(fromJNI: set, in: environment)) + } + """ + ] + ) + } + + @Test("Import: (Set) -> Set (Java)") + func stringSet_roundtrip_java() throws { + try assertOutput( + input: "public func f(set: Set) -> Set {}", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static org.swift.swiftkit.core.collections.SwiftSet f(org.swift.swiftkit.core.collections.SwiftSet set, SwiftArena swiftArena) { + return SwiftSet.wrapMemoryAddressUnsafe(SwiftModule.$f(Objects.requireNonNull(set, "set must not be null").$memoryAddress()), swiftArena); + } + """, + """ + private static native long $f(long set); + """, + ] + ) + } + + @Test("Import: (Set) -> Set (Swift)") + func stringSet_roundtrip_swift() throws { + try assertOutput( + input: "public func f(set: Set) -> Set {}", + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f__J") + public func Java_com_example_swift_SwiftModule__00024f__J(environment: UnsafeMutablePointer!, thisClass: jclass, set: jlong) -> jlong { + return SwiftModule.f(set: Set(fromJNI: set, in: environment)).setGetJNIValue(in: environment) + } + """ + ] + ) + } + + // ==== ------------------------------------------------------------------- + // MARK: Different element types + + @Test("Import: () -> Set (Java)") + func int64Set_result_java() throws { + try assertOutput( + input: "public func f() -> Set {}", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static org.swift.swiftkit.core.collections.SwiftSet f(SwiftArena swiftArena) { + return SwiftSet.wrapMemoryAddressUnsafe(SwiftModule.$f(), swiftArena); + } + """ + ] + ) + } + + @Test("Import: () -> Set (Java)") + func boolSet_result_java() throws { + try assertOutput( + input: "public func f() -> Set {}", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static org.swift.swiftkit.core.collections.SwiftSet f(SwiftArena swiftArena) { + return SwiftSet.wrapMemoryAddressUnsafe(SwiftModule.$f(), swiftArena); + } + """ + ] + ) + } + + @Test("Import: () -> Set (Java)") + func doubleSet_result_java() throws { + try assertOutput( + input: "public func f() -> Set {}", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static org.swift.swiftkit.core.collections.SwiftSet f(SwiftArena swiftArena) { + return SwiftSet.wrapMemoryAddressUnsafe(SwiftModule.$f(), swiftArena); + } + """ + ] + ) + } + + // ==== ------------------------------------------------------------------- + // MARK: Multiple set parameters + + @Test("Import: (Set, Set) -> Void (Java)") + func multipleSetParams_java() throws { + try assertOutput( + input: "public func f(a: Set, b: Set) {}", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static void f(org.swift.swiftkit.core.collections.SwiftSet a, org.swift.swiftkit.core.collections.SwiftSet b) { + SwiftModule.$f(Objects.requireNonNull(a, "a must not be null").$memoryAddress(), Objects.requireNonNull(b, "b must not be null").$memoryAddress()); + } + """, + """ + private static native void $f(long a, long b); + """, + ] + ) + } + + @Test("Import: (Set, Set) -> Void (Swift)") + func multipleSetParams_swift() throws { + try assertOutput( + input: "public func f(a: Set, b: Set) {}", + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f__JJ") + public func Java_com_example_swift_SwiftModule__00024f__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, a: jlong, b: jlong) { + SwiftModule.f(a: Set(fromJNI: a, in: environment), b: Set(fromJNI: b, in: environment)) + } + """ + ] + ) + } + + // ==== ------------------------------------------------------------------- + // MARK: Set with other parameter types + + @Test("Import: (Set, String) -> Set (Java)") + func setWithPrimitiveParams_java() throws { + try assertOutput( + input: "public func f(set: Set, element: String) -> Set {}", + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public static org.swift.swiftkit.core.collections.SwiftSet f(org.swift.swiftkit.core.collections.SwiftSet set, java.lang.String element, SwiftArena swiftArena) { + return SwiftSet.wrapMemoryAddressUnsafe(SwiftModule.$f(Objects.requireNonNull(set, "set must not be null").$memoryAddress(), element), swiftArena); + } + """, + """ + private static native long $f(long set, java.lang.String element); + """, + ] + ) + } + + @Test("Import: (Set, String) -> Set (Swift)") + func setWithPrimitiveParams_swift() throws { + try assertOutput( + input: "public func f(set: Set, element: String) -> Set {}", + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024f__JLjava_lang_String_2") + public func Java_com_example_swift_SwiftModule__00024f__JLjava_lang_String_2(environment: UnsafeMutablePointer!, thisClass: jclass, set: jlong, element: jstring?) -> jlong { + return SwiftModule.f(set: Set(fromJNI: set, in: environment), element: String(fromJNI: element, in: environment)).setGetJNIValue(in: environment) + } + """ + ] + ) + } +}