From dc5995ed68087deaf1b21f1447fede067e4d51cf Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Wed, 1 Jul 2026 15:56:50 +0200 Subject: [PATCH 1/2] add support for returning protocols --- .../MySwiftLibrary/ConcreteProtocolAB.swift | 7 + .../Sources/MySwiftLibrary/ProtocolA.swift | 24 ++ .../Sources/MySwiftLibrary/ProtocolC.swift | 4 + .../MySwiftLibrary/ProtocolWithStatic.swift | 12 + .../MySwiftLibrary/ReturnProtocol.swift | 66 ++++ .../Sources/MySwiftLibrary/RichGreeter.swift | 64 ++++ .../swift/ReturnInheritedProtocolTest.java | 99 ++++++ .../swift/ReturnProtocolDispatchTest.java | 76 +++++ .../com/example/swift/ReturnProtocolTest.java | 73 +++++ .../example/swift/ReturnRichProtocolTest.java | 85 +++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 300 +++++++++++++----- ...ISwift2JavaGenerator+JavaTranslation.swift | 43 ++- ...wift2JavaGenerator+NativeTranslation.swift | 138 +++++++- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 120 ++++++- .../JNI/JNISwift2JavaGenerator.swift | 37 +++ .../Documentation.docc/SupportedFeatures.md | 45 ++- .../JNI/JNIProtocolTests.swift | 224 +++++++++++++ 17 files changed, 1317 insertions(+), 100 deletions(-) create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/RichGreeter.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnInheritedProtocolTest.java create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolDispatchTest.java create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnRichProtocolTest.java 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 4da66f1fc..f10d45cc6 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,112 @@ 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) { + 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. + /// Reuses `javaTranslator.translate(_:)` for the + /// parameter/result shape, but the translation is *not* cached via + /// `translatedDecl(for:)` (that cache is keyed by the protocol method decl + /// and is shared with the plain `interface P` printing, which never prints + /// a body) — and `parentName` is overridden to the box's own name so the + /// native downcall target and JNI symbol are unique to the box, not the + /// interface. + 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 +358,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: ", ") - 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: ", ")), swiftArena); - } - - public static\(genericClause) \(javaName)\(genericClause) wrapMemoryAddressUnsafe(\(swiftPointerArg)) { - return new \(javaName)\(genericClause)(\(swiftPointerParams.joined(separator: ", ")), 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 +435,113 @@ extension JNISwift2JavaGenerator { printTypeMetadataAddressFunction(&printer, decl) printer.println() + printSwiftInstanceObjectMethods(&printer) + printer.println() + } + } + + /// Prints the designated (memory-managed) constructor shared by every `JNISwiftInstance` + /// (both existential boxes and concrete imported types): it takes the pointer params + /// (`selfPointer`, and `selfTypePointer` when present) plus a `SwiftArena`, null-checks and + /// stores each pointer, creates the cleanup, and registers with the arena. + 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` + /// (both existential boxes and concrete imported types). The doc summary sentence differs + /// between callers (a box also carries type metadata), so it is passed in verbatim. + private func printWrapMemoryAddressUnsafeFactory( + _ printer: inout JavaPrinter, + javaName: String, + 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 the `equals`/`hashCode`/`toString`/`toDebugString` overrides shared by + /// every `JNISwiftInstance` (both existential boxes and concrete imported types): + /// they all delegate to `SwiftObjects` using `$memoryAddress()`/`$typeMetadataAddress()`. + 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 0ce3291f8..635dfbe02 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? @@ -368,7 +365,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 } @@ -967,7 +966,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) } } @@ -2088,6 +2115,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 e9212ed69..36f893412 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), @@ -815,11 +834,38 @@ 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 exactly like a + /// generic-instance return: `(selfPointer, selfTypePointer)` via + /// `_OutSwiftGenericInstance`. + 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], @@ -1140,6 +1186,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 { @@ -1228,6 +1281,13 @@ extension JNISwift2JavaGenerator { /// Allocate memory for a Swift value and outputs the pointer indirect case allocateSwiftValue(NativeSwiftConversionStep, name: String, swiftType: SwiftType) + /// Boxes a returned `any P` / `some P` value by opening the existential: + /// allocates memory for, and initializes it with, the concrete + /// dynamic value inside the existential container, and captures the + /// concrete dynamic type metadata. + /// Outputs `(pointerBits, metadataPointerBits)`. + 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) @@ -1268,6 +1328,17 @@ extension JNISwift2JavaGenerator { outArgumentName: String ) + /// Writes a boxed existential's `(pointerBits, metadataPointerBits)` + /// pair — as produced by `allocateExistentialValue` — into the + /// `_OutSwiftGenericInstance` out-object's `selfPointer` / + /// `selfTypePointer` fields. Structurally identical to + /// `genericValueIndirectReturn`'s `SetLongField` shape, but the metadata + /// comes from the opened existential's dynamic type, not a static type. + indirect case existentialValueIndirectReturn( + NativeSwiftConversionStep, + outArgumentName: String + ) + indirect case constructor( _ swiftType: SwiftType, arguments: [(String?, NativeSwiftConversionStep)] = [] @@ -1487,6 +1558,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" @@ -1680,6 +1785,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 14e075b02..36262a73e 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -372,11 +372,41 @@ 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). + /// + /// Each thunk carries both `selfPointer` and `selfTypePointer` — exactly + /// like a generic-instance method thunk — and reconstructs `any P` from + /// them via `extractSwiftProtocolValue` before + /// calling the requirement directly on the opened existential. + 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) { @@ -610,6 +640,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 { @@ -722,6 +778,58 @@ extension JNISwift2JavaGenerator { "return \(nativeSignature.result.javaType.swiftJniPlaceholderExpr)" } + /// Prints the body of an existential box's per-requirement setter thunk: + /// `(selfPointer, selfTypePointer)` identify the concrete dynamic value + /// boxed by `any P`, but unlike the read path there's no `let` + /// reconstruction we can assign through — opening an existential yields a + /// *copy* of the underlying value, so mutating that copy would silently + /// not persist for value types. + /// + /// Instead this opens the existential generically over the concrete + /// dynamic type `Ty` bound to `selfTypePointer`'s metadata, reads the + /// typed value out of `selfPointer`'s storage as `any P`, mutates the + /// requirement being set on that existential copy, and writes the mutated + /// existential back into `selfPointer`'s storage as `Ty` — a normal + /// in-place mutation for reference types, and a real load-mutate-store + /// round-trip for value types. + private func printExistentialBoxSetterDowncall( + _ printer: inout SwiftPrinter, + _ decl: ExtractedFunc, + protocolType: SwiftNominalType, + newValueArgument: String, + ) { + 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, @@ -1135,6 +1243,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 bbb6e1b0b..5e5def7bf 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` | ✅ | ✅ | @@ -352,7 +353,45 @@ but specifically, for allowing passing concrete binding types generated by jextr 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. ### `async` functions 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); + """, + ] + ) + } } From f31452104d380627a051c85c594358b2d04cc25b Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Mon, 6 Jul 2026 12:27:27 +0200 Subject: [PATCH 2/2] PR feedback --- ...t2JavaGenerator+JavaBindingsPrinting.swift | 22 ++++-------------- ...wift2JavaGenerator+NativeTranslation.swift | 15 +----------- ...ift2JavaGenerator+SwiftThunkPrinting.swift | 23 ++++--------------- 3 files changed, 10 insertions(+), 50 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index e26b783e8..eda831e90 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -220,6 +220,8 @@ extension JNISwift2JavaGenerator { /// 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) @@ -287,13 +289,6 @@ extension JNISwift2JavaGenerator { } /// Prints one existential box method. - /// Reuses `javaTranslator.translate(_:)` for the - /// parameter/result shape, but the translation is *not* cached via - /// `translatedDecl(for:)` (that cache is keyed by the protocol method decl - /// and is shared with the plain `interface P` printing, which never prints - /// a body) — and `parentName` is overridden to the box's own name so the - /// native downcall target and JNI symbol are unique to the box, not the - /// interface. private func printExistentialBoxMethod( _ printer: inout JavaPrinter, _ decl: ExtractedNominalType, @@ -441,9 +436,6 @@ extension JNISwift2JavaGenerator { } /// Prints the designated (memory-managed) constructor shared by every `JNISwiftInstance` - /// (both existential boxes and concrete imported types): it takes the pointer params - /// (`selfPointer`, and `selfTypePointer` when present) plus a `SwiftArena`, null-checks and - /// stores each pointer, creates the cleanup, and registers with the arena. private func printDesignatedConstructor( _ printer: inout JavaPrinter, javaName: String, @@ -480,12 +472,10 @@ extension JNISwift2JavaGenerator { } } - /// Prints the `wrapMemoryAddressUnsafe` factory pair shared by every `JNISwiftInstance` - /// (both existential boxes and concrete imported types). The doc summary sentence differs - /// between callers (a box also carries type metadata), so it is passed in verbatim. + /// Prints the `wrapMemoryAddressUnsafe` factory pair shared by every `JNISwiftInstance`. private func printWrapMemoryAddressUnsafeFactory( _ printer: inout JavaPrinter, - javaName: String, + javaName: JavaClassName, genericClause: String, pointerParams: [String], docSummary: String, @@ -514,9 +504,7 @@ extension JNISwift2JavaGenerator { ) } - /// Prints the `equals`/`hashCode`/`toString`/`toDebugString` overrides shared by - /// every `JNISwiftInstance` (both existential boxes and concrete imported types): - /// they all delegate to `SwiftObjects` using `$memoryAddress()`/`$typeMetadataAddress()`. + /// Prints common Swift object methods such as `equals`, `hashCode` etc. private func printSwiftInstanceObjectMethods(_ printer: inout JavaPrinter) { printer.print( """ diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 5b360d85b..1275f0944 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -849,9 +849,7 @@ extension JNISwift2JavaGenerator { } } - /// Boxes a returned `any P` / `some P` value exactly like a - /// generic-instance return: `(selfPointer, selfTypePointer)` via - /// `_OutSwiftGenericInstance`. + /// Boxes a returned `any P` / `some P` value. private func translateProtocolResult( protocolType: SwiftNominalType, resultName: String @@ -1281,11 +1279,6 @@ extension JNISwift2JavaGenerator { /// Allocate memory for a Swift value and outputs the pointer indirect case allocateSwiftValue(NativeSwiftConversionStep, name: String, swiftType: SwiftType) - /// Boxes a returned `any P` / `some P` value by opening the existential: - /// allocates memory for, and initializes it with, the concrete - /// dynamic value inside the existential container, and captures the - /// concrete dynamic type metadata. - /// Outputs `(pointerBits, metadataPointerBits)`. indirect case allocateExistentialValue(NativeSwiftConversionStep, name: String, protocolTypes: [SwiftNominalType]) /// The thing to which the pointer typed, which is the `pointee` property @@ -1328,12 +1321,6 @@ extension JNISwift2JavaGenerator { outArgumentName: String ) - /// Writes a boxed existential's `(pointerBits, metadataPointerBits)` - /// pair — as produced by `allocateExistentialValue` — into the - /// `_OutSwiftGenericInstance` out-object's `selfPointer` / - /// `selfTypePointer` fields. Structurally identical to - /// `genericValueIndirectReturn`'s `SetLongField` shape, but the metadata - /// comes from the opened existential's dynamic type, not a static type. indirect case existentialValueIndirectReturn( NativeSwiftConversionStep, outArgumentName: String diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift index 158a637c0..cf6230a01 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+SwiftThunkPrinting.swift @@ -387,11 +387,6 @@ extension JNISwift2JavaGenerator { /// 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). - /// - /// Each thunk carries both `selfPointer` and `selfTypePointer` — exactly - /// like a generic-instance method thunk — and reconstructs `any P` from - /// them via `extractSwiftProtocolValue` before - /// calling the requirement directly on the opened existential. private func printExistentialBoxDispatchThunks(_ printer: inout SwiftPrinter, _ type: ExtractedNominalType) { let boxParentName = SwiftQualifiedTypeName(type.swiftNominal.javaExistentialBoxName) @@ -778,26 +773,16 @@ extension JNISwift2JavaGenerator { "return \(nativeSignature.result.javaType.swiftJniPlaceholderExpr)" } - /// Prints the body of an existential box's per-requirement setter thunk: - /// `(selfPointer, selfTypePointer)` identify the concrete dynamic value - /// boxed by `any P`, but unlike the read path there's no `let` - /// reconstruction we can assign through — opening an existential yields a - /// *copy* of the underlying value, so mutating that copy would silently - /// not persist for value types. - /// - /// Instead this opens the existential generically over the concrete - /// dynamic type `Ty` bound to `selfTypePointer`'s metadata, reads the - /// typed value out of `selfPointer`'s storage as `any P`, mutates the - /// requirement being set on that existential copy, and writes the mutated - /// existential back into `selfPointer`'s storage as `Ty` — a normal - /// in-place mutation for reference types, and a real load-mutate-store - /// round-trip for value types. + /// 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"