Skip to content

Commit 8ead722

Browse files
committed
[cxx-interop] Overhaul virtual method support
This adds a new implementation of virtual method dispatch that handles reference types correctly. Previously, for all C++ types an invocation of a virtual method would actually get dispatched statically. For value types this is expected and matches what C++ does because of slicing. For reference types, however, this is incorrect, we should do dynamic dispatch. rdar://123852577
1 parent 638abd9 commit 8ead722

10 files changed

+214
-5
lines changed

lib/ClangImporter/ImportDecl.cpp

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3753,10 +3753,43 @@ namespace {
37533753
}
37543754
}
37553755

3756-
if (decl->isVirtual() && isa_and_nonnull<ValueDecl>(method)) {
3757-
Impl.markUnavailable(
3758-
cast<ValueDecl>(method),
3759-
"virtual functions are not yet available in Swift");
3756+
if (decl->isVirtual()) {
3757+
if (auto funcDecl = dyn_cast_or_null<FuncDecl>(method)) {
3758+
if (Impl.isCxxInteropCompatVersionAtLeast(6)) {
3759+
if (auto structDecl =
3760+
dyn_cast_or_null<StructDecl>(method->getDeclContext())) {
3761+
// If this is a method of a Swift struct, any possible override of
3762+
// this method would get sliced away, and an invocation would get
3763+
// dispatched statically. This is fine because it matches the C++
3764+
// behavior.
3765+
if (decl->isPure()) {
3766+
// If this is a pure virtual method, we won't have any
3767+
// implementation of it to invoke.
3768+
Impl.markUnavailable(
3769+
funcDecl, "virtual function is not available in Swift "
3770+
"because it is pure");
3771+
}
3772+
} else if (auto classDecl = dyn_cast_or_null<ClassDecl>(
3773+
funcDecl->getDeclContext())) {
3774+
// This is a foreign reference type. Since `class T` on the Swift
3775+
// side is mapped from `T*` on the C++ side, an invocation of a
3776+
// virtual method `t->method()` should get dispatched dynamically.
3777+
// Create a thunk that will perform dynamic dispatch.
3778+
// TODO: we don't have to import the actual `method` in this case,
3779+
// we can just synthesize a thunk and import that instead.
3780+
auto result = synthesizer.makeVirtualMethod(decl);
3781+
if (result) {
3782+
return result;
3783+
} else {
3784+
Impl.markUnavailable(
3785+
funcDecl, "virtual function is not available in Swift");
3786+
}
3787+
}
3788+
} else {
3789+
Impl.markUnavailable(
3790+
funcDecl, "virtual functions are not yet available in Swift");
3791+
}
3792+
}
37603793
}
37613794

37623795
if (Impl.SwiftContext.LangOpts.CxxInteropGettersSettersAsProperties ||

lib/ClangImporter/ImportName.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2258,6 +2258,14 @@ ImportedName NameImporter::importNameImpl(const clang::NamedDecl *D,
22582258
baseName = newName;
22592259
}
22602260
}
2261+
if (method->isImplicit() &&
2262+
baseName.starts_with("__synthesizedVirtualCall_")) {
2263+
// If this is a thunk for a virtual method of a C++ reference type, we
2264+
// strip away the underscored prefix. This method should be visible and
2265+
// callable from Swift.
2266+
newName = baseName.substr(StringRef("__synthesizedVirtualCall_").size());
2267+
baseName = newName;
2268+
}
22612269
}
22622270

22632271
// swift_newtype-ed declarations may have common words with the type name

lib/ClangImporter/SwiftDeclSynthesizer.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2235,6 +2235,26 @@ SwiftDeclSynthesizer::makeOperator(FuncDecl *operatorMethod,
22352235
return topLevelStaticFuncDecl;
22362236
}
22372237

2238+
// MARK: C++ virtual methods
2239+
2240+
FuncDecl *SwiftDeclSynthesizer::makeVirtualMethod(
2241+
const clang::CXXMethodDecl *clangMethodDecl) {
2242+
auto clangDC = clangMethodDecl->getParent();
2243+
auto &ctx = ImporterImpl.SwiftContext;
2244+
2245+
assert(!clangMethodDecl->isStatic() &&
2246+
"C++ virtual functions cannot be static");
2247+
2248+
auto newMethod = synthesizeCXXForwardingMethod(
2249+
clangDC, clangDC, clangMethodDecl, ForwardingMethodKind::Virtual,
2250+
ReferenceReturnTypeBehaviorForBaseMethodSynthesis::KeepReference,
2251+
/*forceConstQualifier*/ false);
2252+
2253+
auto result = dyn_cast_or_null<FuncDecl>(
2254+
ctx.getClangModuleLoader()->importDeclDirectly(newMethod));
2255+
return result;
2256+
}
2257+
22382258
// MARK: C++ properties
22392259

22402260
static std::pair<BraceStmt *, bool>

lib/ClangImporter/SwiftDeclSynthesizer.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,10 @@ class SwiftDeclSynthesizer {
315315
ReferenceReturnTypeBehaviorForBaseMethodSynthesis::KeepReference,
316316
bool forceConstQualifier = false);
317317

318+
/// Given an overload of a C++ virtual method on a reference type, create a
319+
/// method that dispatches the call dynamically.
320+
FuncDecl *makeVirtualMethod(const clang::CXXMethodDecl *clangMethodDecl);
321+
318322
VarDecl *makeComputedPropertyFromCXXMethods(FuncDecl *getter,
319323
FuncDecl *setter);
320324

test/Interop/Cxx/class/Inputs/protocol-conformance.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,8 @@ struct HasOperatorPlusEqual {
6868

6969
using HasOperatorPlusEqualInt = HasOperatorPlusEqual<int>;
7070

71+
struct HasVirtualMethod {
72+
virtual int return42() { return 42; }
73+
};
74+
7175
#endif // TEST_INTEROP_CXX_CLASS_INPUTS_PROTOCOL_CONFORMANCE_H

test/Interop/Cxx/class/inheritance/Inputs/virtual-methods.h

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
extern "C" void puts(const char *);
1+
extern "C" void puts(const char *_Null_unspecified);
22

33
inline void testFunctionCollected() {
44
puts("test\n");
@@ -38,3 +38,50 @@ using UnusedInt = Unused<int>;
3838
struct VirtualNonAbstractBase {
3939
virtual void nonAbstractMethod() const;
4040
};
41+
42+
struct CallsPureMethod {
43+
virtual int getPureInt() const = 0;
44+
int getInt() const { return getPureInt() + 1; }
45+
};
46+
47+
struct DerivedFromCallsPureMethod : CallsPureMethod {
48+
int getPureInt() const override { return 789; }
49+
};
50+
51+
struct DerivedFromDerivedFromCallsPureMethod : DerivedFromCallsPureMethod {};
52+
53+
// MARK: Reference Types:
54+
55+
#define IMMORTAL_FRT \
56+
__attribute__((swift_attr("import_reference"))) \
57+
__attribute__((swift_attr("retain:immortal"))) \
58+
__attribute__((swift_attr("release:immortal")))
59+
60+
struct IMMORTAL_FRT ImmortalBase {
61+
int value = 0;
62+
63+
virtual int get42() const { return 42; }
64+
virtual int getOverridden42() const { return 123; }
65+
virtual int getIntValue() const { return value; }
66+
};
67+
68+
struct IMMORTAL_FRT Immortal : public ImmortalBase {
69+
static Immortal *_Nonnull create() { return new Immortal(); }
70+
71+
virtual int getOverridden42() const override { return 42; }
72+
virtual void setIntValue(int newValue) { this->value = newValue; }
73+
};
74+
75+
struct IMMORTAL_FRT DerivedFromImmortal : public Immortal {
76+
static DerivedFromImmortal *_Nonnull create() { return new DerivedFromImmortal(); }
77+
};
78+
79+
inline const ImmortalBase *_Nonnull castToImmortalBase(
80+
const Immortal *_Nonnull immortal) {
81+
return static_cast<const ImmortalBase *>(immortal);
82+
}
83+
84+
inline const Immortal *_Nonnull castToImmortal(
85+
const DerivedFromImmortal *_Nonnull immortal) {
86+
return static_cast<const Immortal *>(immortal);
87+
}

test/Interop/Cxx/class/inheritance/virtual-methods-module-interface.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@
2424
// CHECK: struct VirtualNonAbstractBase {
2525
// CHECK-NEXT: init()
2626
// CHECK-NEXT: func nonAbstractMethod()
27+
28+
// CHECK: class ImmortalBase {
29+
// CHECK: func get42() -> Int32
30+
// CHECK: func getOverridden42() -> Int32
31+
// CHECK: }
32+
// CHECK: class Immortal {
33+
// CHECK: func getOverridden42() -> Int32
34+
// CHECK: func get42() -> Int32
35+
// CHECK: }

test/Interop/Cxx/class/inheritance/virtual-methods-typechecker.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,26 @@ let _ = Derived4()
1313
let _ = DerivedFromDerived2()
1414

1515
VirtualNonAbstractBase().nonAbstractMethod()
16+
17+
@available(macOS 13.3, *)
18+
func takesImmortalBase(_ i: ImmortalBase) {
19+
let _ = i.get42()
20+
let _ = i.getOverridden42()
21+
let _ = i.getIntValue()
22+
}
23+
24+
@available(macOS 13.3, *)
25+
func takesImmortal(_ i: Immortal) {
26+
let _ = i.get42()
27+
let _ = i.getOverridden42()
28+
let _ = i.getIntValue()
29+
i.setIntValue(1)
30+
}
31+
32+
@available(macOS 13.3, *)
33+
func takesDerivedFromImmortal(_ i: DerivedFromImmortal) {
34+
let _ = i.get42()
35+
let _ = i.getOverridden42()
36+
let _ = i.getIntValue()
37+
i.setIntValue(1)
38+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// RUN: %target-run-simple-swift(-I %S/Inputs -cxx-interoperability-mode=swift-6)
2+
// RUN: %target-run-simple-swift(-I %S/Inputs -cxx-interoperability-mode=upcoming-swift)
3+
4+
// REQUIRES: executable_test
5+
6+
import StdlibUnittest
7+
import VirtualMethods
8+
9+
var VirtualMethodsTestSuite = TestSuite("Virtual Methods")
10+
11+
VirtualMethodsTestSuite.test("value type") {
12+
var d2 = Derived2()
13+
expectEqual(42, d2.f())
14+
15+
var d3 = Derived3()
16+
expectEqual(42, d3.f())
17+
18+
var d4 = Derived4()
19+
expectEqual(24, d4.f())
20+
21+
let d5 = DerivedFromCallsPureMethod()
22+
expectEqual(790, d5.getInt())
23+
expectEqual(789, d5.getPureInt())
24+
25+
let d6 = DerivedFromDerivedFromCallsPureMethod()
26+
expectEqual(790, d6.getInt())
27+
expectEqual(789, d6.getPureInt())
28+
}
29+
30+
if #available(macOS 13.3, *) {
31+
VirtualMethodsTestSuite.test("immortal reference type") {
32+
let i = Immortal.create()
33+
expectEqual(42, i.get42())
34+
expectEqual(0, i.getIntValue())
35+
36+
let base = castToImmortalBase(i)
37+
expectEqual(42, base.get42())
38+
expectEqual(42, base.getOverridden42())
39+
expectEqual(0, base.getIntValue())
40+
41+
i.setIntValue(566)
42+
expectEqual(566, i.getIntValue())
43+
expectEqual(566, base.getIntValue())
44+
45+
let d = DerivedFromImmortal.create()
46+
expectEqual(42, d.get42())
47+
expectEqual(42, d.getOverridden42())
48+
d.setIntValue(321)
49+
expectEqual(321, d.getIntValue())
50+
let base2 = castToImmortalBase(castToImmortal(d))
51+
expectEqual(321, base2.getIntValue())
52+
}
53+
}
54+
55+
runAllTests()

test/Interop/Cxx/class/protocol-conformance-typechecker.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Tests that a C++ class can conform to a Swift protocol.
22

33
// RUN: %target-typecheck-verify-swift -I %S/Inputs -enable-experimental-cxx-interop
4+
// RUN: %target-typecheck-verify-swift -I %S/Inputs -D VIRTUAL_METHODS -cxx-interoperability-mode=swift-6
5+
// RUN: %target-typecheck-verify-swift -I %S/Inputs -D VIRTUAL_METHODS -cxx-interoperability-mode=upcoming-swift
46

57
import ProtocolConformance
68

@@ -10,6 +12,10 @@ protocol HasReturn42 {
1012

1113
extension ConformsToProtocol : HasReturn42 {}
1214

15+
#if VIRTUAL_METHODS
16+
extension HasVirtualMethod : HasReturn42 {}
17+
#endif
18+
1319
extension DoesNotConformToProtocol : HasReturn42 {} // expected-error {{'DoesNotConformToProtocol' does not conform to protocol}}
1420

1521

0 commit comments

Comments
 (0)