Skip to content

[cxx-interop] Add ability to specify protocol conformance on C++ side. #62330

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

Merged
merged 3 commits into from
Dec 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions include/swift/AST/Attr.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,7 @@ class DeclAttribute : public AttributeBase {
kind : 1
);

SWIFT_INLINE_BITFIELD(SynthesizedProtocolAttr, DeclAttribute,
NumKnownProtocolKindBits+1,
kind : NumKnownProtocolKindBits,
SWIFT_INLINE_BITFIELD(SynthesizedProtocolAttr, DeclAttribute, 1,
isUnchecked : 1
);

Expand Down Expand Up @@ -1364,22 +1362,22 @@ class ObjCBridgedAttr : public DeclAttribute {
/// synthesized conformances.
class SynthesizedProtocolAttr : public DeclAttribute {
LazyConformanceLoader *Loader;
ProtocolDecl *protocol;

public:
SynthesizedProtocolAttr(KnownProtocolKind protocolKind,
SynthesizedProtocolAttr(ProtocolDecl *protocol,
LazyConformanceLoader *Loader,
bool isUnchecked)
: DeclAttribute(DAK_SynthesizedProtocol, SourceLoc(), SourceRange(),
/*Implicit=*/true), Loader(Loader)
/*Implicit=*/true), Loader(Loader), protocol(protocol)
{
Bits.SynthesizedProtocolAttr.kind = unsigned(protocolKind);
Bits.SynthesizedProtocolAttr.isUnchecked = unsigned(isUnchecked);
}

/// Retrieve the known protocol kind naming the protocol to be
/// synthesized.
KnownProtocolKind getProtocolKind() const {
return KnownProtocolKind(Bits.SynthesizedProtocolAttr.kind);
ProtocolDecl *getProtocol() const {
return protocol;
}

bool isUnchecked() const {
Expand Down
10 changes: 10 additions & 0 deletions include/swift/AST/DiagnosticsClangImporter.def
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ ERROR(foreign_reference_types_invalid_release,none,
"specified release function '%0' is invalid. Release must have exactly "
"one argument of type '%1'", (StringRef, StringRef))

ERROR(cannot_find_conforms_to,none,
"specified protocol conformance could not be found '%0'.", (StringRef))

ERROR(conforms_to_ambiguous,none,
"specified protocol conformance is ambiguous. "
"Found multiple protocols named '%0'.", (StringRef))

ERROR(conforms_to_not_protocol,none,
"specified protocol conformance '%0' is not a protocol.", (StringRef))

NOTE(unsupported_builtin_type, none, "built-in type '%0' not supported", (StringRef))
NOTE(record_field_not_imported, none, "field %0 not imported", (const clang::NamedDecl*))
NOTE(invoked_func_not_imported, none, "function %0 not imported", (const clang::NamedDecl*))
Expand Down
5 changes: 3 additions & 2 deletions lib/AST/ASTPrinter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7069,13 +7069,14 @@ swift::getInheritedForPrinting(
llvm::SetVector<ProtocolDecl *> protocols;
llvm::TinyPtrVector<ProtocolDecl *> uncheckedProtocols;
for (auto attr : decl->getAttrs().getAttributes<SynthesizedProtocolAttr>()) {
if (auto *proto = ctx.getProtocol(attr->getProtocolKind())) {
if (auto *proto = attr->getProtocol()) {
// The SerialExecutor conformance is only synthesized on the root
// actor class, so we can just test resilience immediately.
if (proto->isSpecificProtocol(KnownProtocolKind::SerialExecutor) &&
cast<ClassDecl>(decl)->isResilient())
continue;
if (attr->getProtocolKind() == KnownProtocolKind::RawRepresentable &&
if (proto->getKnownProtocolKind() &&
*proto->getKnownProtocolKind() == KnownProtocolKind::RawRepresentable &&
isa<EnumDecl>(decl) &&
cast<EnumDecl>(decl)->hasRawType())
continue;
Expand Down
2 changes: 1 addition & 1 deletion lib/AST/ConformanceLookupTable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,7 @@ ConformanceLookupTable::getConformance(NominalTypeDecl *nominal,
// Find a SynthesizedProtocolAttr corresponding to the protocol.
for (auto attr : conformingNominal->getAttrs()
.getAttributes<SynthesizedProtocolAttr>()) {
auto otherProto = ctx.getProtocol(attr->getProtocolKind());
auto otherProto = attr->getProtocol();
if (otherProto == impliedProto) {
// Set the conformance loader to the loader stashed inside
// the attribute.
Expand Down
29 changes: 15 additions & 14 deletions lib/AST/ProtocolConformance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1052,49 +1052,50 @@ void NominalTypeDecl::prepareConformanceTable() const {

SmallPtrSet<ProtocolDecl *, 2> protocols;

auto addSynthesized = [&](KnownProtocolKind kind) {
if (auto *proto = getASTContext().getProtocol(kind)) {
if (protocols.count(proto) == 0) {
ConformanceTable->addSynthesizedConformance(
mutableThis, proto, mutableThis);
protocols.insert(proto);
}
auto addSynthesized = [&](ProtocolDecl *proto) {
if (!proto)
return;

if (protocols.count(proto) == 0) {
ConformanceTable->addSynthesizedConformance(
mutableThis, proto, mutableThis);
protocols.insert(proto);
}
};

// Add protocols for any synthesized protocol attributes.
for (auto attr : getAttrs().getAttributes<SynthesizedProtocolAttr>()) {
addSynthesized(attr->getProtocolKind());
addSynthesized(attr->getProtocol());
}

// Add any implicit conformances.
if (auto theEnum = dyn_cast<EnumDecl>(mutableThis)) {
if (theEnum->hasCases() && theEnum->hasOnlyCasesWithoutAssociatedValues()) {
// Simple enumerations conform to Equatable.
addSynthesized(KnownProtocolKind::Equatable);
addSynthesized(ctx.getProtocol(KnownProtocolKind::Equatable));

// Simple enumerations conform to Hashable.
addSynthesized(KnownProtocolKind::Hashable);
addSynthesized(ctx.getProtocol(KnownProtocolKind::Hashable));
}

// Enumerations with a raw type conform to RawRepresentable.
if (theEnum->hasRawType() && !theEnum->getRawType()->hasError()) {
addSynthesized(KnownProtocolKind::RawRepresentable);
addSynthesized(ctx.getProtocol(KnownProtocolKind::RawRepresentable));
}
}

// Actor classes conform to the actor protocol.
if (auto classDecl = dyn_cast<ClassDecl>(mutableThis)) {
if (classDecl->isDistributedActor()) {
addSynthesized(KnownProtocolKind::DistributedActor);
addSynthesized(ctx.getProtocol(KnownProtocolKind::DistributedActor));
} else if (classDecl->isActor()) {
addSynthesized(KnownProtocolKind::Actor);
addSynthesized(ctx.getProtocol(KnownProtocolKind::Actor));
}
}

// Global actors conform to the GlobalActor protocol.
if (mutableThis->getAttrs().hasAttribute<GlobalActorAttr>()) {
addSynthesized(KnownProtocolKind::GlobalActor);
addSynthesized(ctx.getProtocol(KnownProtocolKind::GlobalActor));
}
}

Expand Down
77 changes: 66 additions & 11 deletions lib/ClangImporter/ImportDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,13 @@ void ClangImporter::Implementation::addSynthesizedProtocolAttrs(
auto &ctx = nominal->getASTContext();

for (auto kind : synthesizedProtocolAttrs) {
nominal->getAttrs().add(
new (ctx) SynthesizedProtocolAttr(kind, this, isUnchecked));
// This is unfortunately not an error because some test use mock protocols.
// If those tests were updated, we could assert that
// ctx.getProtocol(kind) != nulltpr which would be nice.
if (auto proto = ctx.getProtocol(kind))
nominal->getAttrs().add(
new (ctx) SynthesizedProtocolAttr(ctx.getProtocol(kind), this,
isUnchecked));
}
}

Expand Down Expand Up @@ -2611,23 +2616,72 @@ namespace {
}

auto result = VisitRecordDecl(decl);
if (!result)
return nullptr;

if (auto classDecl = dyn_cast_or_null<ClassDecl>(result))
if (auto classDecl = dyn_cast<ClassDecl>(result))
validateForeignReferenceType(decl, classDecl);

// If this module is declared as a C++ module, try to synthesize
// conformances to Swift protocols from the Cxx module.
auto clangModule = decl->getOwningModule();
if (clangModule && requiresCPlusPlus(clangModule)) {
if (auto structDecl = dyn_cast_or_null<NominalTypeDecl>(result)) {
conformToCxxIteratorIfNeeded(Impl, structDecl, decl);
conformToCxxSequenceIfNeeded(Impl, structDecl, decl);
}
auto nominalDecl = cast<NominalTypeDecl>(result);
conformToCxxIteratorIfNeeded(Impl, nominalDecl, decl);
conformToCxxSequenceIfNeeded(Impl, nominalDecl, decl);
}

addExplicitProtocolConformances(cast<NominalTypeDecl>(result));

return result;
}

void addExplicitProtocolConformances(NominalTypeDecl *decl) {
auto clangDecl = decl->getClangDecl();

if (!clangDecl->hasAttrs())
return;

SmallVector<ValueDecl *, 1> results;
auto conformsToAttr =
llvm::find_if(clangDecl->getAttrs(), [](auto *attr) {
if (auto swiftAttr = dyn_cast<clang::SwiftAttrAttr>(attr))
return swiftAttr->getAttribute().startswith("conforms_to:");
return false;
});
if (conformsToAttr == clangDecl->getAttrs().end())
return;

auto name = cast<clang::SwiftAttrAttr>(*conformsToAttr)
->getAttribute()
.drop_front(StringRef("conforms_to:").size())
.str();

for (auto &module : Impl.SwiftContext.getLoadedModules()) {
module.second->lookupValue(Impl.SwiftContext.getIdentifier(name),
NLKind::UnqualifiedLookup, results);
}

if (results.empty()) {
HeaderLoc attrLoc((*conformsToAttr)->getLocation());
Impl.diagnose(attrLoc, diag::cannot_find_conforms_to, name);
return;
} else if (results.size() != 1) {
HeaderLoc attrLoc((*conformsToAttr)->getLocation());
Impl.diagnose(attrLoc, diag::conforms_to_ambiguous, name);
return;
}

auto result = results.front();
if (auto protocol = dyn_cast<ProtocolDecl>(result)) {
decl->getAttrs().add(
new (Impl.SwiftContext) SynthesizedProtocolAttr(protocol, &Impl, false));
} else {
HeaderLoc attrLoc((*conformsToAttr)->getLocation());
Impl.diagnose(attrLoc, diag::conforms_to_not_protocol, name);
}
}

bool isSpecializationDepthGreaterThan(
const clang::ClassTemplateSpecializationDecl *decl, unsigned maxDepth) {
for (auto arg : decl->getTemplateArgs().asArray()) {
Expand Down Expand Up @@ -5126,10 +5180,11 @@ static bool conformsToProtocolInOriginalModule(NominalTypeDecl *nominal,
if (inheritanceListContainsProtocol(nominal, proto))
return true;

for (auto attr : nominal->getAttrs().getAttributes<SynthesizedProtocolAttr>())
if (auto *otherProto = ctx.getProtocol(attr->getProtocolKind()))
if (otherProto == proto || otherProto->inheritsFrom(proto))
return true;
for (auto attr : nominal->getAttrs().getAttributes<SynthesizedProtocolAttr>()) {
auto *otherProto = attr->getProtocol();
if (otherProto == proto || otherProto->inheritsFrom(proto))
return true;
}

// Only consider extensions from the original module...or from an overlay
// or the Swift half of a mixed-source framework.
Expand Down
20 changes: 20 additions & 0 deletions test/Interop/Cxx/class/Inputs/conforms-to.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#ifndef TEST_INTEROP_CXX_CLASS_INPUTS_DESTRUCTORS_H
#define TEST_INTEROP_CXX_CLASS_INPUTS_DESTRUCTORS_H

struct
__attribute__((swift_attr("conforms_to:Testable")))
HasTest {
void test() const;
};

struct
__attribute__((swift_attr("conforms_to:Playable")))
__attribute__((swift_attr("import_reference")))
__attribute__((swift_attr("retain:immortal")))
__attribute__((swift_attr("release:immortal")))
HasPlay {
void play() const;
};


#endif // TEST_INTEROP_CXX_CLASS_INPUTS_DESTRUCTORS_H
5 changes: 5 additions & 0 deletions test/Interop/Cxx/class/Inputs/module.modulemap
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,8 @@ module ForwardDeclaredInNamespace {
header "forward-declared-in-namespace.h"
requires cplusplus
}

module ConformsTo {
header "conforms-to.h"
requires cplusplus
}
30 changes: 30 additions & 0 deletions test/Interop/Cxx/class/conforms-to-errors.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// RUN: rm -rf %t
// RUN: split-file %s %t
// RUN: not %target-swift-frontend -typecheck -I %t/Inputs %t/test.swift -enable-experimental-cxx-interop 2>&1 | %FileCheck %s

//--- Inputs/module.modulemap
module Test {
header "test.h"
requires cplusplus
}

//--- Inputs/test.h

struct __attribute__((swift_attr("conforms_to:X"))) CX {};
struct __attribute__((swift_attr("conforms_to:A"))) CA {};
struct __attribute__((swift_attr("conforms_to:B"))) CB {};

//--- test.swift

import Test

struct B {}

protocol A {}
protocol A {}

// CHECK: error: specified protocol conformance could not be found 'X'.
// CHECK: error: specified protocol conformance is ambiguous. Found multiple protocols named 'A'.
// CHECK: error: specified protocol conformance 'B' is not a protocol.

func test(_ x: CX, _ a: CA, _ b: CB) {}
27 changes: 27 additions & 0 deletions test/Interop/Cxx/class/conforms-to.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// RUN: %target-typecheck-verify-swift -verify-ignore-unknown -I %S/Inputs -enable-experimental-cxx-interop

import ConformsTo

protocol Testable {
func test()
}

protocol Playable {
func play()
}

func callee(_ _: Testable) {

}

func caller(_ x: HasTest) {
callee(x)
}

func callee(_ _: Playable) {

}

func caller(_ x: Playable) {
callee(x)
}