diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolB.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolB.swift index 6f20cf0f..129cd4fb 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolB.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolB.swift @@ -27,3 +27,7 @@ public func takeGenericProtocol(_ proto1: F public func takeCombinedGenericProtocol(_ proto: T) -> Int64 { proto.constantA + proto.constantB } + +public func takeProtocolB(_ proto: some ProtocolB) -> Int64 { + proto.constantB +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolC.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolC.swift new file mode 100644 index 00000000..8bcacdd1 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolC.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 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 protocol ProtocolC: ProtocolB { + var constantC: Int64 { get } +} + +public struct ConcreteProtocolC: ProtocolC { + public var constantB: Int64 + public var constantC: Int64 + public init(b: Int64, c: Int64) { + constantB = b + constantC = c + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java index 098babf1..885dbf6f 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ProtocolTest.java @@ -81,6 +81,14 @@ void protocolClassMethod() { } } + @Test + void useChildProtocolAsParentProtocol() { + try (var arena = SwiftArena.ofConfined()) { + ProtocolC protoC = ConcreteProtocolC.init(3, 5, arena); + assertEquals(3, MySwiftLibrary.takeProtocolB(protoC)); + } + } + static class JavaStorage implements Storage { StorageItem item; @@ -110,4 +118,4 @@ void useStorage() { assertEquals(5, MySwiftLibrary.loadWithStorage(storage, arena).getValue()); } } -} \ No newline at end of file +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index e02a7f76..ec7b1448 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -171,12 +171,12 @@ extension JNISwift2JavaGenerator { } private func printProtocol(_ printer: inout CodePrinter, _ decl: ImportedNominalType) { - var extends = [String]() + var extends = self.inheritedProtocols(of: decl).map(\.effectiveJavaSimpleName) // If we cannot generate Swift wrappers // that allows the user to implement the wrapped interface in Java // then we require only JExtracted types can conform to this. - if !self.interfaceProtocolWrappers.keys.contains(decl) { + if !self.interfaceProtocolWrappers.keys.contains(decl) && !extends.contains("JNISwiftInstance") { extends.append("JNISwiftInstance") } let extendsString = extends.isEmpty ? "" : " extends \(extends.joined(separator: ", "))" diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 3f6797f1..823fe748 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -181,13 +181,23 @@ extension JNISwift2JavaGenerator { _ printer: inout CodePrinter, _ translatedWrapper: JavaInterfaceSwiftWrapper, ) throws { - printer.printBraceBlock("protocol \(translatedWrapper.wrapperName): \(translatedWrapper.swiftName)") { printer in + let inheritedWrappers = self.inheritedProtocols(of: translatedWrapper.importedType).compactMap { self.interfaceProtocolWrappers[$0] } + let inheritedTypes = [translatedWrapper.swiftName] + inheritedWrappers.map(\.wrapperName) + + printer.printBraceBlock("protocol \(translatedWrapper.wrapperName): \(inheritedTypes.joined(separator: ", "))") { printer in printer.print( "var \(translatedWrapper.javaInterfaceVariableName): \(translatedWrapper.javaInterfaceName) { get }" ) } printer.println() try printer.printBraceBlock("extension \(translatedWrapper.wrapperName)") { printer in + for inherited in inheritedWrappers { + printer.printBraceBlock("var \(inherited.javaInterfaceVariableName): \(inherited.javaInterfaceName)") { printer in + printer.print("\(translatedWrapper.javaInterfaceVariableName)") + } + printer.println() + } + for function in translatedWrapper.functions { try printInterfaceWrapperFunctionImpl(&printer, function, inside: translatedWrapper) printer.println() diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index adcef96f..208a3228 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -141,4 +141,13 @@ extension JNISwift2JavaGenerator { static func indirectVariableName(for parameterName: String) -> String { "\(parameterName)$indirect" } + + func inheritedProtocols(of type: ImportedNominalType) -> [ImportedNominalType] { + type.inheritedTypes + .compactMap(\.asNominalTypeDeclaration) + .filter { $0.kind == .protocol } + .compactMap { + self.analysis.importedTypes[$0.qualifiedName] + } + } } diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index 6e763172..43f627a8 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -41,6 +41,16 @@ struct JNIProtocolTests { public func takeComposite(x: any SomeProtocol & B) """ + let protocolInheritanceSource = """ + public protocol ParentProtocol { + public func parentMethod() + } + + public protocol ChildProtocol: ParentProtocol { + public func childMethod() + } + """ + @Test func generatesJavaInterface() throws { try assertOutput( @@ -72,6 +82,33 @@ struct JNIProtocolTests { ) } + @Test + func generatesJavaInterfaceWithInheritedProtocol() throws { + try assertOutput( + input: protocolInheritanceSource, + config: config, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public interface ParentProtocol { + ... + public void parentMethod(); + ... + } + """, + """ + public interface ChildProtocol extends ParentProtocol { + ... + public void childMethod(); + ... + } + """, + ] + ) + } + @Test func generatesJavaClassWithExtends() throws { try assertOutput( @@ -348,4 +385,37 @@ struct JNIProtocolTests { ] ) } + + @Test + func generatesProtocolWrappersWithInheritedProtocol() throws { + try assertOutput( + input: protocolInheritanceSource, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + protocol SwiftJavaParentProtocolWrapper: ParentProtocol { + var _javaParentProtocolInterface: JavaParentProtocol { get } + } + """, + """ + protocol SwiftJavaChildProtocolWrapper: ChildProtocol, SwiftJavaParentProtocolWrapper { + var _javaChildProtocolInterface: JavaChildProtocol { get } + } + """, + """ + extension SwiftJavaChildProtocolWrapper { + var _javaParentProtocolInterface: JavaParentProtocol { + _javaChildProtocolInterface + } + public func childMethod() { + ... + } + } + """, + ] + ) + } }