From 925a43a3a710f2dbb9abaed59097fa57366a3d41 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 7 Jul 2026 10:36:38 +0200 Subject: [PATCH 1/7] add dynamicCast to SwiftObjects --- Sources/SwiftJava/SwiftObjects.swift | 30 +++++++++++++++++++ .../org/swift/swiftkit/core/SwiftObjects.java | 1 + 2 files changed, 31 insertions(+) diff --git a/Sources/SwiftJava/SwiftObjects.swift b/Sources/SwiftJava/SwiftObjects.swift index 1a412094d..d42e7120b 100644 --- a/Sources/SwiftJava/SwiftObjects.swift +++ b/Sources/SwiftJava/SwiftObjects.swift @@ -158,6 +158,36 @@ extension SwiftObjects { } return perform(as: typeMetadata) } + + @JavaMethod + public static func dynamicCast( + environment: UnsafeMutablePointer!, + selfPointer: Int64, + selfTypePointer: Int64, + targetTypePointter: Int64 + ) -> Int64 { + guard let selfType$ = UnsafeRawPointer(bitPattern: Int(selfTypePointer)) else { + fatalError("selfType metadata address was null") + } + guard let targetType$ = UnsafeRawPointer(bitPattern: Int(targetTypePointter)) else { + fatalError("targetType metadata address was null") + } + + let selfTypeMetadata = unsafeBitCast(selfType$, to: Any.Type.self) + let targetTypeMetadata = unsafeBitCast(targetType$, to: Any.Type.self) + + func perform(srcType: S.Type, targetType: T.Type) -> Int64 { + guard let self$ = UnsafeMutablePointer(bitPattern: Int(selfPointer)) else { + fatalError("self memory address was null") + } + + guard let castedValue = self$.pointee as? T else { return 0 } + let castedPointer = UnsafeMutablePointer.allocate(capacity: 1) + castedPointer.initialize(to: castedValue) + return Int64(Int(bitPattern: castedPointer)) + } + return perform(srcType: selfTypeMetadata, targetType: targetTypeMetadata) + } } public class HashableClass: Hashable { diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java index a8cce9166..240162273 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java @@ -31,4 +31,5 @@ public static void requireNonZero(long number, String name) { public static native String typeDescription(long selfTypePointer); public static native boolean equals(long lhsPointer, long lhsTypePointer, long rhsPointer, long rhsTypePointer); public static native int hashCode(long selfPointer, long selfTypePointer); + public static native int dynamicCast(long selfPointer, long selfTypePointer, long targetTypePointer); } From b302c2b6e0129001bff5012f902c223a58dd2d77 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 7 Jul 2026 11:31:49 +0200 Subject: [PATCH 2/7] print cast function --- ...t2JavaGenerator+JavaBindingsPrinting.swift | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index b213c71ca..6389682e3 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -28,6 +28,7 @@ extension JNISwift2JavaGenerator { "org.swift.swiftkit.core.util.*", "org.swift.swiftkit.core.collections.*", "java.util.*", + "java.lang.reflect.Method", // NonNull, Unsigned and friends "org.swift.swiftkit.core.annotations.*", @@ -213,6 +214,9 @@ extension JNISwift2JavaGenerator { printFunctionDowncallMethods(&printer, requirement, skipMethodBody: true) printer.println() } + + // Print the `as` helper + printDynamicCastFunction(&printer, inProtocol: true) } } @@ -431,6 +435,7 @@ extension JNISwift2JavaGenerator { printer.println() printSwiftInstanceObjectMethods(&printer) + printDynamicCastFunction(&printer, inProtocol: false) printer.println() } } @@ -530,6 +535,41 @@ extension JNISwift2JavaGenerator { ) } + private func printDynamicCastFunction(_ printer: inout JavaPrinter, inProtocol: Bool) { + let modifier = inProtocol ? "default" : "public" + + // Java does not support static protocol requirements + // and we need to access the type memory address of T + // so we use Java reflection to call the static native downcall instead. + // and also the "constructor" to get back the correct Java wrapper. + printer.printBraceBlock("\(modifier) Optional as(Class type, SwiftArena arena)") { printer in + printer.print( + """ + if (!(this instanceof JNISwiftInstance src)) return Optional.empty(); + try { + Method m = type.getDeclaredMethod("$typeMetadataAddressDowncall"); + m.setAccessible(true); + long targetType = (long) m.invoke(null); + long p = SwiftObjects.dynamicCast(src.$memoryAddress(), src.$typeMetadataAddress(), targetType); + if (p == 0) return Optional.empty(); + Method wrap = type.getMethod("wrapMemoryAddressUnsafe", long.class, SwiftArena.class); + return Optional.of(type.cast(wrap.invoke(null, p, arena))); + } catch (ReflectiveOperationException e) { + // Not a concrete JExtract type + return Optional.empty(); + } + """ + ) + } + + if config.effectiveMemoryManagementMode.requiresGlobalArena { + printer.println() + printer.printBraceBlock("\(modifier) Optional as(Class type)") { printer in + printer.print("return as(type, SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA);") + } + } + } + /// Prints helpers for specific types like `Foundation.Date` private func printSpecificTypeHelpers(_ printer: inout JavaPrinter, _ decl: ExtractedNominalType) { guard let knownType = decl.swiftNominal.knownTypeKind else { return } From c123c8a710c059c13bd422a062307b9fab80f004 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 7 Jul 2026 12:43:26 +0200 Subject: [PATCH 3/7] move to java type instead --- .../example/swift/ReturnProtocolCastTest.java | 122 ++++++++++++++++++ ...t2JavaGenerator+JavaBindingsPrinting.swift | 42 +----- .../swift/swiftkit/core/JNISwiftInstance.java | 2 +- .../swiftkit/core/SwiftDowncastable.java | 65 ++++++++++ .../org/swift/swiftkit/core/SwiftObjects.java | 2 +- 5 files changed, 191 insertions(+), 42 deletions(-) create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolCastTest.java create mode 100644 SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftDowncastable.java diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolCastTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolCastTest.java new file mode 100644 index 000000000..e04a504d9 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolCastTest.java @@ -0,0 +1,122 @@ +//===----------------------------------------------------------------------===// +// +// 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 java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Exercises the dynamic {@code as} downcast (Swift's {@code as?}) that recovers a + * concrete jextracted Swift type from a value returned as {@code any P} / {@code some P}. + */ +public class ReturnProtocolCastTest { + + /** Downcast a returned {@code any Greeter} to its concrete Swift struct type. */ + @Test + void downcastToConcreteType_succeeds() { + try (var arena = SwiftArena.ofConfined()) { + Greeter greeter = MySwiftLibrary.makeEnglishGreeter("World", arena); + + Optional english = greeter.as(EnglishGreeter.class, arena); + assertTrue(english.isPresent()); + // `name` is a member of the concrete struct, not visible through the protocol. + assertEquals("World", english.get().getName()); + assertEquals("Hello, World!", english.get().greeting()); + } + } + + /** Downcasting to the wrong dynamic type returns empty (a faithful Swift {@code as?}). */ + @Test + void downcastToWrongType_isEmpty() { + try (var arena = SwiftArena.ofConfined()) { + Greeter greeter = MySwiftLibrary.makeEnglishGreeter("World", arena); + + Optional danish = greeter.as(DanishGreeter.class, arena); + assertTrue(danish.isEmpty()); + } + } + + /** {@code some Greeter} (opaque return) downcasts the same as {@code any Greeter}. */ + @Test + void downcastOpaqueReturn_succeeds() { + try (var arena = SwiftArena.ofConfined()) { + Greeter greeter = MySwiftLibrary.makeOpaqueGreeter("World", arena); + + Optional english = greeter.as(EnglishGreeter.class, arena); + assertTrue(english.isPresent()); + assertEquals("World", english.get().getName()); + } + } + + /** + * {@code as} works on a concrete-typed reference too (the {@code public} variant on + * the class, not the {@code default} on the protocol interface). + */ + @Test + void downcastFromConcreteReference() { + try (var arena = SwiftArena.ofConfined()) { + EnglishGreeter greeter = EnglishGreeter.init("World", arena); + + Optional english = greeter.as(EnglishGreeter.class, arena); + assertTrue(english.isPresent()); + assertEquals("World", english.get().getName()); + + Optional danish = greeter.as(DanishGreeter.class, arena); + assertTrue(danish.isEmpty()); + } + } + + /** + * A Java-implemented conformer has no backing Swift value, so {@code as} returns + * empty rather than crashing (the {@code instanceof JNISwiftInstance} guard). This is + * the case that only exists with Java callbacks enabled. + */ + @Test + void downcastJavaImplementedConformer_isEmpty() { + try (var arena = SwiftArena.ofConfined()) { + Greeter javaGreeter = new Greeter() { + @Override + public String greeting() { + return "Hi from Java"; + } + + @Override + public String repeated(long count) { + return "Hi from Java"; + } + }; + assertFalse(javaGreeter instanceof JNISwiftInstance); + + Optional english = javaGreeter.as(EnglishGreeter.class, arena); + assertTrue(english.isEmpty()); + } + } + + /** The downcast result is an independent, fully working Swift instance. */ + @Test + void downcastResultIsAWorkingInstance() { + try (var arena = SwiftArena.ofConfined()) { + Greeter greeter = MySwiftLibrary.makeEnglishGreeter("World", arena); + + EnglishGreeter english = greeter.as(EnglishGreeter.class, arena).orElseThrow(); + assertEquals("Hello, World! Hello, World!", english.repeated(2)); + } + } +} diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 6389682e3..119c26614 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -28,7 +28,6 @@ extension JNISwift2JavaGenerator { "org.swift.swiftkit.core.util.*", "org.swift.swiftkit.core.collections.*", "java.util.*", - "java.lang.reflect.Method", // NonNull, Unsigned and friends "org.swift.swiftkit.core.annotations.*", @@ -198,6 +197,8 @@ extension JNISwift2JavaGenerator { // then we require only JExtracted types can conform to this. if !self.interfaceProtocolWrappers.keys.contains(decl) && !extends.contains("JNISwiftInstance") { extends.append("JNISwiftInstance") + } else if !extends.contains("JNISwiftInstance") { + extends.append("SwiftDowncastable") } let extendsString = extends.isEmpty ? "" : " extends \(extends.joined(separator: .comma))" @@ -214,9 +215,6 @@ extension JNISwift2JavaGenerator { printFunctionDowncallMethods(&printer, requirement, skipMethodBody: true) printer.println() } - - // Print the `as` helper - printDynamicCastFunction(&printer, inProtocol: true) } } @@ -435,7 +433,6 @@ extension JNISwift2JavaGenerator { printer.println() printSwiftInstanceObjectMethods(&printer) - printDynamicCastFunction(&printer, inProtocol: false) printer.println() } } @@ -535,41 +532,6 @@ extension JNISwift2JavaGenerator { ) } - private func printDynamicCastFunction(_ printer: inout JavaPrinter, inProtocol: Bool) { - let modifier = inProtocol ? "default" : "public" - - // Java does not support static protocol requirements - // and we need to access the type memory address of T - // so we use Java reflection to call the static native downcall instead. - // and also the "constructor" to get back the correct Java wrapper. - printer.printBraceBlock("\(modifier) Optional as(Class type, SwiftArena arena)") { printer in - printer.print( - """ - if (!(this instanceof JNISwiftInstance src)) return Optional.empty(); - try { - Method m = type.getDeclaredMethod("$typeMetadataAddressDowncall"); - m.setAccessible(true); - long targetType = (long) m.invoke(null); - long p = SwiftObjects.dynamicCast(src.$memoryAddress(), src.$typeMetadataAddress(), targetType); - if (p == 0) return Optional.empty(); - Method wrap = type.getMethod("wrapMemoryAddressUnsafe", long.class, SwiftArena.class); - return Optional.of(type.cast(wrap.invoke(null, p, arena))); - } catch (ReflectiveOperationException e) { - // Not a concrete JExtract type - return Optional.empty(); - } - """ - ) - } - - if config.effectiveMemoryManagementMode.requiresGlobalArena { - printer.println() - printer.printBraceBlock("\(modifier) Optional as(Class type)") { printer in - printer.print("return as(type, SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA);") - } - } - } - /// 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/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java index 7c8c89344..1526e2117 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java @@ -14,7 +14,7 @@ package org.swift.swiftkit.core; -public interface JNISwiftInstance extends SwiftInstance { +public interface JNISwiftInstance extends SwiftInstance, SwiftDowncastable { /** * Creates a function that will be called when the value should be destroyed. * This will be code-generated to call a native method to do deinitialization and deallocation. diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftDowncastable.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftDowncastable.java new file mode 100644 index 000000000..d149a126f --- /dev/null +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftDowncastable.java @@ -0,0 +1,65 @@ +//===----------------------------------------------------------------------===// +// +// 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 org.swift.swiftkit.core; + +import java.lang.reflect.Method; +import java.util.Optional; + +/** + * Exposes Swift's dynamic {@code as?} cast on jextracted values: recover a concrete + * jextracted Swift type from a value whose static Java type is a protocol interface + * (e.g. a value returned as {@code any P} / {@code some P}) or any other super-type. +*/ +public interface SwiftDowncastable { + + /** + * Swift {@code as?}: attempt to recover {@code type} — a non-generic concrete + * jextracted Swift type — from this value. + * + * @return the recovered instance, or {@link Optional#empty()} if this value's + * dynamic type is not {@code type}, if this value is not backed by a Swift value + * (e.g. a Java-implemented conformer), or if {@code type} is not a concrete + * jextracted type. + */ + default Optional as(Class type, SwiftArena arena) { + if (!(this instanceof JNISwiftInstance)) return Optional.empty(); + JNISwiftInstance src = (JNISwiftInstance) this; + try { + // Java does not support static protocol requirements + // and we need to access the type memory address of T + // so we use Java reflection to call the static native downcall instead. + // and also the "constructor" to get back the correct Java wrapper. + Method metadata = type.getDeclaredMethod("$typeMetadataAddressDowncall"); + metadata.setAccessible(true); + long targetType = (long) metadata.invoke(null); + long p = SwiftObjects.dynamicCast(src.$memoryAddress(), src.$typeMetadataAddress(), targetType); + if (p == 0) return Optional.empty(); + Method wrap = type.getMethod("wrapMemoryAddressUnsafe", long.class, SwiftArena.class); + return Optional.of(type.cast(wrap.invoke(null, p, arena))); + } catch (ReflectiveOperationException e) { + // Not a concrete jextracted type + return Optional.empty(); + } + } + + /** + * Convenience overload using the default automatic arena. + * + * @see #as(Class, SwiftArena) + */ + default Optional as(Class type) { + return as(type, SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA); + } +} diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java index 240162273..785ee758b 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftObjects.java @@ -31,5 +31,5 @@ public static void requireNonZero(long number, String name) { public static native String typeDescription(long selfTypePointer); public static native boolean equals(long lhsPointer, long lhsTypePointer, long rhsPointer, long rhsTypePointer); public static native int hashCode(long selfPointer, long selfTypePointer); - public static native int dynamicCast(long selfPointer, long selfTypePointer, long targetTypePointer); + public static native long dynamicCast(long selfPointer, long selfTypePointer, long targetTypePointer); } From 2932ebe4fe85380a59660ad00fd0574c11546762 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 7 Jul 2026 12:58:51 +0200 Subject: [PATCH 4/7] only apply to protocols --- .../JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift | 4 ++-- .../java/org/swift/swiftkit/core/JNISwiftInstance.java | 2 +- Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift index 119c26614..376bdc867 100644 --- a/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift +++ b/Sources/JExtractSwiftLib/JNI/JNISwift2JavaGenerator+JavaBindingsPrinting.swift @@ -197,9 +197,9 @@ extension JNISwift2JavaGenerator { // then we require only JExtracted types can conform to this. if !self.interfaceProtocolWrappers.keys.contains(decl) && !extends.contains("JNISwiftInstance") { extends.append("JNISwiftInstance") - } else if !extends.contains("JNISwiftInstance") { - extends.append("SwiftDowncastable") } + + extends.append("SwiftDowncastable") let extendsString = extends.isEmpty ? "" : " extends \(extends.joined(separator: .comma))" printer.printBraceBlock("public interface \(decl.effectiveJavaSimpleName)\(extendsString)") { printer in diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java index 1526e2117..7c8c89344 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/JNISwiftInstance.java @@ -14,7 +14,7 @@ package org.swift.swiftkit.core; -public interface JNISwiftInstance extends SwiftInstance, SwiftDowncastable { +public interface JNISwiftInstance extends SwiftInstance { /** * Creates a function that will be called when the value should be destroyed. * This will be code-generated to call a native method to do deinitialization and deallocation. diff --git a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift index a3cadbd09..98f4df649 100644 --- a/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift +++ b/Tests/JExtractSwiftTests/JNI/JNIProtocolTests.swift @@ -101,7 +101,7 @@ struct JNIProtocolTests { import org.swift.swiftkit.core.util.*; """, """ - public interface SomeProtocol { + public interface SomeProtocol extends SwiftDowncastable { ... public void method(); ... @@ -123,14 +123,14 @@ struct JNIProtocolTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - public interface ParentProtocol { + public interface ParentProtocol extends SwiftDowncastable { ... public void parentMethod(); ... } """, """ - public interface ChildProtocol extends ParentProtocol { + public interface ChildProtocol extends ParentProtocol, SwiftDowncastable { ... public void childMethod(); ... @@ -594,7 +594,7 @@ struct JNIProtocolTests { detectChunkByInitialLines: 1, expectedChunks: [ """ - public interface Loader extends JNISwiftInstance { + public interface Loader extends JNISwiftInstance, SwiftDowncastable { ... public long load(); ... From 724082dde0e75c7dda71677c7c33eabdd8ba8c32 Mon Sep 17 00:00:00 2001 From: Mads Odgaard Date: Tue, 7 Jul 2026 13:04:19 +0200 Subject: [PATCH 5/7] docs --- .../Documentation.docc/SupportedFeatures.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 38b7bd0b7..1b1b71f5e 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -76,6 +76,7 @@ SwiftJava's `swift-java jextract` tool automates generating Java bindings from S | Existential parameters `f(x: any (A & B)) ` | ❌ | ✅ | | Existential return types of a single protocol: `f() -> any SomeProtocol` | ❌ | ✅ | | Existential return types of a composite: `f() -> any (A & B)` | ❌ | ❌ | +| Downcasting a returned protocol value to its concrete type: `greeter.as(EnglishGreeter.class, arena)` | ❌ | ✅ | | Foundation Data and DataProtocol: `f(x: any DataProtocol) -> Data` | ✅ | ✅ | | Foundation Date: `f(date: Date) -> Date` | ❌ | ✅ | | Foundation UUID: `f(uuid: UUID) -> UUID` | ❌ | ✅ | @@ -395,6 +396,44 @@ Static requirements (`static func`, `init`) and returning a *composite* existent (`any (A & B)`) are not currently supported; such requirements are simply omitted from the generated box, and composite returns are not extracted. +#### Downcasting to the concrete type (`as`) + +> Note: Downcasting returned protocol values is currently only supported in JNI mode. + +A value returned as `any P` / `some P` preserves its concrete dynamic type, so it can be +recovered — the equivalent of Swift's `as?`. Every imported protocol `interface` therefore +exposes an `as` method: + +```java + Optional as(Class type, SwiftArena arena); + Optional as(Class type); // uses the default arena +``` + +`type` must be a concrete jextracted type (a `class`, `struct`, or `enum` binding — the +`JNISwiftInstance` bound enforces this at compile time). The cast succeeds only when the +value's dynamic Swift type is exactly that type, in which case you receive a fresh binding +registered in the given arena; otherwise the result is `Optional.empty()`. + +Continuing the `Greeter` example, where `makeEnglishGreeter` returns `any Greeter` backed +by a concrete `EnglishGreeter`: + +```java +try (var arena = SwiftArena.ofConfined()) { + Greeter greeter = MySwiftLibrary.makeEnglishGreeter("World", arena); + + // Recover the concrete type — its `struct`-only members become available: + Optional english = greeter.as(EnglishGreeter.class, arena); + assertEquals("World", english.orElseThrow().getName()); + + // A cast to the wrong dynamic type yields an empty Optional: + assertTrue(greeter.as(DanishGreeter.class, arena).isEmpty()); +} +``` + +The cast returns `Optional.empty()` — rather than throwing — whenever it cannot apply: when +the dynamic type differs, when `type` is not a concrete jextracted type (e.g. a generic type +or another protocol), or when the receiver is not backed by a wrapped Swift value. + ### Swift closures Non-escaping closures are called synchronously by the Swift function they are passed to, From 795a8f4cfae0da38e0b62740d182fcaad1edff66 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Thu, 9 Jul 2026 10:29:40 +0900 Subject: [PATCH 6/7] Update Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolCastTest.java --- .../src/test/java/com/example/swift/ReturnProtocolCastTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolCastTest.java b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolCastTest.java index e04a504d9..48263b655 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolCastTest.java +++ b/Samples/SwiftJavaExtractJNISampleApp/src/test/java/com/example/swift/ReturnProtocolCastTest.java @@ -109,7 +109,6 @@ public String repeated(long count) { } } - /** The downcast result is an independent, fully working Swift instance. */ @Test void downcastResultIsAWorkingInstance() { try (var arena = SwiftArena.ofConfined()) { From e0181a1d4746c7c81c30c1d11fa1e1912c9599d9 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Thu, 9 Jul 2026 10:43:57 +0900 Subject: [PATCH 7/7] Apply suggestions from code review --- .../Documentation.docc/SupportedFeatures.md | 11 +++++------ .../org/swift/swiftkit/core/SwiftDowncastable.java | 10 +++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md index 1b1b71f5e..a76787756 100644 --- a/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md +++ b/Sources/SwiftJavaDocumentation/Documentation.docc/SupportedFeatures.md @@ -401,7 +401,7 @@ the generated box, and composite returns are not extracted. > Note: Downcasting returned protocol values is currently only supported in JNI mode. A value returned as `any P` / `some P` preserves its concrete dynamic type, so it can be -recovered — the equivalent of Swift's `as?`. Every imported protocol `interface` therefore +recovered; This is equivalent to Swift's `as?`, of a checked `instanceof` cast in Java. Every imported protocol `interface` therefore exposes an `as` method: ```java @@ -409,8 +409,8 @@ exposes an `as` method: Optional as(Class type); // uses the default arena ``` -`type` must be a concrete jextracted type (a `class`, `struct`, or `enum` binding — the -`JNISwiftInstance` bound enforces this at compile time). The cast succeeds only when the +`type` must be a concrete jextracted type. +The cast succeeds only when the value's dynamic Swift type is exactly that type, in which case you receive a fresh binding registered in the given arena; otherwise the result is `Optional.empty()`. @@ -421,7 +421,6 @@ by a concrete `EnglishGreeter`: try (var arena = SwiftArena.ofConfined()) { Greeter greeter = MySwiftLibrary.makeEnglishGreeter("World", arena); - // Recover the concrete type — its `struct`-only members become available: Optional english = greeter.as(EnglishGreeter.class, arena); assertEquals("World", english.orElseThrow().getName()); @@ -430,9 +429,9 @@ try (var arena = SwiftArena.ofConfined()) { } ``` -The cast returns `Optional.empty()` — rather than throwing — whenever it cannot apply: when +The cast returns an empty optional if the cast fails, which might happen when the dynamic type differs, when `type` is not a concrete jextracted type (e.g. a generic type -or another protocol), or when the receiver is not backed by a wrapped Swift value. +or another protocol). ### Swift closures diff --git a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftDowncastable.java b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftDowncastable.java index d149a126f..ecb0b9adc 100644 --- a/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftDowncastable.java +++ b/SwiftKitCore/src/main/java/org/swift/swiftkit/core/SwiftDowncastable.java @@ -25,12 +25,12 @@ public interface SwiftDowncastable { /** - * Swift {@code as?}: attempt to recover {@code type} — a non-generic concrete - * jextracted Swift type — from this value. + * Swift {@code as?}: attempt type-cast this instance to {@code T}, + * which must be a non-generic concrete jextracted Swift wrapper type. * - * @return the recovered instance, or {@link Optional#empty()} if this value's - * dynamic type is not {@code type}, if this value is not backed by a Swift value - * (e.g. a Java-implemented conformer), or if {@code type} is not a concrete + * @return the type-casted instance, or {@link Optional#empty()} if this value's + * dynamic type is not {@code T}, if this value is not backed by a Swift value + * (e.g. a Java-implemented conformer), or if {@code T} is not a concrete * jextracted type. */ default Optional as(Class type, SwiftArena arena) {