From 4bc106f971fc6aeee5d55a4adbd6faaeb99bd3cd Mon Sep 17 00:00:00 2001 From: Om Singhal Date: Mon, 5 Oct 2026 19:35:02 -0400 Subject: [PATCH 1/2] JNI: fix existential result boxing in Swift 6 language mode Results of type `any P` or `some P` are boxed in a branch guarded by `#if hasFeature(ImplicitOpenExistentials)`. That branch used `UnsafeMutablePointer`, which is not valid Swift, so the generated thunks failed to compile in Swift 6 language mode or with the upcoming feature enabled. Define the generic box helper above the `#if` and call it directly when the feature is on, since the call then opens the existential. Swift 5 mode still uses `_openExistential` and compiles the same code as before. --- ...wift2JavaGenerator+NativeTranslation.swift | 15 ++++------ .../JNI/JNIProtocolTests.swift | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift index 229e48f55..764edaa6f 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+NativeTranslation.swift @@ -1640,23 +1640,20 @@ extension JNISwift2JavaGenerator { // existential so the boxing helper below can open it uniformly. printer.print("let \(existentialName): \(existentialType) = \(inner)") + // With ImplicitOpenExistentials, passing the existential to the generic + // helper opens it, so T is the dynamic type. Without the feature, T would + // be the existential type itself, so it has to be opened explicitly. 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))) } + #if hasFeature(ImplicitOpenExistentials) + let \(boxedName) = \(name)Box$(\(existentialName)) + #else let \(boxedName) = _openExistential(\(existentialName), do: \(name)Box$) #endif """ diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index fc1e971bf..eef6119d9 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -542,6 +542,36 @@ struct JNIProtocolTests { ) } + @Test + func returnsExistentialProtocol_boxing_swift() throws { + try assertOutput( + input: returnSource, + config: config, + .jni, + .swift, + detectChunkByInitialLines: 2, + expectedChunks: [ + """ + let resultExistential$: (any Greeter) = SwiftModule.makeGreeter() + func resultBox$(_ 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))) + } + #if hasFeature(ImplicitOpenExistentials) + let resultBoxed$ = resultBox$(resultExistential$) + #else + let resultBoxed$ = _openExistential(resultExistential$, do: resultBox$) + #endif + """ + ], + notExpectedChunks: [ + "UnsafeMutablePointer" + ] + ) + } + @Test func existentialBoxPropertyAccessors_swift() throws { try assertOutput( From a82b4ba677f4543c2ef4a13ca3e5244e293e58b7 Mon Sep 17 00:00:00 2001 From: Om Singhal Date: Mon, 5 Oct 2026 20:49:54 -0400 Subject: [PATCH 2/2] JNI: run the existential result samples with ImplicitOpenExistentials The JNI sample builds MySwiftLibrary in Swift 5 mode, so its runtime tests only went through the `_openExistential` branch of the generated thunks, and the branch taken in Swift 6 mode was never compiled in CI. Enable the ImplicitOpenExistentials upcoming feature for that target so the protocol tests run through that branch, and add GreeterFactory to cover a method and a computed property returning `any Greeter`, and a method returning `some Greeter`. The new tests also check that the returned value carries the concrete type's metadata. --- .../Package.swift | 5 ++- .../MySwiftLibrary/ReturnProtocol.swift | 20 +++++++++++ .../com/example/swift/ReturnProtocolTest.java | 36 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/Package.swift b/Samples/SwiftJavaExtractJNISampleApp/Package.swift index 4f86b1a9e..ba0313608 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Package.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Package.swift @@ -51,7 +51,10 @@ let package = Package( "swift-java.config" ], swiftSettings: [ - .swiftLanguageMode(.v5) + .swiftLanguageMode(.v5), + // Generated thunks use a different code path when existentials are opened + // implicitly (the Swift 6 default), so enable it to build and test that path. + .enableUpcomingFeature("ImplicitOpenExistentials"), ], plugins: [ .plugin(name: "JExtractSwiftPlugin", package: "swift-java") diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift index d82574ed6..0111894a9 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/ReturnProtocol.swift @@ -68,3 +68,23 @@ public func makeOpaqueGreeter(name: String) -> some Greeter { public func describeGreeter(_ greeter: any Greeter) -> String { greeter.greeting() } + +public final class GreeterFactory { + public let name: String + + public init(name: String) { + self.name = name + } + + public var englishGreeter: any Greeter { + EnglishGreeter(name: name) + } + + public func makeDanishGreeter() -> any Greeter { + DanishGreeter(name: name) + } + + public func makeOpaqueGreeter() -> some Greeter { + DanishGreeter(name: name) + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java index 2095e7c08..0e713490b 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolTest.java @@ -72,4 +72,40 @@ void returnedProtocolRoundTripsIntoParameter() { assertEquals("Hej, Verden!", MySwiftLibrary.describeGreeter(greeter)); } } + + @Test + void returnExistentialFromMethod() { + try (var arena = SwiftArena.ofConfined()) { + GreeterFactory factory = GreeterFactory.init("Verden", arena); + Greeter greeter = factory.makeDanishGreeter(arena); + long danishType = DanishGreeter.init("Verden", arena).$typeMetadataAddress(); + assertEquals(danishType, ((JNISwiftInstance) greeter).$typeMetadataAddress()); + assertEquals("Hej, Verden!", greeter.greeting()); + assertEquals("Hej, Verden! Hej, Verden!", greeter.repeated(2)); + } + } + + @Test + void returnOpaqueFromMethod() { + try (var arena = SwiftArena.ofConfined()) { + GreeterFactory factory = GreeterFactory.init("Verden", arena); + Greeter greeter = factory.makeOpaqueGreeter(arena); + long danishType = DanishGreeter.init("Verden", arena).$typeMetadataAddress(); + assertEquals(danishType, ((JNISwiftInstance) greeter).$typeMetadataAddress()); + assertEquals("Hej, Verden!", greeter.greeting()); + assertEquals("Hej, Verden!", MySwiftLibrary.describeGreeter(greeter)); + } + } + + @Test + void returnExistentialFromComputedProperty() { + try (var arena = SwiftArena.ofConfined()) { + GreeterFactory factory = GreeterFactory.init("World", arena); + Greeter greeter = factory.getEnglishGreeter(arena); + long englishType = EnglishGreeter.init("World", arena).$typeMetadataAddress(); + assertEquals(englishType, ((JNISwiftInstance) greeter).$typeMetadataAddress()); + assertEquals("Hello, World!", greeter.greeting()); + assertEquals("Hello, World! Hello, World!", greeter.repeated(2)); + } + } }