Skip to content

Commit ba6de8c

Browse files
madsodgaardktoso
andauthored
jextract/jni: Add support for returning protocols (#814)
Co-authored-by: Konrad `ktoso` Malawski <ktoso@apple.com>
1 parent 05681e8 commit ba6de8c

17 files changed

Lines changed: 1276 additions & 100 deletions

‎Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,10 @@ public class ConcreteProtocolAB: ProtocolA, ProtocolB {
3030
MySwiftClass(x: 10, y: 50)
3131
}
3232
}
33+
34+
/// Returns a **reference-type** (class) `ProtocolA` conformer boxed as `any
35+
/// ProtocolA`, to exercise existential-box setter write-back against a
36+
/// class conformer.
37+
public func makeProtocolAClass(constantA: Int64, constantB: Int64) -> any ProtocolA {
38+
ConcreteProtocolAB(constantA: constantA, constantB: constantB)
39+
}

‎Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,27 @@ public protocol ProtocolA {
2323
public func takeProtocol(_ proto1: any ProtocolA, _ proto2: some ProtocolA) -> Int64 {
2424
proto1.constantA + proto2.constantA
2525
}
26+
27+
/// A struct conformer to `ProtocolA`, used to prove that
28+
/// setter dispatch through a returned existential box
29+
public struct ConcreteProtocolAStruct: ProtocolA {
30+
public let constantA: Int64
31+
public var mutable: Int64 = 0
32+
33+
public init(constantA: Int64) {
34+
self.constantA = constantA
35+
}
36+
37+
public func name() -> String {
38+
"ConcreteProtocolAStruct"
39+
}
40+
41+
public func makeClass() -> MySwiftClass {
42+
MySwiftClass(x: constantA, y: mutable)
43+
}
44+
}
45+
46+
/// Returns a value-type `ProtocolA` conformer boxed as `any ProtocolA`.
47+
public func makeProtocolA(constantA: Int64) -> any ProtocolA {
48+
ConcreteProtocolAStruct(constantA: constantA)
49+
}

‎Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolC.swift‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ public struct ConcreteProtocolC: ProtocolC {
2424
constantC = c
2525
}
2626
}
27+
28+
public func makeProtocolC(b: Int64, c: Int64) -> any ProtocolC {
29+
ConcreteProtocolC(b: b, c: c)
30+
}

‎Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolWithStatic.swift‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ public func useProtocolWithStatic(_ value: any ProtocolWithStatic) -> Int {
2323
return meta.myFunc()
2424
}
2525

26+
public struct ConcreteProtocolWithStatic: ProtocolWithStatic {
27+
public init() {}
28+
29+
public static func myFunc() -> Int {
30+
42
31+
}
32+
}
33+
34+
public func makeProtocolWithStatic() -> any ProtocolWithStatic {
35+
ConcreteProtocolWithStatic()
36+
}
37+
2638
public protocol ProtocolWithStaticProperty {
2739
static var value: Int { get }
2840
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 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 protocol Greeter {
16+
func greeting() -> String
17+
func repeated(count: Int64) -> String
18+
}
19+
20+
public struct EnglishGreeter: Greeter {
21+
public let name: String
22+
23+
public init(name: String) {
24+
self.name = name
25+
}
26+
27+
public func greeting() -> String {
28+
"Hello, \(name)!"
29+
}
30+
31+
public func repeated(count: Int64) -> String {
32+
Array(repeating: greeting(), count: Int(count)).joined(separator: " ")
33+
}
34+
}
35+
36+
public struct DanishGreeter: Greeter {
37+
public let name: String
38+
39+
public init(name: String) {
40+
self.name = name
41+
}
42+
43+
public func greeting() -> String {
44+
"Hej, \(name)!"
45+
}
46+
47+
public func repeated(count: Int64) -> String {
48+
Array(repeating: greeting(), count: Int(count)).joined(separator: " ")
49+
}
50+
}
51+
52+
public func makeEnglishGreeter(name: String) -> any Greeter {
53+
EnglishGreeter(name: name)
54+
}
55+
56+
public func makeDanishGreeter(name: String) -> any Greeter {
57+
DanishGreeter(name: name)
58+
}
59+
60+
public func makeOpaqueGreeter(name: String) -> some Greeter {
61+
EnglishGreeter(name: name)
62+
}
63+
64+
public func describeGreeter(_ greeter: any Greeter) -> String {
65+
greeter.greeting()
66+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 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 struct GreeterError: Error {}
16+
17+
public protocol RichGreeter {
18+
func nickname() -> String?
19+
func aliases() -> [String]
20+
func decorate(_ object: MySwiftClass) -> MySwiftClass
21+
func greetOrThrow(shouldThrow: Bool) throws -> String
22+
func recordGreeting()
23+
func count() -> Int64
24+
}
25+
26+
public final class RichGreeterImpl: RichGreeter {
27+
private var greetings: Int64 = 0
28+
private let base: String
29+
30+
public init(base: String) {
31+
self.base = base
32+
}
33+
34+
public func nickname() -> String? {
35+
base.isEmpty ? nil : "Mr. \(base)"
36+
}
37+
38+
public func aliases() -> [String] {
39+
[base, base + base]
40+
}
41+
42+
public func decorate(_ object: MySwiftClass) -> MySwiftClass {
43+
MySwiftClass(x: object.x + 1, y: object.y + 1)
44+
}
45+
46+
public func greetOrThrow(shouldThrow: Bool) throws -> String {
47+
if shouldThrow {
48+
throw GreeterError()
49+
}
50+
return "Hi \(base)"
51+
}
52+
53+
public func recordGreeting() {
54+
greetings += 1
55+
}
56+
57+
public func count() -> Int64 {
58+
greetings
59+
}
60+
}
61+
62+
public func makeRichGreeter(base: String) -> any RichGreeter {
63+
RichGreeterImpl(base: base)
64+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 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 org.junit.jupiter.api.Test;
18+
import org.swift.swiftkit.core.JNISwiftInstance;
19+
import org.swift.swiftkit.core.SwiftArena;
20+
21+
import static org.junit.jupiter.api.Assertions.*;
22+
23+
public class ReturnInheritedProtocolTest {
24+
@Test
25+
void returnedRefinedProtocolExposesOwnAndInheritedRequirements() {
26+
try (var arena = SwiftArena.ofConfined()) {
27+
ProtocolC protoC = MySwiftLibrary.makeProtocolC(7, 11, arena);
28+
29+
// Own requirement, declared directly on ProtocolC.
30+
assertEquals(11, protoC.getConstantC());
31+
32+
// Inherited requirement, declared on ProtocolB, which ProtocolC refines.
33+
assertEquals(7, protoC.getConstantB());
34+
}
35+
}
36+
37+
@Test
38+
void returnedRefinedProtocolIsAlsoUsableAsParentProtocol() {
39+
try (var arena = SwiftArena.ofConfined()) {
40+
ProtocolC protoC = MySwiftLibrary.makeProtocolC(7, 11, arena);
41+
ProtocolB protoB = protoC;
42+
assertEquals(7, protoB.getConstantB());
43+
assertEquals(7, MySwiftLibrary.takeProtocolB(protoB));
44+
}
45+
}
46+
47+
@Test
48+
void returnedRefinedProtocolIsABackingSwiftInstance() {
49+
try (var arena = SwiftArena.ofConfined()) {
50+
ProtocolC protoC = MySwiftLibrary.makeProtocolC(7, 11, arena);
51+
assertInstanceOf(JNISwiftInstance.class, protoC);
52+
}
53+
}
54+
55+
@Test
56+
void returnedProtocolBoxSetterWritesBackToValueTypeStorage() {
57+
try (var arena = SwiftArena.ofConfined()) {
58+
// ConcreteProtocolAStruct is a *value type* (struct) conformer to
59+
// ProtocolA. Opening the existential returned by makeProtocolA
60+
// yields a copy of the struct, so if setMutable merely mutated the
61+
// opened copy without writing it back into the box's storage, the
62+
// new value would be silently lost and getMutable() would still
63+
// read 0.
64+
ProtocolA proto = MySwiftLibrary.makeProtocolA(10, arena);
65+
assertEquals(10, proto.getConstantA());
66+
assertEquals(0, proto.getMutable());
67+
68+
proto.setMutable(42);
69+
assertEquals(42, proto.getMutable());
70+
}
71+
}
72+
73+
@Test
74+
void returnedProtocolBoxSetterWritesBackToReferenceTypeStorage() {
75+
try (var arena = SwiftArena.ofConfined()) {
76+
// ConcreteProtocolAB is a *reference type* (class) conformer to
77+
// ProtocolA, exercising the same write-back path against a class
78+
// conformer boxed as any ProtocolA.
79+
ProtocolA proto = MySwiftLibrary.makeProtocolAClass(10, 5, arena);
80+
assertEquals(10, proto.getConstantA());
81+
assertEquals(0, proto.getMutable());
82+
83+
proto.setMutable(7);
84+
assertEquals(7, proto.getMutable());
85+
}
86+
}
87+
88+
@Test
89+
void returnedProtocolWithUnsupportedStaticRequirementIsStillUsable() {
90+
try (var arena = SwiftArena.ofConfined()) {
91+
// ProtocolWithStatic declares a static func and an init(), neither of which
92+
// are supported requirements for existential boxing, so ProtocolWithStaticBox
93+
// simply omits them. The box/interface should still come back usable as a
94+
// JNISwiftInstance-backed reference.
95+
ProtocolWithStatic protoWithStatic = MySwiftLibrary.makeProtocolWithStatic(arena);
96+
assertInstanceOf(JNISwiftInstance.class, protoWithStatic);
97+
}
98+
}
99+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2026 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 org.junit.jupiter.api.Test;
18+
import org.swift.swiftkit.core.SwiftArena;
19+
20+
import static org.junit.jupiter.api.Assertions.*;
21+
22+
public class ReturnProtocolDispatchTest {
23+
@Test
24+
void boxMethodReturningJextractedClass() {
25+
try (var arena = SwiftArena.ofConfined()) {
26+
ProtocolA proto = MySwiftLibrary.makeProtocolA(10, arena);
27+
assertEquals(10, proto.makeClass(arena).getX());
28+
}
29+
}
30+
31+
@Test
32+
void boxSetterWriteBackObservableThroughSecondRequirement() {
33+
try (var arena = SwiftArena.ofConfined()) {
34+
// ConcreteProtocolAStruct.makeClass() returns
35+
// MySwiftClass(x: constantA, y: mutable), so observing the
36+
// write-back of setMutable through makeClass()'s y (rather than
37+
// through getMutable() itself) proves the setter thunk wrote the
38+
// new value into the box's actual backing storage: makeClass()
39+
// is a *different* requirement that independently reads that
40+
// storage.
41+
ProtocolA proto = MySwiftLibrary.makeProtocolA(10, arena);
42+
proto.setMutable(42);
43+
assertEquals(42, proto.makeClass(arena).getY());
44+
}
45+
}
46+
47+
@Test
48+
void returnedBoxesRoundTripIntoProtocolParameter() {
49+
try (var arena = SwiftArena.ofConfined()) {
50+
assertEquals(
51+
30,
52+
MySwiftLibrary.takeProtocol(
53+
MySwiftLibrary.makeProtocolA(10, arena),
54+
MySwiftLibrary.makeProtocolA(20, arena)
55+
)
56+
);
57+
}
58+
}
59+
60+
@Test
61+
void returnedBoxesRoundTripIntoGenericProtocolParameters() {
62+
try (var arena = SwiftArena.ofConfined()) {
63+
// A ProtocolA box and a ProtocolC box (whose protocol refines
64+
// ProtocolB) fed into takeGenericProtocol<First: ProtocolA,
65+
// Second: ProtocolB>; the ProtocolC box must satisfy the
66+
// ProtocolB generic bound.
67+
assertEquals(
68+
17,
69+
MySwiftLibrary.takeGenericProtocol(
70+
MySwiftLibrary.makeProtocolA(10, arena),
71+
MySwiftLibrary.makeProtocolC(7, 11, arena)
72+
)
73+
);
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)