-
Notifications
You must be signed in to change notification settings - Fork 111
Add as to cast wrapped protocols to a concrete type
#819
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
925a43a
b302c2b
c123c8a
2932ebe
724082d
795a8f4
e0181a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<EnglishGreeter> 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<DanishGreeter> 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<EnglishGreeter> 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<EnglishGreeter> english = greeter.as(EnglishGreeter.class, arena); | ||
| assertTrue(english.isPresent()); | ||
| assertEquals("World", english.get().getName()); | ||
|
|
||
| Optional<DanishGreeter> 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<EnglishGreeter> 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)); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
ktoso marked this conversation as resolved.
Outdated
|
||
| exposes an `as` method: | ||
|
|
||
| ```java | ||
| <T extends JNISwiftInstance> Optional<T> as(Class<T> type, SwiftArena arena); | ||
| <T extends JNISwiftInstance> Optional<T> as(Class<T> type); // uses the default arena | ||
| ``` | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to not have signatures copy pasted into docs |
||
|
|
||
| `type` must be a concrete jextracted type (a `class`, `struct`, or `enum` binding — the | ||
|
ktoso marked this conversation as resolved.
Outdated
|
||
| `JNISwiftInstance` bound enforces this at compile time). The cast succeeds only when the | ||
|
ktoso marked this conversation as resolved.
Outdated
|
||
| 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: | ||
|
ktoso marked this conversation as resolved.
Outdated
|
||
| Optional<EnglishGreeter> 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 | ||
|
ktoso marked this conversation as resolved.
Outdated
|
||
| 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. | ||
|
ktoso marked this conversation as resolved.
Outdated
|
||
|
|
||
| ### Swift closures | ||
|
|
||
| Non-escaping closures are called synchronously by the Swift function they are passed to, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
ktoso marked this conversation as resolved.
Outdated
|
||
| * jextracted Swift type — from this value. | ||
|
ktoso marked this conversation as resolved.
Outdated
|
||
| * | ||
| * @return the recovered instance, or {@link Optional#empty()} if this value's | ||
|
ktoso marked this conversation as resolved.
Outdated
|
||
| * dynamic type is not {@code type}, if this value is not backed by a Swift value | ||
|
ktoso marked this conversation as resolved.
Outdated
|
||
| * (e.g. a Java-implemented conformer), or if {@code type} is not a concrete | ||
|
ktoso marked this conversation as resolved.
Outdated
|
||
| * jextracted type. | ||
| */ | ||
| default <T extends JNISwiftInstance> Optional<T> as(Class<T> 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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll get warnings about this AFAIR on newer JDKs unless I'm misremembering, but that's fine for the time being... Better would be to just make the method |
||
| 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 <T extends JNISwiftInstance> Optional<T> as(Class<T> type) { | ||
| return as(type, SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.