swift icon indicating copy to clipboard operation
swift copied to clipboard

[cxx-interop] Imported class heirarchy of reference types unable to (implicitly) upcast to base

Open ADKaster opened this issue 11 months ago • 5 comments

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

ADKaster avatar Mar 22 '25 23:03 ADKaster

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 avatar Mar 25 '25 11:03 Xazax-hun

@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.

ADKaster avatar Mar 25 '25 14:03 ADKaster

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)

ADKaster avatar Mar 25 '25 19:03 ADKaster

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.

ADKaster avatar Mar 25 '25 20:03 ADKaster

Thanks a lot for the awesome minimal reproducers! I will try to look into why you need a workaround for the workaround next week.

Xazax-hun avatar Mar 26 '25 15:03 Xazax-hun

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

Xazax-hun avatar Apr 03 '25 16:04 Xazax-hun