[cxx-interop] Imported class heirarchy of reference types unable to (implicitly) upcast to base
Description
When dealing with imported hierarchy of SWIFT_UNSAFE_REFERENCE or SWIFT_IMMORTAL_REFERENCE types, pointers-to-derived cannot be (implicitly or explicitly) converted to pointer-to-base within the bounds of the type system.
Reproduction
Test.h
#pragma once
#include <swift/bridging>
class Base {
public:
virtual void doSomething() = 0;
protected:
virtual ~Base() = default;
} SWIFT_UNSAFE_REFERENCE;
class Derived : public Base {
public:
void doSomething() override {}
// stand-in for "object of this type becomes visible to swift code somehow"
static Derived* make() { return new Derived; }
static void unmake(Derived* d) { delete d; }
} SWIFT_UNSAFE_REFERENCE;
// Desired behavior: Any type derived from Base can be passed to this method from swift
void inspectBase(Base*) {}
main.swift
import MyCxx
func inspectDerived() {
let d = Derived.make()
inspectBase(d) // error: cannot convert value of type 'Derived?' to expected argument type 'Base?'
inspectBase(d!) // error: cannot convert value of type 'Derived' to expected argument type 'Base'
inspectBase(d as Base?) // error: cannot convert value of type 'Derived?' to type 'Base?' in coercion
inspectBase(d! as Base) // error: cannot convert value of type 'Derived' to type 'Base' in coercion
inspectBase(d! as! Base) // warning: cast from 'Derived' to unrelated type 'Base' always fails
Derived.unmake(d)
}
module.modulemap
module MyCxx {
header "Test.h"
requires cplusplus
export *
}
Compile with:
swiftc -I. \
-cxx-interoperability-mode=default \
-I$(swiftc -print-target-info | jq -r '.paths.runtimeResourcePath + "/../../include"')
main.swift
Expected behavior
Each of the calls to inspectBase() in main.swift should compile without warnings or errors.
Environment
Swift version 6.2-dev (LLVM 162ee50b401fff2, Swift 57288d13c9f3c02) Target: x86_64-unknown-linux-gnu Build config: +assertions
aka Swiftly main-snapshot-2025-03-14
Additional information
This pattern is used in Ladybird's garbage collector, for a visitor pattern on GC cells. GC-allocated types are expected to "visit_edges" of all member variables that are also GC-allocated. A GC::Visitor is passed to each object derived from GC::Cell, and callers are expected to call visitor.visit(m_my_member);.
I suspect this can be worked around by dodging the type system altogether.
Something like
public extension GC.Cell.Visitor {
func visit<T>(_ hopefullyACell: T) {
// convert `hopefullyACell` to OpaquePointer/UnsafeRawMutablePointer
// visit(p.assumingMemoryBound(to: GC.Cell).pointee)
// pray the caller was correct that this is a Cell, otherwise we're in trouble
}
}
https://forums.swift.org/t/ladybird-gc-and-imported-class-hierarchies/78790
This is an unfortunate limitation that we have at the moment. You can work this around by using the limited template support we have in interop.
You can add a C++ template for casts like this:
template <class I, class O> O cxxCast(I i) { return static_cast<O>(i); }
And on the Swift side you can use it like this:
inspectBase(cxxCast(d!))
This is not idiomatic and we hope to improve the situation in the future. But I was wondering if this workaround can unblock you in the meantime.
@Xazax-hun I added your example to one of my header files like so:
namespace AK {
template <class From, class To>
To cxxCast(From i) {
return static_cast<To>(i);
}
}
And the compiler is not happy. It thinks the return type is ():
Heap+Swift.swift:21:32: error: cannot convert value of type '()' to specified type 'GC.Cell'
19 | extension GC.Cell.Visitor {
20 | public func visitUnsafe<T>(_ hopefullyCell: T) {
21 | let cell: GC.Cell = AK.cxxCast(hopefullyCell, To: GC.Cell.self)
| `- error: cannot convert value of type '()' to specified type 'GC.Cell'
22 | visit(cell)
23 | }
Updated to the swiftly 2025-03-17 main snapshot
Swift version 6.2-dev (LLVM 21406e90d7382d7, Swift 90340a069a706c3)
Edit: On the example code from the issue though, it does work. Though a Base& overload is required.
template <class I, class O> O cxxCast(I i) { return static_cast<O>(i); }
// Desired behavior: Any type derived from Base can be passed to this method from swift
void inspectBase(Base*) {}
void inspectBase(Base&) {}
import MyCxx
func inspectDerived() {
let d = Derived.make()
inspectBase(cxxCast(d!))
Derived.unmake(d)
}
Moving the cast closer to the caller also does not work:
HTML/Parser/SpeculativeHTMLParser.swift:47:17: error: no exact matches in call to instance method 'visit'
45 |
46 | public func visitEdges(_ visitor: GC.Cell.Visitor) {
47 | visitor.visit(AK.cxxCast(self.parser.ptr(), To: GC.Cell.self))
| `- error: no exact matches in call to instance method 'visit'
48 | }
49 | }
/home/andrew/ladybird-org/ladybird-browser/Libraries/LibGC/Cell.h:75:14: note: candidate expects value of type 'GC.Cell' for parameter #1 (got '()')
73 | }
74 |
75 | void visit(Cell& cell)
| `- note: candidate expects value of type 'GC.Cell' for parameter #1 (got '()')
76 | {
77 | visit_impl(cell);
:
158 | }
159 |
160 | void visit(NanBoxedValue const& value);
| `- note: candidate expects value of type 'GC.NanBoxedValue' for parameter #1 (got '()')
161 |
162 | // Allow explicitly ignoring a GC-allocated member in a visit_edges implementation instead
My assumption here is that there's something funny with cross-module types here? In reality, there's 6 different modules here: AK (swift module), AKCxx (generated from AK C++ headers and module map), and the same for LibGC and LibWeb. I can put the cast into a LibGC header and it still doesn't work for either case though.
This repository better reproduces the issue with the suggested workaround https://github.com/ADKaster/swift-base-class-unsafe-unretained/
Reproduce by running ./build.sh
It might need some tweaking to play nice on macOS (remove the jq-based include?)
I get the following errors with Swift version 6.2-dev (LLVM 21406e90d7382d7, Swift 90340a069a706c3)
main.swift:7:21: error: cannot convert value of type '()' to expected argument type 'A.Base'
5 | let d = Derived.make()
6 |
7 | A.inspectBase(A.cxxCast(d!))
| `- error: cannot convert value of type '()' to expected argument type 'A.Base'
8 |
9 | A.inspectBase(A.cxxCast(d!, O: A.Base.self))
main.swift:7:21: error: generic parameter 'O' could not be inferred
5 | let d = Derived.make()
6 |
7 | A.inspectBase(A.cxxCast(d!))
| `- error: generic parameter 'O' could not be inferred
8 |
9 | A.inspectBase(A.cxxCast(d!, O: A.Base.self))
/home/andrew/ladybird-org/swift-test-apps/base-class-unsafe-unretained/./A/Base.h:18:3: note: in call to function 'cxxCast(_:O:)'
16 |
17 | template<typename I, typename O>
18 | O cxxCast(I i) { return static_cast<O>(i); }
| `- note: in call to function 'cxxCast(_:O:)'
19 |
20 | }
main.swift:7:31: error: missing argument for parameter 'O' in call
5 | let d = Derived.make()
6 |
7 | A.inspectBase(A.cxxCast(d!))
| `- error: missing argument for parameter 'O' in call
8 |
9 | A.inspectBase(A.cxxCast(d!, O: A.Base.self))
/home/andrew/ladybird-org/swift-test-apps/base-class-unsafe-unretained/./A/Base.h:18:3: note: 'cxxCast(_:O:)' declared here
16 |
17 | template<typename I, typename O>
18 | O cxxCast(I i) { return static_cast<O>(i); }
| `- note: 'cxxCast(_:O:)' declared here
19 |
20 | }
main.swift:9:21: error: cannot convert value of type '()' to expected argument type 'A.Base'
7 | A.inspectBase(A.cxxCast(d!))
8 |
9 | A.inspectBase(A.cxxCast(d!, O: A.Base.self))
| `- error: cannot convert value of type '()' to expected argument type 'A.Base'
10 |
11 | Derived.unmake(d)
Apparently the issue is that the cxxCast template was added inside a namespace? I guess I can put it in the global namespace for now as a workaround for the workaround.
Thanks a lot for the awesome minimal reproducers! I will try to look into why you need a workaround for the workaround next week.
Thanks again for the awesome repro! I have a fix pending for the workaround of the workaround at https://github.com/swiftlang/swift/pull/80500