Skip to content

Commit 3b0c883

Browse files
authored
jextract/jni: Support protocol inheritance (#773)
* Add inherited protocol as extended interface * Remove maybe unnecessary unique logic
1 parent 6eea105 commit 3b0c883

7 files changed

Lines changed: 131 additions & 4 deletions

File tree

Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolB.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ public func takeGenericProtocol<First: ProtocolA, Second: ProtocolB>(_ proto1: F
2727
public func takeCombinedGenericProtocol<T: ProtocolA & ProtocolB>(_ proto: T) -> Int64 {
2828
proto.constantA + proto.constantB
2929
}
30+
31+
public func takeProtocolB(_ proto: some ProtocolB) -> Int64 {
32+
proto.constantB
33+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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 ProtocolC: ProtocolB {
16+
var constantC: Int64 { get }
17+
}
18+
19+
public struct ConcreteProtocolC: ProtocolC {
20+
public var constantB: Int64
21+
public var constantC: Int64
22+
public init(b: Int64, c: Int64) {
23+
constantB = b
24+
constantC = c
25+
}
26+
}

Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ void protocolClassMethod() {
8181
}
8282
}
8383

84+
@Test
85+
void useChildProtocolAsParentProtocol() {
86+
try (var arena = SwiftArena.ofConfined()) {
87+
ProtocolC protoC = ConcreteProtocolC.init(3, 5, arena);
88+
assertEquals(3, MySwiftLibrary.takeProtocolB(protoC));
89+
}
90+
}
91+
8492
static class JavaStorage implements Storage {
8593
StorageItem item;
8694

@@ -110,4 +118,4 @@ void useStorage() {
110118
assertEquals(5, MySwiftLibrary.loadWithStorage(storage, arena).getValue());
111119
}
112120
}
113-
}
121+
}

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,12 @@ extension JNISwift2JavaGenerator {
171171
}
172172

173173
private func printProtocol(_ printer: inout CodePrinter, _ decl: ImportedNominalType) {
174-
var extends = [String]()
174+
var extends = self.inheritedProtocols(of: decl).map(\.effectiveJavaSimpleName)
175175

176176
// If we cannot generate Swift wrappers
177177
// that allows the user to implement the wrapped interface in Java
178178
// then we require only JExtracted types can conform to this.
179-
if !self.interfaceProtocolWrappers.keys.contains(decl) {
179+
if !self.interfaceProtocolWrappers.keys.contains(decl) && !extends.contains("JNISwiftInstance") {
180180
extends.append("JNISwiftInstance")
181181
}
182182
let extendsString = extends.isEmpty ? "" : " extends \(extends.joined(separator: ", "))"

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,23 @@ extension JNISwift2JavaGenerator {
181181
_ printer: inout CodePrinter,
182182
_ translatedWrapper: JavaInterfaceSwiftWrapper,
183183
) throws {
184-
printer.printBraceBlock("protocol \(translatedWrapper.wrapperName): \(translatedWrapper.swiftName)") { printer in
184+
let inheritedWrappers = self.inheritedProtocols(of: translatedWrapper.importedType).compactMap { self.interfaceProtocolWrappers[$0] }
185+
let inheritedTypes = [translatedWrapper.swiftName] + inheritedWrappers.map(\.wrapperName)
186+
187+
printer.printBraceBlock("protocol \(translatedWrapper.wrapperName): \(inheritedTypes.joined(separator: ", "))") { printer in
185188
printer.print(
186189
"var \(translatedWrapper.javaInterfaceVariableName): \(translatedWrapper.javaInterfaceName) { get }"
187190
)
188191
}
189192
printer.println()
190193
try printer.printBraceBlock("extension \(translatedWrapper.wrapperName)") { printer in
194+
for inherited in inheritedWrappers {
195+
printer.printBraceBlock("var \(inherited.javaInterfaceVariableName): \(inherited.javaInterfaceName)") { printer in
196+
printer.print("\(translatedWrapper.javaInterfaceVariableName)")
197+
}
198+
printer.println()
199+
}
200+
191201
for function in translatedWrapper.functions {
192202
try printInterfaceWrapperFunctionImpl(&printer, function, inside: translatedWrapper)
193203
printer.println()

Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,13 @@ extension JNISwift2JavaGenerator {
141141
static func indirectVariableName(for parameterName: String) -> String {
142142
"\(parameterName)$indirect"
143143
}
144+
145+
func inheritedProtocols(of type: ImportedNominalType) -> [ImportedNominalType] {
146+
type.inheritedTypes
147+
.compactMap(\.asNominalTypeDeclaration)
148+
.filter { $0.kind == .protocol }
149+
.compactMap {
150+
self.analysis.importedTypes[$0.qualifiedName]
151+
}
152+
}
144153
}

Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ struct JNIProtocolTests {
4141
public func takeComposite(x: any SomeProtocol & B)
4242
"""
4343

44+
let protocolInheritanceSource = """
45+
public protocol ParentProtocol {
46+
public func parentMethod()
47+
}
48+
49+
public protocol ChildProtocol: ParentProtocol {
50+
public func childMethod()
51+
}
52+
"""
53+
4454
@Test
4555
func generatesJavaInterface() throws {
4656
try assertOutput(
@@ -72,6 +82,33 @@ struct JNIProtocolTests {
7282
)
7383
}
7484

85+
@Test
86+
func generatesJavaInterfaceWithInheritedProtocol() throws {
87+
try assertOutput(
88+
input: protocolInheritanceSource,
89+
config: config,
90+
.jni,
91+
.java,
92+
detectChunkByInitialLines: 1,
93+
expectedChunks: [
94+
"""
95+
public interface ParentProtocol {
96+
...
97+
public void parentMethod();
98+
...
99+
}
100+
""",
101+
"""
102+
public interface ChildProtocol extends ParentProtocol {
103+
...
104+
public void childMethod();
105+
...
106+
}
107+
""",
108+
]
109+
)
110+
}
111+
75112
@Test
76113
func generatesJavaClassWithExtends() throws {
77114
try assertOutput(
@@ -348,4 +385,37 @@ struct JNIProtocolTests {
348385
]
349386
)
350387
}
388+
389+
@Test
390+
func generatesProtocolWrappersWithInheritedProtocol() throws {
391+
try assertOutput(
392+
input: protocolInheritanceSource,
393+
config: config,
394+
.jni,
395+
.swift,
396+
detectChunkByInitialLines: 1,
397+
expectedChunks: [
398+
"""
399+
protocol SwiftJavaParentProtocolWrapper: ParentProtocol {
400+
var _javaParentProtocolInterface: JavaParentProtocol { get }
401+
}
402+
""",
403+
"""
404+
protocol SwiftJavaChildProtocolWrapper: ChildProtocol, SwiftJavaParentProtocolWrapper {
405+
var _javaChildProtocolInterface: JavaChildProtocol { get }
406+
}
407+
""",
408+
"""
409+
extension SwiftJavaChildProtocolWrapper {
410+
var _javaParentProtocolInterface: JavaParentProtocol {
411+
_javaChildProtocolInterface
412+
}
413+
public func childMethod() {
414+
...
415+
}
416+
}
417+
""",
418+
]
419+
)
420+
}
351421
}

0 commit comments

Comments
 (0)