Skip to content

Commit 5af6a20

Browse files
committed
Native Swift Set support for jextract/jni
1 parent dc8aede commit 5af6a20

19 files changed

Lines changed: 823 additions & 13 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
public func makeStringSet() -> Set<String> {
16+
["hello", "world"]
17+
}
18+
19+
public func stringSet(set: Set<String>) -> Set<String> {
20+
set
21+
}
22+
23+
public func insertIntoStringSet(set: Set<String>, element: String) -> Set<String> {
24+
var copy = set
25+
copy.insert(element)
26+
return copy
27+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift.org project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift.org project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
package com.example.swift;
16+
17+
import java.util.HashSet;
18+
import java.util.Set;
19+
import org.junit.jupiter.api.Test;
20+
import org.swift.swiftkit.core.collections.SwiftSet;
21+
import org.swift.swiftkit.core.SwiftArena;
22+
23+
import static org.junit.jupiter.api.Assertions.*;
24+
25+
public class SwiftSetTest {
26+
@Test
27+
void makeStringSet() {
28+
try (var arena = SwiftArena.ofConfined()) {
29+
SwiftSet<String> set = MySwiftLibrary.makeStringSet(arena);
30+
assertEquals(2, set.size());
31+
assertTrue(set.contains("hello"));
32+
assertTrue(set.contains("world"));
33+
assertFalse(set.contains("missing"));
34+
}
35+
}
36+
37+
@Test
38+
void stringSetRoundtrip() {
39+
try (var arena = SwiftArena.ofConfined()) {
40+
SwiftSet<String> original = MySwiftLibrary.makeStringSet(arena);
41+
SwiftSet<String> roundtripped = MySwiftLibrary.stringSet(original, arena);
42+
assertEquals(original.size(), roundtripped.size());
43+
assertTrue(roundtripped.contains("hello"));
44+
assertTrue(roundtripped.contains("world"));
45+
}
46+
}
47+
48+
@Test
49+
void insertIntoStringSet() {
50+
try (var arena = SwiftArena.ofConfined()) {
51+
SwiftSet<String> original = MySwiftLibrary.makeStringSet(arena);
52+
assertEquals(2, original.size());
53+
54+
// Insert a new element by passing the set through Swift
55+
SwiftSet<String> modified =
56+
MySwiftLibrary.insertIntoStringSet(original, "swift", arena);
57+
58+
// The modified set has the new element
59+
assertEquals(3, modified.size());
60+
assertTrue(modified.contains("hello"));
61+
assertTrue(modified.contains("world"));
62+
assertTrue(modified.contains("swift"));
63+
64+
// The original set is unchanged (Swift value semantics — it's a copy)
65+
assertEquals(2, original.size());
66+
assertFalse(original.contains("swift"));
67+
}
68+
}
69+
70+
@Test
71+
void toJavaSet() {
72+
Set<String> javaSet;
73+
try (var arena = SwiftArena.ofConfined()) {
74+
SwiftSet<String> set = MySwiftLibrary.makeStringSet(arena);
75+
javaSet = set.toJavaSet();
76+
77+
// The copy has the same contents as the original
78+
assertEquals(2, javaSet.size());
79+
assertTrue(javaSet.contains("hello"));
80+
assertTrue(javaSet.contains("world"));
81+
assertFalse(javaSet.contains("missing"));
82+
83+
// It's a plain HashSet, not the native-backed set
84+
assertInstanceOf(HashSet.class, javaSet);
85+
}
86+
87+
// The Java set copy survives arena closure
88+
assertEquals(2, javaSet.size());
89+
assertTrue(javaSet.contains("hello"));
90+
assertTrue(javaSet.contains("world"));
91+
}
92+
}

Sources/JExtractSwiftLib/FFM/CDeclLowering/CRepresentation.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ extension CType {
7070
case .optional(let wrapped) where wrapped.isPointer:
7171
try self.init(cdeclType: wrapped)
7272

73-
case .genericParameter, .metatype, .optional, .tuple, .opaque, .existential, .composite, .array, .dictionary:
73+
case .genericParameter, .metatype, .optional, .tuple, .opaque, .existential, .composite, .array, .dictionary, .set:
7474
throw CDeclToCLoweringError.invalidCDeclType(cdeclType)
7575
}
7676
}

Sources/JExtractSwiftLib/FFM/CDeclLowering/FFMSwift2JavaGenerator+FunctionLowering.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,9 @@ struct CdeclLowering {
438438

439439
case .dictionary:
440440
throw LoweringError.unhandledType(type)
441+
442+
case .set:
443+
throw LoweringError.unhandledType(type)
441444
}
442445
}
443446

@@ -530,7 +533,7 @@ struct CdeclLowering {
530533
}
531534
throw LoweringError.unhandledType(.optional(wrappedType))
532535

533-
case .function, .metatype, .optional, .composite, .array, .dictionary:
536+
case .function, .metatype, .optional, .composite, .array, .dictionary, .set:
534537
throw LoweringError.unhandledType(.optional(wrappedType))
535538
}
536539
}
@@ -632,7 +635,7 @@ struct CdeclLowering {
632635
// Custom types are not supported yet.
633636
throw LoweringError.unhandledType(type)
634637

635-
case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .array, .dictionary:
638+
case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .array, .dictionary, .set:
636639
// TODO: Implement
637640
throw LoweringError.unhandledType(type)
638641
}
@@ -835,7 +838,7 @@ struct CdeclLowering {
835838
)
836839
)
837840

838-
case .genericParameter, .function, .optional, .existential, .opaque, .composite, .array, .dictionary:
841+
case .genericParameter, .function, .optional, .existential, .opaque, .composite, .array, .dictionary, .set:
839842
throw LoweringError.unhandledType(type)
840843
}
841844
}

Sources/JExtractSwiftLib/FFM/FFMSwift2JavaGenerator+JavaTranslation.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,9 @@ extension FFMSwift2JavaGenerator {
511511

512512
case .dictionary:
513513
throw JavaTranslationError.unhandledType(swiftType)
514+
515+
case .set:
516+
throw JavaTranslationError.unhandledType(swiftType)
514517
}
515518
}
516519

@@ -742,7 +745,7 @@ extension FFMSwift2JavaGenerator {
742745
)
743746
)
744747

745-
case .genericParameter, .optional, .function, .existential, .opaque, .composite, .array, .dictionary:
748+
case .genericParameter, .optional, .function, .existential, .opaque, .composite, .array, .dictionary, .set:
746749
throw JavaTranslationError.unhandledType(swiftType)
747750
}
748751

Sources/JExtractSwiftLib/JNI/JNIJavaTypeTranslator.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ enum JNIJavaTypeTranslator {
5050
.essentialsData, .essentialsDataProtocol,
5151
.array,
5252
.dictionary,
53+
.set,
5354
.foundationDate, .essentialsDate,
5455
.foundationUUID, .essentialsUUID:
5556
return nil
@@ -75,6 +76,7 @@ enum JNIJavaTypeTranslator {
7576
.essentialsData, .essentialsDataProtocol,
7677
.array,
7778
.dictionary,
79+
.set,
7880
.foundationDate, .essentialsDate,
7981
.foundationUUID, .essentialsUUID:
8082
nil
@@ -100,6 +102,7 @@ enum JNIJavaTypeTranslator {
100102
.essentialsData, .essentialsDataProtocol,
101103
.array,
102104
.dictionary,
105+
.set,
103106
.foundationDate, .essentialsDate,
104107
.foundationUUID, .essentialsUUID:
105108
nil

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+InterfaceWrapperGeneration.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ extension JNISwift2JavaGenerator {
241241
case .dictionary:
242242
throw JavaTranslationError.unsupportedSwiftType(type)
243243

244+
case .set:
245+
throw JavaTranslationError.unsupportedSwiftType(type)
246+
244247
case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite:
245248
throw JavaTranslationError.unsupportedSwiftType(type)
246249
}
@@ -259,7 +262,7 @@ extension JNISwift2JavaGenerator {
259262
)
260263
)
261264

262-
case .array, .dictionary, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple:
265+
case .array, .dictionary, .set, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple:
263266
throw JavaTranslationError.unsupportedSwiftType(.array(elementType))
264267
}
265268
}
@@ -330,6 +333,9 @@ extension JNISwift2JavaGenerator {
330333
case .dictionary:
331334
throw JavaTranslationError.unsupportedSwiftType(type)
332335

336+
case .set:
337+
throw JavaTranslationError.unsupportedSwiftType(type)
338+
333339
case .genericParameter, .function, .metatype, .tuple, .existential, .opaque, .composite:
334340
throw JavaTranslationError.unsupportedSwiftType(type)
335341
}
@@ -348,7 +354,7 @@ extension JNISwift2JavaGenerator {
348354
)
349355
)
350356

351-
case .array, .dictionary, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple:
357+
case .array, .dictionary, .set, .composite, .existential, .function, .genericParameter, .metatype, .opaque, .optional, .tuple:
352358
throw JavaTranslationError.unsupportedSwiftType(.array(elementType))
353359
}
354360
}
@@ -484,7 +490,7 @@ extension SwiftType {
484490
case .array(let elementType):
485491
return elementType.isDirectlyTranslatedToWrapJava
486492

487-
case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .dictionary:
493+
case .genericParameter, .function, .metatype, .optional, .tuple, .existential, .opaque, .composite, .dictionary, .set:
488494
return false
489495
}
490496
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,15 @@ extension JNISwift2JavaGenerator {
485485
parameterName: parameterName
486486
)
487487

488+
case .set:
489+
guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else {
490+
throw JavaTranslationError.setRequiresElementType(swiftType)
491+
}
492+
return try translateSetParameter(
493+
elementType: genericArgs[0],
494+
parameterName: parameterName
495+
)
496+
488497
case .foundationDate, .essentialsDate:
489498
break // Handled as wrapped struct
490499

@@ -590,6 +599,12 @@ extension JNISwift2JavaGenerator {
590599
parameterName: parameterName
591600
)
592601

602+
case .set(let elementType):
603+
return try translateSetParameter(
604+
elementType: elementType,
605+
parameterName: parameterName
606+
)
607+
593608
case .metatype:
594609
return TranslatedParameter(
595610
parameter: JavaParameter(name: parameterName, type: .long),
@@ -889,6 +904,14 @@ extension JNISwift2JavaGenerator {
889904
valueType: genericArgs[1]
890905
)
891906

907+
case .set:
908+
guard let genericArgs = nominalType.genericArguments, genericArgs.count == 1 else {
909+
throw JavaTranslationError.setRequiresElementType(swiftType)
910+
}
911+
return try translateSetResult(
912+
elementType: genericArgs[0]
913+
)
914+
892915
case .foundationDate, .essentialsDate:
893916
// Handled as wrapped struct
894917
break
@@ -974,6 +997,11 @@ extension JNISwift2JavaGenerator {
974997
valueType: valueType
975998
)
976999

1000+
case .set(let elementType):
1001+
return try translateSetResult(
1002+
elementType: elementType
1003+
)
1004+
9771005
case .tuple(let elements) where !elements.isEmpty:
9781006
return try translateTupleResult(elements: elements, resultName: resultName)
9791007

@@ -1293,6 +1321,36 @@ extension JNISwift2JavaGenerator {
12931321
conversion: .wrapMemoryAddressUnsafe(.placeholder, dictType)
12941322
)
12951323
}
1324+
1325+
func translateSetParameter(
1326+
elementType: SwiftType,
1327+
parameterName: String
1328+
) throws -> TranslatedParameter {
1329+
let elementJavaType = try javaTypeForDictionaryComponent(elementType)
1330+
let setType = JavaType.swiftSet(elementJavaType)
1331+
1332+
return TranslatedParameter(
1333+
parameter: JavaParameter(name: parameterName, type: setType),
1334+
conversion: .method(
1335+
.requireNonNull(.placeholder, message: "\(parameterName) must not be null"),
1336+
function: "$memoryAddress",
1337+
arguments: []
1338+
)
1339+
)
1340+
}
1341+
1342+
func translateSetResult(
1343+
elementType: SwiftType
1344+
) throws -> TranslatedResult {
1345+
let elementJavaType = try javaTypeForDictionaryComponent(elementType)
1346+
let setType = JavaType.swiftSet(elementJavaType)
1347+
1348+
return TranslatedResult(
1349+
javaType: setType,
1350+
outParameters: [],
1351+
conversion: .wrapMemoryAddressUnsafe(.placeholder, setType)
1352+
)
1353+
}
12961354
}
12971355

12981356
struct TranslatedEnumCase {
@@ -1788,5 +1846,8 @@ extension JNISwift2JavaGenerator {
17881846

17891847
/// Dictionary type requires exactly two generic type arguments (key and value).
17901848
case dictionaryRequiresKeyAndValueTypes(SwiftType)
1849+
1850+
/// Set type requires exactly one generic type argument (element).
1851+
case setRequiresElementType(SwiftType)
17911852
}
17921853
}

0 commit comments

Comments
 (0)