diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift index 2dbb80825..e6ea8689e 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ConcreteProtocolAB.swift @@ -30,3 +30,10 @@ public class ConcreteProtocolAB: ProtocolA, ProtocolB { MySwiftClass(x: 10, y: 50) } } + +/// Returns a **reference-type** (class) `ProtocolA` conformer boxed as `any +/// ProtocolA`, to exercise existential-box setter write-back against a +/// class conformer. +public func makeProtocolAClass(constantA: Int64, constantB: Int64) -> any ProtocolA { + ConcreteProtocolAB(constantA: constantA, constantB: constantB) +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift index 4d337e087..2ca862f0b 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolA.swift @@ -23,3 +23,27 @@ public protocol ProtocolA { public func takeProtocol(_ proto1: any ProtocolA, _ proto2: some ProtocolA) -> Int64 { proto1.constantA + proto2.constantA } + +/// A struct conformer to `ProtocolA`, used to prove that +/// setter dispatch through a returned existential box +public struct ConcreteProtocolAStruct: ProtocolA { + public let constantA: Int64 + public var mutable: Int64 = 0 + + public init(constantA: Int64) { + self.constantA = constantA + } + + public func name() -> String { + "ConcreteProtocolAStruct" + } + + public func makeClass() -> MySwiftClass { + MySwiftClass(x: constantA, y: mutable) + } +} + +/// Returns a value-type `ProtocolA` conformer boxed as `any ProtocolA`. +public func makeProtocolA(constantA: Int64) -> any ProtocolA { + ConcreteProtocolAStruct(constantA: constantA) +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolC.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolC.swift index 8bcacdd11..8f8bf0cdf 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolC.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolC.swift @@ -24,3 +24,7 @@ public struct ConcreteProtocolC: ProtocolC { constantC = c } } + +public func makeProtocolC(b: Int64, c: Int64) -> any ProtocolC { + ConcreteProtocolC(b: b, c: c) +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolWithStatic.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolWithStatic.swift index f565b1c68..38964355c 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolWithStatic.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ProtocolWithStatic.swift @@ -23,6 +23,18 @@ public func useProtocolWithStatic(_ value: any ProtocolWithStatic) -> Int { return meta.myFunc() } +public struct ConcreteProtocolWithStatic: ProtocolWithStatic { + public init() {} + + public static func myFunc() -> Int { + 42 + } +} + +public func makeProtocolWithStatic() -> any ProtocolWithStatic { + ConcreteProtocolWithStatic() +} + public protocol ProtocolWithStaticProperty { static var value: Int { get } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift new file mode 100644 index 000000000..f58b9a33e --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift @@ -0,0 +1,66 @@ +//===----------------------------------------------------------------------===// +// +// 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 Greeter { + func greeting() -> String + func repeated(count: Int64) -> String +} + +public struct EnglishGreeter: Greeter { + public let name: String + + public init(name: String) { + self.name = name + } + + public func greeting() -> String { + "Hello, \(name)!" + } + + public func repeated(count: Int64) -> String { + Array(repeating: greeting(), count: Int(count)).joined(separator: " ") + } +} + +public struct DanishGreeter: Greeter { + public let name: String + + public init(name: String) { + self.name = name + } + + public func greeting() -> String { + "Hej, \(name)!" + } + + public func repeated(count: Int64) -> String { + Array(repeating: greeting(), count: Int(count)).joined(separator: " ") + } +} + +public func makeEnglishGreeter(name: String) -> any Greeter { + EnglishGreeter(name: name) +} + +public func makeDanishGreeter(name: String) -> any Greeter { + DanishGreeter(name: name) +} + +public func makeOpaqueGreeter(name: String) -> some Greeter { + EnglishGreeter(name: name) +} + +public func describeGreeter(_ greeter: any Greeter) -> String { + greeter.greeting() +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/RichGreeter.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/RichGreeter.swift new file mode 100644 index 000000000..048d0c6ee --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/RichGreeter.swift @@ -0,0 +1,64 @@ +//===----------------------------------------------------------------------===// +// +// 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 struct GreeterError: Error {} + +public protocol RichGreeter { + func nickname() -> String? + func aliases() -> [String] + func decorate(_ object: MySwiftClass) -> MySwiftClass + func greetOrThrow(shouldThrow: Bool) throws -> String + func recordGreeting() + func count() -> Int64 +} + +public final class RichGreeterImpl: RichGreeter { + private var greetings: Int64 = 0 + private let base: String + + public init(base: String) { + self.base = base + } + + public func nickname() -> String? { + base.isEmpty ? nil : "Mr. \(base)" + } + + public func aliases() -> [String] { + [base, base + base] + } + + public func decorate(_ object: MySwiftClass) -> MySwiftClass { + MySwiftClass(x: object.x + 1, y: object.y + 1) + } + + public func greetOrThrow(shouldThrow: Bool) throws -> String { + if shouldThrow { + throw GreeterError() + } + return "Hi \(base)" + } + + public func recordGreeting() { + greetings += 1 + } + + public func count() -> Int64 { + greetings + } +} + +public func makeRichGreeter(base: String) -> any RichGreeter { + RichGreeterImpl(base: base) +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnInheritedProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnInheritedProtocolTest.java new file mode 100644 index 000000000..62d041401 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnInheritedProtocolTest.java @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.JNISwiftInstance; +import org.swift.swiftkit.core.SwiftArena; + +import static org.junit.jupiter.api.Assertions.*; + +public class ReturnInheritedProtocolTest { + @Test + void returnedRefinedProtocolExposesOwnAndInheritedRequirements() { + try (var arena = SwiftArena.ofConfined()) { + ProtocolC protoC = MySwiftLibrary.makeProtocolC(7, 11, arena); + + // Own requirement, declared directly on ProtocolC. + assertEquals(11, protoC.getConstantC()); + + // Inherited requirement, declared on ProtocolB, which ProtocolC refines. + assertEquals(7, protoC.getConstantB()); + } + } + + @Test + void returnedRefinedProtocolIsAlsoUsableAsParentProtocol() { + try (var arena = SwiftArena.ofConfined()) { + ProtocolC protoC = MySwiftLibrary.makeProtocolC(7, 11, arena); + ProtocolB protoB = protoC; + assertEquals(7, protoB.getConstantB()); + assertEquals(7, MySwiftLibrary.takeProtocolB(protoB)); + } + } + + @Test + void returnedRefinedProtocolIsABackingSwiftInstance() { + try (var arena = SwiftArena.ofConfined()) { + ProtocolC protoC = MySwiftLibrary.makeProtocolC(7, 11, arena); + assertInstanceOf(JNISwiftInstance.class, protoC); + } + } + + @Test + void returnedProtocolBoxSetterWritesBackToValueTypeStorage() { + try (var arena = SwiftArena.ofConfined()) { + // ConcreteProtocolAStruct is a *value type* (struct) conformer to + // ProtocolA. Opening the existential returned by makeProtocolA + // yields a copy of the struct, so if setMutable merely mutated the + // opened copy without writing it back into the box's storage, the + // new value would be silently lost and getMutable() would still + // read 0. + ProtocolA proto = MySwiftLibrary.makeProtocolA(10, arena); + assertEquals(10, proto.getConstantA()); + assertEquals(0, proto.getMutable()); + + proto.setMutable(42); + assertEquals(42, proto.getMutable()); + } + } + + @Test + void returnedProtocolBoxSetterWritesBackToReferenceTypeStorage() { + try (var arena = SwiftArena.ofConfined()) { + // ConcreteProtocolAB is a *reference type* (class) conformer to + // ProtocolA, exercising the same write-back path against a class + // conformer boxed as any ProtocolA. + ProtocolA proto = MySwiftLibrary.makeProtocolAClass(10, 5, arena); + assertEquals(10, proto.getConstantA()); + assertEquals(0, proto.getMutable()); + + proto.setMutable(7); + assertEquals(7, proto.getMutable()); + } + } + + @Test + void returnedProtocolWithUnsupportedStaticRequirementIsStillUsable() { + try (var arena = SwiftArena.ofConfined()) { + // ProtocolWithStatic declares a static func and an init(), neither of which + // are supported requirements for existential boxing, so ProtocolWithStaticBox + // simply omits them. The box/interface should still come back usable as a + // JNISwiftInstance-backed reference. + ProtocolWithStatic protoWithStatic = MySwiftLibrary.makeProtocolWithStatic(arena); + assertInstanceOf(JNISwiftInstance.class, protoWithStatic); + } + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolDispatchTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolDispatchTest.java new file mode 100644 index 000000000..c3e4f2ef2 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolDispatchTest.java @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import static org.junit.jupiter.api.Assertions.*; + +public class ReturnProtocolDispatchTest { + @Test + void boxMethodReturningJextractedClass() { + try (var arena = SwiftArena.ofConfined()) { + ProtocolA proto = MySwiftLibrary.makeProtocolA(10, arena); + assertEquals(10, proto.makeClass(arena).getX()); + } + } + + @Test + void boxSetterWriteBackObservableThroughSecondRequirement() { + try (var arena = SwiftArena.ofConfined()) { + // ConcreteProtocolAStruct.makeClass() returns + // MySwiftClass(x: constantA, y: mutable), so observing the + // write-back of setMutable through makeClass()'s y (rather than + // through getMutable() itself) proves the setter thunk wrote the + // new value into the box's actual backing storage: makeClass() + // is a *different* requirement that independently reads that + // storage. + ProtocolA proto = MySwiftLibrary.makeProtocolA(10, arena); + proto.setMutable(42); + assertEquals(42, proto.makeClass(arena).getY()); + } + } + + @Test + void returnedBoxesRoundTripIntoProtocolParameter() { + try (var arena = SwiftArena.ofConfined()) { + assertEquals( + 30, + MySwiftLibrary.takeProtocol( + MySwiftLibrary.makeProtocolA(10, arena), + MySwiftLibrary.makeProtocolA(20, arena) + ) + ); + } + } + + @Test + void returnedBoxesRoundTripIntoGenericProtocolParameters() { + try (var arena = SwiftArena.ofConfined()) { + // A ProtocolA box and a ProtocolC box (whose protocol refines + // ProtocolB) fed into takeGenericProtocol; the ProtocolC box must satisfy the + // ProtocolB generic bound. + assertEquals( + 17, + MySwiftLibrary.takeGenericProtocol( + MySwiftLibrary.makeProtocolA(10, arena), + MySwiftLibrary.makeProtocolC(7, 11, arena) + ) + ); + } + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java new file mode 100644 index 000000000..67d91aa80 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.JNISwiftInstance; +import org.swift.swiftkit.core.SwiftArena; + +import static org.junit.jupiter.api.Assertions.*; + +public class ReturnProtocolTest { + @Test + void returnExistentialAndCallMethod() { + try (var arena = SwiftArena.ofConfined()) { + Greeter greeter = MySwiftLibrary.makeEnglishGreeter("World", arena); + assertEquals("Hello, World!", greeter.greeting()); + } + } + + @Test + void returnedProtocolIsABackingSwiftInstance() { + try (var arena = SwiftArena.ofConfined()) { + Greeter greeter = MySwiftLibrary.makeEnglishGreeter("World", arena); + assertInstanceOf(JNISwiftInstance.class, greeter); + } + } + + @Test + void returnExistentialWithArgument() { + try (var arena = SwiftArena.ofConfined()) { + Greeter greeter = MySwiftLibrary.makeEnglishGreeter("World", arena); + assertEquals("Hello, World! Hello, World! Hello, World!", greeter.repeated(3)); + } + } + + @Test + void returnOpaqueAndCallMethod() { + try (var arena = SwiftArena.ofConfined()) { + Greeter greeter = MySwiftLibrary.makeOpaqueGreeter("World", arena); + assertEquals("Hello, World!", greeter.greeting()); + } + } + + @Test + void returnExistentialPreservesDynamicType() { + try (var arena = SwiftArena.ofConfined()) { + Greeter english = MySwiftLibrary.makeEnglishGreeter("World", arena); + Greeter german = MySwiftLibrary.makeDanishGreeter("Verden", arena); + assertEquals("Hello, World!", english.greeting()); + assertEquals("Hej, Verden!", german.greeting()); + } + } + + @Test + void returnedProtocolRoundTripsIntoParameter() { + try (var arena = SwiftArena.ofConfined()) { + Greeter greeter = MySwiftLibrary.makeDanishGreeter("Verden", arena); + assertEquals("Hej, Verden!", MySwiftLibrary.describeGreeter(greeter)); + } + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnRichProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnRichProtocolTest.java new file mode 100644 index 000000000..2704d1d2c --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnRichProtocolTest.java @@ -0,0 +1,85 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +package com.example.swift; + +import org.junit.jupiter.api.Test; +import org.swift.swiftkit.core.SwiftArena; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +public class ReturnRichProtocolTest { + @Test + void optionalResultPresent() { + try (var arena = SwiftArena.ofConfined()) { + RichGreeter greeter = MySwiftLibrary.makeRichGreeter("World", arena); + assertEquals(Optional.of("Mr. World"), greeter.nickname()); + } + } + + @Test + void optionalResultNull() { + try (var arena = SwiftArena.ofConfined()) { + RichGreeter greeter = MySwiftLibrary.makeRichGreeter("", arena); + assertEquals(Optional.empty(), greeter.nickname()); + } + } + + @Test + void arrayResult() { + try (var arena = SwiftArena.ofConfined()) { + RichGreeter greeter = MySwiftLibrary.makeRichGreeter("Ho", arena); + assertArrayEquals(new String[] { "Ho", "HoHo" }, greeter.aliases()); + } + } + + @Test + void objectParameterAndObjectResult() { + try (var arena = SwiftArena.ofConfined()) { + RichGreeter greeter = MySwiftLibrary.makeRichGreeter("World", arena); + MySwiftClass input = MySwiftClass.init(10, 5, arena); + MySwiftClass output = greeter.decorate(input, arena); + assertEquals(11, output.getX()); + assertEquals(6, output.getY()); + } + } + + @Test + void throwingFunctionReturnsOnSuccess() throws Exception { + try (var arena = SwiftArena.ofConfined()) { + RichGreeter greeter = MySwiftLibrary.makeRichGreeter("World", arena); + assertEquals("Hi World", greeter.greetOrThrow(false)); + } + } + + @Test + void throwingFunctionThrowsOnFailure() { + try (var arena = SwiftArena.ofConfined()) { + RichGreeter greeter = MySwiftLibrary.makeRichGreeter("World", arena); + assertThrows(Exception.class, () -> greeter.greetOrThrow(true)); + } + } + + @Test + void voidSideEffectObservableThroughSecondRequirement() { + try (var arena = SwiftArena.ofConfined()) { + RichGreeter greeter = MySwiftLibrary.makeRichGreeter("World", arena); + greeter.recordGreeting(); + greeter.recordGreeting(); + assertEquals(2, greeter.count()); + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 587b0c5b3..b213c71ca 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -88,6 +88,24 @@ extension JNISwift2JavaGenerator { } } + // One box class per protocol returned as `any P` / `some P` from at + // least one extracted function. + for protocolType in self.existentialProtocolBoxes { + let boxName = protocolType.swiftNominal.javaExistentialBoxName + let filename = "\(boxName).java" + logger.debug("Printing contents: \(filename)") + printExistentialBoxFile(&printer, protocolType) + + if let outputFile = try printer.writeContents( + outputDirectory: javaOutputDirectory, + javaPackagePath: javaPackagePath, + filename: filename, + ) { + exportedFileNames.append(outputFile.path(percentEncoded: false)) + logger.info("[swift-java] Generated: \(boxName.bold).java (at \(outputFile))") + } + } + // Write java sources list file if let generatedJavaSourcesListFileOutput = config.generatedJavaSourcesListFileOutput, !exportedFileNames.isEmpty { let outputPath = URL(fileURLWithPath: javaOutputDirectory).appending(path: generatedJavaSourcesListFileOutput) @@ -187,26 +205,107 @@ extension JNISwift2JavaGenerator { self.logger.debug("Skipping static method '\(initializer.name)'") } - for method in decl.methods { - if method.isStatic { - self.logger.debug("Skipping static method '\(method.name)'") - continue - } - printFunctionDowncallMethods(&printer, method, skipMethodBody: true) + // Use the same requirement predicate as the existential box + // (`supportedProtocolRequirements(of:)`) so the interface never + // declares a requirement (e.g. a property setter) that the box + // doesn't implement. + for requirement in self.supportedProtocolRequirements(of: decl) { + printFunctionDowncallMethods(&printer, requirement, skipMethodBody: true) printer.println() } + } + } - for variable in decl.variables { - if variable.isStatic { - self.logger.debug("Skipping static property '\(variable.name)'") - continue + /// Prints the full `.java` file for a protocol's existential box — the + /// class used to represent a value returned as `any P` / `some P` from an + /// extracted Swift function. + private func printExistentialBoxFile(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) { + assert(decl.swiftNominal.kind == .protocol, "printExistentialBoxFile must only be called for a protocol, but was called with: \(decl)") + + printHeader(&printer) + printPackage(&printer) + printImports(&printer) + + self.currentJavaIdentifiers = JavaIdentifierFactory(self.allProtocolRequirementMethods(of: decl)) + + printExistentialBox(&printer, decl) + } + + /// Prints `public final class

Box implements JNISwiftInstance,

{ ... }`. + /// + /// Boxes a value returned as `any P` / `some P`: it carries the concrete + /// (dynamic) value pointer and the concrete (dynamic) type-metadata pointer. + private func printExistentialBox(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) { + let boxName = decl.swiftNominal.javaExistentialBoxName + let implementsClause = "JNISwiftInstance, \(decl.effectiveJavaSimpleName)" + + printer.printBraceBlock("final class \(boxName) implements \(implementsClause)") { printer in + printDesignatedConstructor(&printer, javaName: boxName, pointerParams: ["selfPointer", "selfTypePointer"]) + printer.println() + printWrapMemoryAddressUnsafeFactory( + &printer, + javaName: boxName, + genericClause: "", + pointerParams: ["selfPointer", "selfTypePointer"], + docSummary: "Assume that the passed {@code long}s represent a memory address and type metadata address of a {@link \(boxName)}." + ) + printer.println() + printer.print( + """ + /** Pointer to the "self". */ + private final long selfPointer; + + /** Pointer to the metatype of the concrete dynamic value boxed here. */ + private final long selfTypePointer; + + /** Tracks whether this instance has been destroyed; doubles as the destroyed-state holder. */ + private final SwiftInstanceCleanup $cleanup; + + public long $memoryAddress() { + return this.selfPointer; } - printFunctionDowncallMethods(&printer, variable, skipMethodBody: true) + + @Override + public SwiftInstanceCleanup $cleanup() { + return $cleanup; + } + + @Override + public long $typeMetadataAddress() { + return this.selfTypePointer; + } + """ + ) + printer.println() + + for method in self.allProtocolRequirementMethods(of: decl) { + printExistentialBoxMethod(&printer, decl, method) printer.println() } + + printSwiftInstanceObjectMethods(&printer) + printer.println() } } + /// Prints one existential box method. + private func printExistentialBoxMethod( + _ printer: inout JavaPrinter, + _ decl: ExtractedNominalType, + _ method: ExtractedFunc + ) { + guard var translated = try? self.javaTranslator.translate(method) else { + self.logger.debug("Failed to translate protocol requirement for existential box: \(method)") + return + } + translated.parentName = SwiftQualifiedTypeName(decl.swiftNominal.javaExistentialBoxName) + + printer.printSeparator(method.displayName) + // We pass importedFunc as nil because we are not printing based on the protocol + // but rather as the concrete box class. + printJavaBindingWrapperMethod(&printer, translated, importedFunc: nil, skipMethodBody: false) + } + private func printConcreteType(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) { printNominal(&printer, decl) { printer in printer.print( @@ -254,63 +353,22 @@ extension JNISwift2JavaGenerator { printer.println() } - printer.print( - """ - /** - * The designated constructor of any imported Swift types. - * - * @param selfPointer a pointer to the memory containing the value - * @param swiftArena the arena this object belongs to. When the arena goes out of scope, this value is destroyed. - */ - """ - ) // Specialized types are concrete — no selfTypePointer needed let isEffectivelyGeneric = decl.swiftNominal.isGeneric && !decl.isSpecialization var swiftPointerParams = ["selfPointer"] if isEffectivelyGeneric { swiftPointerParams.append("selfTypePointer") } - let swiftPointerArg = swiftPointerParams.map { "long \($0)" }.joined(separator: .comma) - printer.printBraceBlock("private \(decl.effectiveJavaSimpleName)(\(swiftPointerArg), SwiftArena swiftArena)") { printer in - for param in swiftPointerParams { - printer.print( - """ - SwiftObjects.requireNonZero(\(param), "\(param)"); - this.\(param) = \(param); - """ - ) - } - printer.print( - """ - this.$cleanup = $createCleanup(); - - // Only register once we have fully initialized the object since this will need the object pointer. - swiftArena.register(this); - """ - ) - } + let javaName = decl.effectiveJavaSimpleName + printDesignatedConstructor(&printer, javaName: javaName, pointerParams: swiftPointerParams) printer.println() let genericClause = decl.javaGenericClause - let javaName = decl.effectiveJavaSimpleName - printer.print( - """ - /** - * Assume that the passed {@code long} represents a memory address of a {@link \(javaName)}. - *

- * Warnings: - *

- */ - public static\(genericClause) \(javaName)\(genericClause) wrapMemoryAddressUnsafe(\(swiftPointerArg), SwiftArena swiftArena) { - return new \(javaName)\(genericClause)(\(swiftPointerParams.joined(separator: .comma)), swiftArena); - } - - public static\(genericClause) \(javaName)\(genericClause) wrapMemoryAddressUnsafe(\(swiftPointerArg)) { - return new \(javaName)\(genericClause)(\(swiftPointerParams.joined(separator: .comma)), SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA); - } - """ + printWrapMemoryAddressUnsafeFactory( + &printer, + javaName: javaName, + genericClause: genericClause, + pointerParams: swiftPointerParams, + docSummary: "Assume that the passed {@code long} represents a memory address of a {@link \(javaName)}." ) printer.print( @@ -372,32 +430,106 @@ extension JNISwift2JavaGenerator { printTypeMetadataAddressFunction(&printer, decl) printer.println() + printSwiftInstanceObjectMethods(&printer) + printer.println() + } + } + + /// Prints the designated (memory-managed) constructor shared by every `JNISwiftInstance` + private func printDesignatedConstructor( + _ printer: inout JavaPrinter, + javaName: String, + pointerParams: [String], + ) { + printer.print( + """ + /** + * The designated constructor of any imported Swift types. + * + * @param selfPointer a pointer to the memory containing the value + * @param swiftArena the arena this object belongs to. When the arena goes out of scope, this value is destroyed. + */ + """ + ) + let pointerArg = pointerParams.map { "long \($0)" }.joined(separator: ", ") + printer.printBraceBlock("private \(javaName)(\(pointerArg), SwiftArena swiftArena)") { printer in + for param in pointerParams { + printer.print( + """ + SwiftObjects.requireNonZero(\(param), "\(param)"); + this.\(param) = \(param); + """ + ) + } printer.print( """ - public boolean equals(Object obj) { - if (obj instanceof JNISwiftInstance rhs) { - return SwiftObjects.equals(this.$memoryAddress(), this.$typeMetadataAddress(), rhs.$memoryAddress(), rhs.$typeMetadataAddress()); - } - return false; - } + this.$cleanup = $createCleanup(); - public int hashCode() { - return SwiftObjects.hashCode(this.$memoryAddress(), this.$typeMetadataAddress()); - } - - public java.lang.String toString() { - return SwiftObjects.toString(this.$memoryAddress(), this.$typeMetadataAddress()); - } - - public java.lang.String toDebugString() { - return SwiftObjects.toDebugString(this.$memoryAddress(), this.$typeMetadataAddress()); - } + // Only register once we have fully initialized the object since this will need the object pointer. + swiftArena.register(this); """ ) - printer.println() } } + /// Prints the `wrapMemoryAddressUnsafe` factory pair shared by every `JNISwiftInstance`. + private func printWrapMemoryAddressUnsafeFactory( + _ printer: inout JavaPrinter, + javaName: JavaClassName, + genericClause: String, + pointerParams: [String], + docSummary: String, + ) { + let pointerArg = pointerParams.map { "long \($0)" }.joined(separator: ", ") + let pointerArgNames = pointerParams.joined(separator: ", ") + printer.print( + """ + /** + * \(docSummary) + *

+ * Warnings: + *

+ */ + public static\(genericClause) \(javaName)\(genericClause) wrapMemoryAddressUnsafe(\(pointerArg), SwiftArena swiftArena) { + return new \(javaName)\(genericClause)(\(pointerArgNames), swiftArena); + } + + public static\(genericClause) \(javaName)\(genericClause) wrapMemoryAddressUnsafe(\(pointerArg)) { + return new \(javaName)\(genericClause)(\(pointerArgNames), SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA); + } + """ + ) + } + + /// Prints common Swift object methods such as `equals`, `hashCode` etc. + private func printSwiftInstanceObjectMethods(_ printer: inout JavaPrinter) { + printer.print( + """ + public boolean equals(Object obj) { + if (obj instanceof JNISwiftInstance rhs) { + return SwiftObjects.equals(this.$memoryAddress(), this.$typeMetadataAddress(), rhs.$memoryAddress(), rhs.$typeMetadataAddress()); + } + return false; + } + + public int hashCode() { + return SwiftObjects.hashCode(this.$memoryAddress(), this.$typeMetadataAddress()); + } + + public java.lang.String toString() { + return SwiftObjects.toString(this.$memoryAddress(), this.$typeMetadataAddress()); + } + + public java.lang.String toDebugString() { + return SwiftObjects.toDebugString(this.$memoryAddress(), this.$typeMetadataAddress()); + } + """ + ) + } + /// Prints helpers for specific types like `Foundation.Date` private func printSpecificTypeHelpers(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) { guard let knownType = decl.swiftNominal.knownTypeKind else { return } diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift index ae4eb1396..1e30455a5 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaTranslation.swift @@ -29,8 +29,7 @@ extension JNISwift2JavaGenerator { knownTypes: SwiftKnownTypes(symbolTable: lookupContext.symbolTable), protocolWrappers: self.interfaceProtocolWrappers, logger: self.logger, - javaIdentifiers: self.currentJavaIdentifiers, - extractedTypes: self.analysis.extractedTypes, + javaIdentifiers: self.currentJavaIdentifiers ) } @@ -72,7 +71,6 @@ extension JNISwift2JavaGenerator { protocolWrappers: self.interfaceProtocolWrappers, logger: self.logger, javaIdentifiers: self.currentJavaIdentifiers, - extractedTypes: self.analysis.extractedTypes, ) translated = try translation.translate(enumCase: decl) } catch { @@ -94,7 +92,6 @@ extension JNISwift2JavaGenerator { let protocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] let logger: Logger var javaIdentifiers: JavaIdentifierFactory - let extractedTypes: [SwiftTypeName: ExtractedNominalType] func translate(enumCase: ExtractedEnumCase) throws -> TranslatedEnumCase { let methodName = "" // TODO: Used for closures, replace with better name? @@ -381,7 +378,9 @@ extension JNISwift2JavaGenerator { return nil } let isGeneric = selfParameter.selfType.asNominalTypeDeclaration?.isGeneric == true - guard isGeneric else { + let isProtocol = selfParameter.selfType.asNominalType?.isProtocol == true + + guard isGeneric || isProtocol else { return nil } @@ -983,7 +982,35 @@ extension JNISwift2JavaGenerator { genericRequirements: genericRequirements, ) - case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite, .inlineArray: + case .opaque(let proto), .existential(let proto): + guard case .nominal(let protoNominalType) = proto, protoNominalType.isProtocol else { + throw JavaTranslationError.cannotReturnNonExtractedProtocol(proto) + } + + let interfaceJavaType = JavaType.class( + package: moduleJavaPackages[protoNominalType.nominalTypeDecl.moduleName], + name: protoNominalType.nominalTypeDecl.qualifiedName, + ) + let boxJavaType = JavaType.class( + package: moduleJavaPackages[protoNominalType.nominalTypeDecl.moduleName], + name: protoNominalType.nominalTypeDecl.javaExistentialBoxName, + ) + + return TranslatedResult( + javaType: interfaceJavaType, + nativeJavaType: .void, + annotations: resultAnnotations, + outParameters: [.init(name: resultName, type: ._OutSwiftGenericInstance, allocation: .new)], + conversion: .wrapMemoryAddressUnsafe( + .commaSeparated([ + .member(.constant(resultName), field: "selfPointer"), + .member(.constant(resultName), field: "selfTypePointer"), + ]), + boxJavaType + ) + ) + + case .metatype, .tuple, .function, .genericParameter, .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(swiftType) } } @@ -2115,6 +2142,10 @@ extension JNISwift2JavaGenerator { /// protocols that we unable to be jextracted. case protocolWasNotExtracted + /// Returning `any P` / `some P` is only supported when `P` is a single + /// jextracted protocol. + case cannotReturnNonExtractedProtocol(SwiftType) + /// Dictionary type requires exactly two generic type arguments (key and value). case dictionaryRequiresKeyAndValueTypes(SwiftType) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 578d072cc..9f025bbdf 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -49,24 +49,43 @@ extension JNISwift2JavaGenerator { } // Lower the self parameter. + let selfIsExtractedProtocolRequirement = self.extractedProtocol(for: functionSignature.selfParameter?.selfType) != nil + let nativeSelf: NativeParameter? = switch functionSignature.selfParameter { case .instance(_, let swiftType): - try translateParameter( - type: swiftType, - parameterName: "selfPointer", - methodName: methodName, - parentName: parentName, - genericParameters: functionSignature.genericParameters, - genericRequirements: functionSignature.genericRequirements - ) + if let protocolType = self.extractedProtocol(for: swiftType) { + // 'self' of a protocol requirement is reconstructed by opening + // the existential from (selfPointer, selfTypePointer). + NativeParameter( + parameters: [ + JavaParameter(name: "selfPointer", type: .long) + ], + conversion: .extractSwiftProtocolValue( + .constant("selfPointer"), + typeMetadataVariableName: .constant("selfTypePointer"), + protocolTypes: [protocolType] + ), + indirectConversion: nil, + conversionCheck: nil + ) + } else { + try translateParameter( + type: swiftType, + parameterName: "selfPointer", + methodName: methodName, + parentName: parentName, + genericParameters: functionSignature.genericParameters, + genericRequirements: functionSignature.genericRequirements + ) + } case nil, .initializer(_), .staticMethod(_): nil } let selfTypeParameter: NativeParameter? = if let selfType = functionSignature.selfParameter?.selfType, - selfType.asNominalTypeDeclaration?.isGeneric == true + selfType.asNominalTypeDeclaration?.isGeneric == true || selfIsExtractedProtocolRequirement { try translateParameter( type: .metatype(selfType), @@ -841,11 +860,36 @@ extension JNISwift2JavaGenerator { case .tuple(let elements) where !elements.isEmpty: return try translateTupleResult(methodName: methodName, elements: elements, resultName: resultName) - case .metatype, .tuple, .function, .existential, .opaque, .genericParameter, .composite, .inlineArray: + case .opaque(let proto), .existential(let proto): + guard case .nominal(let protoNominalType) = proto, protoNominalType.isProtocol else { + throw JavaTranslationError.cannotReturnNonExtractedProtocol(proto) + } + + return translateProtocolResult( + protocolType: SwiftNominalType(nominalTypeDecl: protoNominalType.nominalTypeDecl), + resultName: resultName + ) + + case .metatype, .tuple, .function, .genericParameter, .composite, .inlineArray: throw JavaTranslationError.unsupportedSwiftType(swiftType) } } + /// Boxes a returned `any P` / `some P` value. + private func translateProtocolResult( + protocolType: SwiftNominalType, + resultName: String + ) -> NativeResult { + NativeResult( + javaType: .void, + conversion: .existentialValueIndirectReturn( + .allocateExistentialValue(.placeholder, name: resultName, protocolTypes: [protocolType]), + outArgumentName: resultName + "Out" + ), + outParameters: [.init(name: resultName + "Out", type: ._OutSwiftGenericInstance)] + ) + } + func translateTupleResult( methodName: String, elements: [SwiftTupleElement], @@ -1166,6 +1210,13 @@ extension JNISwift2JavaGenerator { throw JavaTranslationError.unsupportedSwiftType(swiftType) } } + + private func extractedProtocol(for type: SwiftType?) -> SwiftNominalType? { + guard case .nominal(let nominalType) = type, nominalType.isProtocol else { + return nil + } + return nominalType + } } struct NativeFunctionSignature { @@ -1261,6 +1312,8 @@ extension JNISwift2JavaGenerator { /// Allocate memory for a Swift value and outputs the pointer indirect case allocateSwiftValue(NativeSwiftConversionStep, name: String, swiftType: SwiftType) + indirect case allocateExistentialValue(NativeSwiftConversionStep, name: String, protocolTypes: [SwiftNominalType]) + /// The thing to which the pointer typed, which is the `pointee` property /// of the `Unsafe(Mutable)Pointer` types in Swift. indirect case pointee(NativeSwiftConversionStep) @@ -1304,6 +1357,11 @@ extension JNISwift2JavaGenerator { outArgumentName: String ) + indirect case existentialValueIndirectReturn( + NativeSwiftConversionStep, + outArgumentName: String + ) + indirect case constructor( _ swiftType: SwiftType, arguments: [(String?, NativeSwiftConversionStep)] = [] @@ -1523,6 +1581,40 @@ extension JNISwift2JavaGenerator { ) return bitsName + case .allocateExistentialValue(let inner, let name, let protocolTypes): + let inner = inner.render(&printer, placeholder) + let existentialType = SwiftKitPrinting.renderExistentialType(protocolTypes) + let existentialName = "\(name)Existential$" + let boxedName = "\(name)Boxed$" + + // Bind at existential type first: for `some P` the static result type + // is the opaque type, not `(any P)` — binding here erases it to the + // existential so the boxing helper below can open it uniformly. + printer.print("let \(existentialName): \(existentialType) = \(inner)") + + printer.print( + """ + #if hasFeature(ImplicitOpenExistentials) + let \(boxedName): (Int64, Int64) = { + let value = \(existentialName) + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.initialize(to: value) + let metadataPointer = unsafeBitCast(type(of: value), to: UnsafeRawPointer.self) + return (Int64(Int(bitPattern: pointer)), Int64(Int(bitPattern: metadataPointer))) + }() + #else + func \(name)Box$(_ value: T) -> (Int64, Int64) { + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.initialize(to: value) + let metadataPointer = unsafeBitCast(T.self, to: UnsafeRawPointer.self) + return (Int64(Int(bitPattern: pointer)), Int64(Int(bitPattern: metadataPointer))) + } + let \(boxedName) = _openExistential(\(existentialName), do: \(name)Box$) + #endif + """ + ) + return boxedName + case .pointee(let inner): let inner = inner.render(&printer, placeholder) return "\(inner).pointee" @@ -1715,6 +1807,19 @@ extension JNISwift2JavaGenerator { } return "" + case .existentialValueIndirectReturn(let inner, let outArgumentName): + let boxed = inner.render(&printer, placeholder) + printer.printBraceBlock("do") { printer in + printer.print( + """ + let (selfPointerBits$, selfTypePointerBits$) = \(boxed) + environment.interface.SetLongField(environment, \(outArgumentName), _JNIMethodIDCache._OutSwiftGenericInstance.selfPointer, selfPointerBits$.getJNIValue(in: environment)) + environment.interface.SetLongField(environment, \(outArgumentName), _JNIMethodIDCache._OutSwiftGenericInstance.selfTypePointer, selfTypePointerBits$.getJNIValue(in: environment)) + """ + ) + } + return "" + case .constructor(let swiftType, let arguments): let args = arguments.map { name, value in let value = value.render(&printer, placeholder) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index d464c80ee..db8c66320 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -372,11 +372,36 @@ extension JNISwift2JavaGenerator { } private func printProtocolThunks(_ printer: inout SwiftPrinter, _ type: ExtractedNominalType) throws { - guard let protocolWrapper = self.interfaceProtocolWrappers[type] else { - return + if let protocolWrapper = self.interfaceProtocolWrappers[type] { + try printSwiftInterfaceWrapper(&printer, protocolWrapper) } - try printSwiftInterfaceWrapper(&printer, protocolWrapper) + // Independent of `interfaceProtocolWrappers`/`enableJavaCallbacks` — this + // is the opposite direction (Swift-implements / Java-receives a + // downcall), so it must not be gated on the upcall machinery. + if self.existentialProtocolBoxes.contains(type) { + printExistentialBoxDispatchThunks(&printer, type) + } + } + + /// Prints one `@_cdecl` dispatch thunk per requirement of a protocol + /// that's returned as `any P` / `some P` from at least one extracted + /// function (including requirements inherited from refined protocols). + private func printExistentialBoxDispatchThunks(_ printer: inout SwiftPrinter, _ type: ExtractedNominalType) { + let boxParentName = SwiftQualifiedTypeName(type.swiftNominal.javaExistentialBoxName) + + for method in self.allProtocolRequirementMethods(of: type) { + guard var translated = try? self.javaTranslator.translate(method) else { + self.logger.debug("Failed to translate protocol requirement for existential box dispatch thunk: \(method)") + continue + } + translated.parentName = boxParentName + + printCDecl(&printer, translated) { printer in + self.printFunctionDowncall(&printer, method) + } + printer.println() + } } private func printEnumRawDiscriminator(_ printer: inout SwiftPrinter, _ type: ExtractedNominalType) { @@ -625,6 +650,32 @@ extension JNISwift2JavaGenerator { printer.print("#endif") } + // A setter accessor dispatched on a protocol `self` + // cannot reuse the generic callee/result machinery below: + // nativeSignature.selfParameter's conversion + // (`extractSwiftProtocolValue`) reconstructs `self` by loading the + // concrete value out of the existential's storage into a let. + // Even if it were mutable, mutating a loaded + // existential only mutates a copy for value types. Instead, open the + // existential, mutate it, and store the mutated existential back into + // the original storage. + if decl.apiKind == .setter, + case .instance = decl.functionSignature.selfParameter, + let protocolType = decl.functionSignature.selfParameter?.selfType.asNominalType, + protocolType.isProtocol + { + guard let newValueArgument = arguments.first else { + fatalError("Setter did not contain newValue parameter: \(decl)") + } + printExistentialBoxSetterDowncall( + &printer, + decl, + protocolType: protocolType, + newValueArgument: newValueArgument, + ) + return + } + // Callee let callee: String = switch decl.functionSignature.selfParameter { @@ -737,6 +788,48 @@ extension JNISwift2JavaGenerator { "return \(nativeSignature.result.javaType.swiftJniPlaceholderExpr)" } + /// Prints the body of an existential box's setter thunk. + private func printExistentialBoxSetterDowncall( + _ printer: inout SwiftPrinter, + _ decl: ExtractedFunc, + protocolType: SwiftNominalType, + newValueArgument: String, + ) { + // We need to do a "load-mutate-store" flow, because + // the underlying type could be a struct, so we cannot mutate that in place. + + let existentialType = SwiftKitPrinting.renderExistentialType([protocolType]) + let selfName = "selfPointer" + + printer.print( + """ + guard let \(selfName)TypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: selfTypePointer, in: environment))) else { + fatalError("selfTypePointer memory address was null") + } + let \(selfName)DynamicType$: Any.Type = unsafeBitCast(\(selfName)TypeMetadataPointer$, to: Any.Type.self) + guard let \(selfName)RawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: \(selfName), in: environment))) else { + fatalError("\(selfName) memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + var \(selfName)Existential$: \(existentialType) = \(selfName)RawPointer$.load(as: \(selfName)DynamicType$) as! \(existentialType) + \(selfName)Existential$.\(decl.name) = \(newValueArgument) + func \(selfName)DoStore$(_ value: Ty) { + \(selfName)RawPointer$.assumingMemoryBound(to: Ty.self).pointee = value + } + \(selfName)DoStore$(\(selfName)Existential$) + #else + func \(selfName)DoSet$(_ ty: Ty.Type) { + let typed$ = \(selfName)RawPointer$.assumingMemoryBound(to: Ty.self) + var existential$: \(existentialType) = typed$.pointee as! \(existentialType) + existential$.\(decl.name) = \(newValueArgument) + typed$.pointee = existential$ as! Ty + } + _openExistential(\(selfName)DynamicType$, do: \(selfName)DoSet$) + #endif + """ + ) + } + private func printCDecl( _ printer: inout SwiftPrinter, _ translatedDecl: TranslatedFunctionDecl, @@ -1149,6 +1242,12 @@ extension SwiftNominalTypeDeclaration { "_\(javaInterfaceName.firstCharacterLowercased)Interface" } + /// The name of the generated Java class that boxes a value returned as + /// `any P` / `some P` from a Swift function. + var javaExistentialBoxName: String { + "\(safeProtocolName)Box" + } + var generatedJavaClassMacroName: String { if let parent { return "\(parent.generatedJavaClassMacroName).Java\(self.name)" diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift index b0379e2f4..c3006b00c 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator.swift @@ -54,6 +54,9 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { var translatedEnumCases: [ExtractedEnumCase: TranslatedEnumCase] = [:] var interfaceProtocolWrappers: [ExtractedNominalType: JavaInterfaceSwiftWrapper] = [:] + /// Protocols that should be boxed to support returning them as `any P / some P` + private(set) var existentialProtocolBoxes: [ExtractedNominalType] = [] + /// Duplicate identifier tracking for the current batch of methods being generated. var currentJavaIdentifiers: JavaIdentifierFactory = JavaIdentifierFactory() @@ -123,6 +126,12 @@ package class JNISwift2JavaGenerator: Swift2JavaGenerator { // in Java. self.interfaceProtocolWrappers = self.generateInterfaceWrappers(Array(self.analysis.extractedTypes.values)) } + + // Every extracted protocol that also gets a plain Java `interface` + // generated for it is eligible to be boxed as an existential. + self.existentialProtocolBoxes = self.analysis.extractedTypes.values + .filter { $0.swiftNominal.kind == .protocol } + .sorted { $0.swiftNominal.qualifiedName < $1.swiftNominal.qualifiedName } } func generate() throws { @@ -151,4 +160,32 @@ extension JNISwift2JavaGenerator { self.analysis.extractedTypes[$0.qualifiedName] } } + + /// The direct (non-inherited) requirements of `type` (a protocol) that are + /// wrappable on the Java side: instance methods and variable accessors + /// (getters/setters), excluding statics and anything whose signature + /// doesn't translate (e.g. referencing `Self`/associated types). + func supportedProtocolRequirements(of type: ExtractedNominalType) -> [ExtractedFunc] { + (type.methods + type.variables).filter { requirement in + !requirement.isStatic && self.translatedDecl(for: requirement) != nil + } + } + + /// All wrappable requirements for `type` (a protocol), including those + /// inherited from refined protocols — the transitive closure of + /// `supportedProtocolRequirements(of:)`. Used to build an existential + /// box's method bodies and per-requirement `@_cdecl` dispatch thunks, + /// since the box must implement everything the protocol (directly or + /// transitively) requires. + func allProtocolRequirementMethods(of type: ExtractedNominalType) -> [ExtractedFunc] { + var visited: Set = [] + var queue: [ExtractedNominalType] = [type] + var methods: [ExtractedFunc] = [] + while let current = queue.popLast() { + guard visited.insert(ObjectIdentifier(current)).inserted else { continue } + methods.append(contentsOf: self.supportedProtocolRequirements(of: current)) + queue.append(contentsOf: inheritedProtocols(of: current)) + } + return methods + } } diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 30bfa3766..38b7bd0b7 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -74,12 +74,13 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Protocols static requirements: `static func`, `init(rawValue:)` | ❌ | ❌ | | Existential parameters `f(x: any SomeProtocol)` (excepts `Any`) | ❌ | ✅ | | Existential parameters `f(x: any (A & B)) ` | ❌ | ✅ | -| Existential return types `f() -> any Collection` | ❌ | ❌ | +| Existential return types of a single protocol: `f() -> any SomeProtocol` | ❌ | ✅ | +| Existential return types of a composite: `f() -> any (A & B)` | ❌ | ❌ | | Foundation Data and DataProtocol: `f(x: any DataProtocol) -> Data` | ✅ | ✅ | | Foundation Date: `f(date: Date) -> Date` | ❌ | ✅ | | Foundation UUID: `f(uuid: UUID) -> UUID` | ❌ | ✅ | | Opaque parameters: `func take(worker: some Builder) -> some Builder` | ❌ | ✅ | -| Opaque return types: `func get() -> some Builder` | ❌ | ❌ | +| Opaque return types: `func get() -> some Builder` | ❌ | ✅ | | Optional parameters: `func f(i: Int?, class: MyClass?)` | ✅ | ✅ | | Optional return types: `func f() -> Int?`, `func g() -> MyClass?` | ❌ | ✅ | | Primitive types: `Bool`, `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Float`, `Double` | ✅ | ✅ | @@ -355,7 +356,44 @@ which conform a to a given Swift protocol. #### Returning protocol types -Protocols are not yet supported as return types. +> Note: Returning protocol types is currently only supported in JNI mode. + +Functions that return an existential (`any SomeProtocol`) or opaque (`some SomeProtocol`) +value of a single protocol are supported. For example: + +```swift +protocol Greeter { + func greeting() -> String +} + +func makeEnglishGreeter(name: String) -> any Greeter { ... } +func makeOpaqueGreeter(name: String) -> some Greeter { ... } +``` + +The returned value is wrapped in a generated *existential box* — a Java class named +`Box` that implements the protocol's Java `interface`. The box carries the +concrete (dynamic) value together with its type metadata, and dispatches each protocol +requirement through a dedicated native thunk that reconstructs the existential from that +value. This means the dynamic type of the returned value is preserved, and each +requirement is called on the underlying concrete conformer: + +```java +try (var arena = SwiftArena.ofConfined()) { + Greeter english = MySwiftLibrary.makeEnglishGreeter("World", arena); + Greeter danish = MySwiftLibrary.makeDanishGreeter("Verden", arena); + assertEquals("Hello, World!", english.greeting()); + assertEquals("Hej, Verden!", danish.greeting()); +} +``` + +Using the returned value works just like using any other imported interface: its +requirements are callable through the box, it can be passed back into functions that +accept the protocol (including generic and opaque parameters), and refined protocols +expose both their own and their inherited requirements. + +Static requirements (`static func`, `init`) and returning a *composite* existential +(`any (A & B)`) are not currently supported; such requirements are simply omitted from +the generated box, and composite returns are not extracted. ### Swift closures diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index 43f627a8a..a3cadbd09 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -51,6 +51,37 @@ struct JNIProtocolTests { } """ + let returnSource = """ + public protocol Greeter { + public func greeting() -> String + public var favoriteNumber: Int64 { get set } + } + + public func makeGreeter() -> any Greeter + public func makeOpaqueGreeter() -> some Greeter + """ + + let partialReturnSource = """ + public protocol Loader { + public func load() -> Int64 + public static func make() -> Loader + } + + public func makeLoader() -> any Loader + """ + + let protocolInheritanceReturnSource = """ + public protocol ParentProtocol { + public func parentMethod() + } + + public protocol ChildProtocol: ParentProtocol { + public func childMethod() + } + + public func makeChild() -> any ChildProtocol + """ + @Test func generatesJavaInterface() throws { try assertOutput( @@ -418,4 +449,197 @@ struct JNIProtocolTests { ] ) } + + @Test + func returnsExistentialProtocol_java() throws { + try assertOutput( + input: returnSource, + config: config, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + final class GreeterBox implements JNISwiftInstance, Greeter { + ... + public static GreeterBox wrapMemoryAddressUnsafe(long selfPointer, long selfTypePointer, SwiftArena swiftArena) { + return new GreeterBox(selfPointer, selfTypePointer, swiftArena); + } + """, + """ + public long $typeMetadataAddress() { + return this.selfTypePointer; + } + """, + """ + public java.lang.String greeting() { + return GreeterBox.$greeting(this.$memoryAddress(), this.$typeMetadataAddress()); + } + private static native java.lang.String $greeting(long selfPointer, long selfTypePointer); + """, + """ + public static Greeter makeGreeter(SwiftArena swiftArena) { + org.swift.swiftkit.core._OutSwiftGenericInstance result = new org.swift.swiftkit.core._OutSwiftGenericInstance(); + SwiftModule.$makeGreeter(result); + return GreeterBox.wrapMemoryAddressUnsafe(result.selfPointer, result.selfTypePointer, swiftArena); + } + private static native void $makeGreeter(org.swift.swiftkit.core._OutSwiftGenericInstance resultOut); + """, + """ + public static Greeter makeOpaqueGreeter(SwiftArena swiftArena) { + org.swift.swiftkit.core._OutSwiftGenericInstance result = new org.swift.swiftkit.core._OutSwiftGenericInstance(); + SwiftModule.$makeOpaqueGreeter(result); + return GreeterBox.wrapMemoryAddressUnsafe(result.selfPointer, result.selfTypePointer, swiftArena); + } + private static native void $makeOpaqueGreeter(org.swift.swiftkit.core._OutSwiftGenericInstance resultOut); + """, + ] + ) + } + + @Test + func returnsExistentialProtocol_swift() throws { + try assertOutput( + input: returnSource, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_SwiftModule__00024makeGreeter__Lorg_swift_swiftkit_core__1OutSwiftGenericInstance_2") + public func Java_com_example_swift_SwiftModule__00024makeGreeter__Lorg_swift_swiftkit_core__1OutSwiftGenericInstance_2(environment: UnsafeMutablePointer!, thisClass: jclass, resultOut: jobject?) { + let resultExistential$: (any Greeter) = SwiftModule.makeGreeter() + ... + do { + let (selfPointerBits$, selfTypePointerBits$) = resultBoxed$ + environment.interface.SetLongField(environment, resultOut, _JNIMethodIDCache._OutSwiftGenericInstance.selfPointer, selfPointerBits$.getJNIValue(in: environment)) + environment.interface.SetLongField(environment, resultOut, _JNIMethodIDCache._OutSwiftGenericInstance.selfTypePointer, selfTypePointerBits$.getJNIValue(in: environment)) + } + """, + """ + @_cdecl("Java_com_example_swift_GreeterBox__00024greeting__JJ") + public func Java_com_example_swift_GreeterBox__00024greeting__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong, selfTypePointer: jlong) -> jstring? { + guard let selfPointerTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: selfTypePointer, in: environment))) else { + fatalError("selfTypePointer memory address was null") + } + let selfPointerDynamicType$: Any.Type = unsafeBitCast(selfPointerTypeMetadataPointer$, to: Any.Type.self) + guard let selfPointerRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: selfPointer, in: environment))) else { + fatalError("selfPointer memory address was null") + } + #if hasFeature(ImplicitOpenExistentials) + let selfPointerExistential$ = selfPointerRawPointer$.load(as: selfPointerDynamicType$) as! (any Greeter) + #else + func selfPointerDoLoad(_ ty: Ty.Type) -> (any Greeter) { + selfPointerRawPointer$.load(as: ty) as! (any Greeter) + } + let selfPointerExistential$ = _openExistential(selfPointerDynamicType$, do: selfPointerDoLoad) + #endif + return selfPointerExistential$.greeting().getJNILocalRefValue(in: environment) + } + """, + ] + ) + } + + @Test + func existentialBoxPropertyAccessors_swift() throws { + try assertOutput( + input: returnSource, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + @_cdecl("Java_com_example_swift_GreeterBox__00024getFavoriteNumber__JJ") + public func Java_com_example_swift_GreeterBox__00024getFavoriteNumber__JJ(environment: UnsafeMutablePointer!, thisClass: jclass, selfPointer: jlong, selfTypePointer: jlong) -> jlong { + ... + return selfPointerExistential$.favoriteNumber.getJNILocalRefValue(in: environment) + } + """, + """ + @_cdecl("Java_com_example_swift_GreeterBox__00024setFavoriteNumber__JJJ") + public func Java_com_example_swift_GreeterBox__00024setFavoriteNumber__JJJ(environment: UnsafeMutablePointer!, thisClass: jclass, newValue: jlong, selfPointer: jlong, selfTypePointer: jlong) { + ... + #if hasFeature(ImplicitOpenExistentials) + var selfPointerExistential$: (any Greeter) = selfPointerRawPointer$.load(as: selfPointerDynamicType$) as! (any Greeter) + selfPointerExistential$.favoriteNumber = Int64(fromJNI: newValue, in: environment) + func selfPointerDoStore$(_ value: Ty) { + selfPointerRawPointer$.assumingMemoryBound(to: Ty.self).pointee = value + } + selfPointerDoStore$(selfPointerExistential$) + #else + func selfPointerDoSet$(_ ty: Ty.Type) { + let typed$ = selfPointerRawPointer$.assumingMemoryBound(to: Ty.self) + var existential$: (any Greeter) = typed$.pointee as! (any Greeter) + existential$.favoriteNumber = Int64(fromJNI: newValue, in: environment) + typed$.pointee = existential$ as! Ty + } + _openExistential(selfPointerDynamicType$, do: selfPointerDoSet$) + #endif + } + """, + ] + ) + } + + @Test + func existentialBoxOmitsUnsupportedRequirement() throws { + try assertOutput( + input: partialReturnSource, + config: config, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + public interface Loader extends JNISwiftInstance { + ... + public long load(); + ... + } + """, + """ + final class LoaderBox implements JNISwiftInstance, Loader { + ... + public long load() { + return LoaderBox.$load(this.$memoryAddress(), this.$typeMetadataAddress()); + } + private static native long $load(long selfPointer, long selfTypePointer); + """, + ], + notExpectedChunks: [ + "// Loader.make", + "$make(", + ] + ) + } + + @Test + func returnsInheritedProtocol_java() throws { + try assertOutput( + input: protocolInheritanceReturnSource, + config: config, + .jni, + .java, + detectChunkByInitialLines: 1, + expectedChunks: [ + """ + final class ChildProtocolBox implements JNISwiftInstance, ChildProtocol { + ... + public void childMethod() { + ChildProtocolBox.$childMethod(this.$memoryAddress(), this.$typeMetadataAddress()); + } + private static native void $childMethod(long selfPointer, long selfTypePointer); + """, + """ + public void parentMethod() { + ChildProtocolBox.$parentMethod(this.$memoryAddress(), this.$typeMetadataAddress()); + } + private static native void $parentMethod(long selfPointer, long selfTypePointer); + """, + ] + ) + } }