Skip to content

Commit 8dbe399

Browse files
committed
Add basic Sema support for @_objcImplementation
Does not validate members yet; nor does it emit different metadata.
1 parent 83e96c0 commit 8dbe399

File tree

17 files changed

+454
-2
lines changed

17 files changed

+454
-2
lines changed

docs/ReferenceGuides/UnderscoredAttributes.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,73 @@ be added to a given type, while `@_nonSendable` indicates that an unavailable
676676
`(_assumed)` after it, in which case `@Sendable` "beats" it.
677677
`@_nonSendable(_assumed)` is intended to be used when mass-marking whole regions
678678
of a header as non-`Sendable` so that you can make spot exceptions with
679-
`@Sendable`.
679+
`@Sendable`.
680+
681+
## `@_objcImplementation(CategoryName)`
682+
683+
Declares an extension that defines an implementation for the Objective-C
684+
category `CategoryName` on the class in question, or for the main `@interface`
685+
if the argument list is omitted.
686+
687+
This attribute is used to write fully Objective-C-compatible implementations in
688+
Swift. Normal Objective-C interop allows Objective-C clients to use instances of
689+
the subclass, but not to subclass them, and uses a generated header that is not
690+
meant to be read by humans. `@_objcImplementation`, on the other hand, creates
691+
classes that are virtually indistinguishable from classes implemented in native
692+
Objective-C: they do not have a Swift vtable or any other Swift-specific
693+
metadata, Swift does not use any special knowledge of the class's "Swiftiness"
694+
when using the class so ObjC runtime calls work correctly and they can even be
695+
subclassed by Objective-C code, and you write a header for the class by hand
696+
that looks exactly like an equivalent ObjC class. Clients should not notice if
697+
you replace a native Objective-C `@implementation Foo (Bar)` with a Swift
698+
`@_objcImplementation(Bar) extension Foo`.
699+
700+
You create a class with this feature very differently from normal ObjC interop:
701+
702+
1. Hand-write headers that declare the classes' Objective-C interface, just as
703+
you would for a native Objective-C class. Since you're handwriting these
704+
headers, you can write them just as you would for an Objective-C class:
705+
splitting them across multiple files, grouping related declarations together,
706+
adding comments, declaring Swift behavior using C attributes or API notes,
707+
etc.
708+
709+
2. Import your headers into Swift using a bridging header or umbrella header so
710+
Swift can see them.
711+
712+
3. Implement your class using a mixture of `@implementation` declarations in
713+
`.m` files and `@_objcImplementation extension`s in `.swift` files. Each
714+
`@interface` should have exactly one corresponding implementation; don't try
715+
to implement some members of a single `@interface` in ObjC and others in
716+
Swift.
717+
718+
* To implement the main `@interface` of a class in Swift, use
719+
`@_objcImplementation extension ClassName`.
720+
721+
* To implement a category in Swift, use
722+
`@_objcImplementation(CategoryName) extension ClassName`.
723+
724+
* We should think about Objective-C generics.
725+
726+
4. Every member of an `@_objcImplementation` extension should be either `@objc`
727+
or `final`. The `final` members can be made `public` if you want to expose
728+
them to Swift clients. The `@objc` members are implicitly `dynamic`.
729+
730+
* Eventually, we'd like to allow members which are not explicitly marked
731+
`@objc`. The compiler will check that they match something declared in the
732+
header, and if they do, it will automatically apply appropriate attributes.
733+
If not, it will reject them. Members with an explicit `@objc` will not be
734+
checked, so you can define helpers that are callable by selector but not in
735+
any header.
736+
737+
* Eventually, we want the main `@_objcImplementation` extension to be able to
738+
declare stored properties that aren't in the interface. We also want
739+
`final` stored properties to be allowed to be resilent Swift types, but
740+
it's not clear how to achieve that without boxing them in `__SwiftValue`
741+
(which we might do as a stopgap).
742+
743+
* We should think about ObjC `direct` members.
744+
745+
* Access control design for ObjC methods TBD.
680746

681747
## `@_objc_non_lazy_realization`
682748

include/swift/AST/Attr.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ class DeclAttribute : public AttributeBase {
172172
kind : NumKnownProtocolKindBits,
173173
isUnchecked : 1
174174
);
175+
176+
SWIFT_INLINE_BITFIELD(ObjCImplementationAttr, DeclAttribute, 1,
177+
isCategoryNameInvalid : 1
178+
);
175179
} Bits;
176180

177181
DeclAttribute *Next = nullptr;
@@ -2289,6 +2293,31 @@ class DocumentationAttr: public DeclAttribute {
22892293
}
22902294
};
22912295

2296+
class ObjCImplementationAttr final : public DeclAttribute {
2297+
public:
2298+
Identifier CategoryName;
2299+
2300+
ObjCImplementationAttr(Identifier CategoryName, SourceLoc AtLoc,
2301+
SourceRange Range, bool Implicit = false,
2302+
bool isCategoryNameInvalid = false)
2303+
: DeclAttribute(DAK_ObjCImplementation, AtLoc, Range, Implicit),
2304+
CategoryName(CategoryName) {
2305+
Bits.ObjCImplementationAttr.isCategoryNameInvalid = isCategoryNameInvalid;
2306+
}
2307+
2308+
bool isCategoryNameInvalid() const {
2309+
return Bits.ObjCImplementationAttr.isCategoryNameInvalid;
2310+
}
2311+
2312+
void setCategoryNameInvalid(bool newValue = true) {
2313+
Bits.ObjCImplementationAttr.isCategoryNameInvalid = newValue;
2314+
}
2315+
2316+
static bool classof(const DeclAttribute *DA) {
2317+
return DA->getKind() == DAK_ObjCImplementation;
2318+
}
2319+
};
2320+
22922321
/// Attributes that may be applied to declarations.
22932322
class DeclAttributes {
22942323
/// Linked list of declaration attributes.

include/swift/AST/Decl.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1496,6 +1496,18 @@ class ExtensionDecl final : public GenericContext, public Decl,
14961496
/// resiliently moved into the original protocol itself.
14971497
bool isEquivalentToExtendedContext() const;
14981498

1499+
/// True if this extension provides an implementation for an imported
1500+
/// Objective-C \c \@interface. This implies various restrictions and special
1501+
/// behaviors for its members.
1502+
bool isObjCImplementation() const;
1503+
1504+
/// Returns the \c clang::ObjCCategoryDecl or \c clang::ObjCInterfaceDecl
1505+
/// implemented by this extension.
1506+
///
1507+
/// This can return \c nullptr if the category doesn't exist. Do not call it
1508+
/// unless \c isObjCImplementation() returns \c true.
1509+
const clang::ObjCContainerDecl *getInterfaceForObjCImplementation() const;
1510+
14991511
// Implement isa/cast/dyncast/etc.
15001512
static bool classof(const Decl *D) {
15011513
return D->getKind() == DeclKind::Extension;
@@ -4359,6 +4371,12 @@ class ClassDecl final : public NominalTypeDecl {
43594371
/// the Objective-C runtime.
43604372
StringRef getObjCRuntimeName(llvm::SmallVectorImpl<char> &buffer) const;
43614373

4374+
/// Return the imported declaration for the category with the given name; this
4375+
/// will always be an Objective-C-backed \c ExtensionDecl or, if \p name is
4376+
/// empty, \c ClassDecl. Returns \c nullptr if the class was not imported from
4377+
/// Objective-C or does not have an imported category by that name.
4378+
Decl *getImportedObjCCategory(Identifier name) const;
4379+
43624380
// Implement isa/cast/dyncast/etc.
43634381
static bool classof(const Decl *D) {
43644382
return D->getKind() == DeclKind::Class;

include/swift/AST/DiagnosticsSema.def

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,6 +1492,26 @@ ERROR(swift_native_objc_runtime_base_not_on_root_class,none,
14921492
"@_swift_native_objc_runtime_base_not_on_root_class can only be applied "
14931493
"to root classes", ())
14941494

1495+
ERROR(attr_objc_implementation_must_be_unconditional,none,
1496+
"only unconditional extensions can implement an Objective-C '@interface'",
1497+
())
1498+
ERROR(attr_objc_implementation_must_extend_class,none,
1499+
"cannot mark extension of %0 %1 with '@_objcImplementation'; it is not "
1500+
"an imported Objective-C class",
1501+
(DescriptiveDeclKind, ValueDecl *))
1502+
ERROR(attr_objc_implementation_must_be_imported,none,
1503+
"'@_objcImplementation' cannot be used to extend %0 %1 because it was "
1504+
"defined by a Swift 'class' declaration, not an imported Objective-C "
1505+
"'@interface' declaration",
1506+
(DescriptiveDeclKind, ValueDecl *))
1507+
ERROR(attr_objc_implementation_category_not_found,none,
1508+
"could not find category %0 on Objective-C class %1; make sure your "
1509+
"umbrella or bridging header imports the header that declares it",
1510+
(Identifier, ValueDecl*))
1511+
NOTE(attr_objc_implementation_fixit_remove_category_name,none,
1512+
"remove arguments to implement the main '@interface' for this class",
1513+
())
1514+
14951515
ERROR(cdecl_not_at_top_level,none,
14961516
"@_cdecl can only be applied to global functions", ())
14971517
ERROR(cdecl_empty_name,none,

include/swift/ClangImporter/ClangImporterRequests.h

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,58 @@ class ClangRecordMemberLookup
178178
evaluate(Evaluator &evaluator, ClangRecordMemberLookupDescriptor desc) const;
179179
};
180180

181+
/// The input type for a clang category lookup request.
182+
struct ClangCategoryLookupDescriptor final {
183+
const ClassDecl *classDecl;
184+
Identifier categoryName;
185+
186+
ClangCategoryLookupDescriptor(const ClassDecl *classDecl,
187+
Identifier categoryName)
188+
: classDecl(classDecl), categoryName(categoryName) {}
189+
190+
friend llvm::hash_code hash_value(const ClangCategoryLookupDescriptor &desc) {
191+
return llvm::hash_combine(desc.classDecl, desc.categoryName);
192+
}
193+
194+
friend bool operator==(const ClangCategoryLookupDescriptor &lhs,
195+
const ClangCategoryLookupDescriptor &rhs) {
196+
return lhs.classDecl == rhs.classDecl
197+
&& lhs.categoryName == rhs.categoryName;
198+
}
199+
200+
friend bool operator!=(const ClangCategoryLookupDescriptor &lhs,
201+
const ClangCategoryLookupDescriptor &rhs) {
202+
return !(lhs == rhs);
203+
}
204+
};
205+
206+
void simple_display(llvm::raw_ostream &out,
207+
const ClangCategoryLookupDescriptor &desc);
208+
SourceLoc extractNearestSourceLoc(const ClangCategoryLookupDescriptor &desc);
209+
210+
/// Given a Swift class, find the imported Swift decl representing the
211+
/// \c \@interface with the given category name. That is, this will return an
212+
/// \c swift::ExtensionDecl backed by a \c clang::ObjCCategoryDecl, or a
213+
/// \c swift::ClassDecl backed by a \c clang::ObjCInterfaceDecl, or \c nullptr
214+
/// if the class is not imported from Clang or it does not have a category by
215+
/// that name.
216+
///
217+
/// An empty/invalid \c categoryName requests the main interface for the class.
218+
class ClangCategoryLookupRequest
219+
: public SimpleRequest<ClangCategoryLookupRequest,
220+
Decl *(ClangCategoryLookupDescriptor),
221+
RequestFlags::Uncached> {
222+
public:
223+
using SimpleRequest::SimpleRequest;
224+
225+
private:
226+
friend SimpleRequest;
227+
228+
// Evaluation.
229+
Decl *evaluate(Evaluator &evaluator,
230+
ClangCategoryLookupDescriptor desc) const;
231+
};
232+
181233
enum class CxxRecordSemanticsKind {
182234
Trivial,
183235
Owned,

include/swift/ClangImporter/ClangImporterTypeIDZone.def

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ SWIFT_REQUEST(ClangImporter, CXXNamespaceMemberLookup,
2424
SWIFT_REQUEST(ClangImporter, ClangRecordMemberLookup,
2525
Decl *(ClangRecordMemberLookupDescriptor), Uncached,
2626
NoLocationInfo)
27+
SWIFT_REQUEST(ClangImporter, ClangCategoryLookupRequest,
28+
Decl *(ClangCategoryLookupDescriptor), Uncached,
29+
NoLocationInfo)
2730
SWIFT_REQUEST(ClangImporter, CxxRecordSemantics,
2831
CxxRecordSemanticsKind(const clang::CXXRecordDecl *), Cached,
2932
NoLocationInfo)

lib/AST/Attr.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,8 @@ StringRef DeclAttribute::getAttrName() const {
13561356
case DAK_ObjC:
13571357
case DAK_ObjCRuntimeName:
13581358
return "objc";
1359+
case DAK_ObjCImplementation:
1360+
return "_objcImplementation";
13591361
case DAK_MainType:
13601362
return "main";
13611363
case DAK_DynamicReplacement:

lib/AST/Decl.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,31 @@ Type ExtensionDecl::getExtendedType() const {
14651465
return ErrorType::get(ctx);
14661466
}
14671467

1468+
bool ExtensionDecl::isObjCImplementation() const {
1469+
return getAttrs().hasAttribute<ObjCImplementationAttr>();
1470+
}
1471+
1472+
const clang::ObjCContainerDecl *
1473+
ExtensionDecl::getInterfaceForObjCImplementation() const {
1474+
assert(isObjCImplementation());
1475+
1476+
auto attr = getAttrs().getAttribute<ObjCImplementationAttr>();
1477+
if (attr->isCategoryNameInvalid())
1478+
return nullptr;
1479+
1480+
auto CD = dyn_cast_or_null<ClassDecl>(getExtendedNominal());
1481+
if (!CD)
1482+
return nullptr;
1483+
1484+
auto importedDecl = CD->getImportedObjCCategory(attr->CategoryName);
1485+
if (!importedDecl)
1486+
return nullptr;
1487+
1488+
assert(importedDecl->hasClangNode() &&
1489+
"@interface imported as clang-node-less decl?");
1490+
return cast<clang::ObjCContainerDecl>(importedDecl->getClangDecl());
1491+
}
1492+
14681493
PatternBindingDecl::PatternBindingDecl(SourceLoc StaticLoc,
14691494
StaticSpellingKind StaticSpelling,
14701495
SourceLoc VarLoc,

lib/ClangImporter/ClangImporter.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5024,6 +5024,56 @@ TinyPtrVector<ValueDecl *> ClangRecordMemberLookup::evaluate(
50245024
return result;
50255025
}
50265026

5027+
Decl *ClangCategoryLookupRequest::
5028+
evaluate(Evaluator &evaluator, ClangCategoryLookupDescriptor desc) const {
5029+
const ClassDecl *CD = desc.classDecl;
5030+
Identifier categoryName = desc.categoryName;
5031+
5032+
auto clangClass =
5033+
dyn_cast_or_null<clang::ObjCInterfaceDecl>(CD->getClangDecl());
5034+
if (!clangClass)
5035+
return nullptr;
5036+
5037+
if (categoryName.empty())
5038+
// No category name, so we want the decl for the `@interface` in
5039+
// `clangClass`.
5040+
return const_cast<ClassDecl *>(CD);
5041+
5042+
auto ident = &clangClass->getASTContext().Idents.get(categoryName.str());
5043+
auto clangCategory = clangClass->FindCategoryDeclaration(ident);
5044+
if (!clangCategory)
5045+
return nullptr;
5046+
5047+
ASTContext &ctx = CD->getASTContext();
5048+
return ctx.getClangModuleLoader()->importDeclDirectly(clangCategory);
5049+
}
5050+
5051+
Decl *ClassDecl::getImportedObjCCategory(Identifier name) const {
5052+
ClangCategoryLookupDescriptor desc{this, name};
5053+
return evaluateOrDefault(getASTContext().evaluator,
5054+
ClangCategoryLookupRequest(desc),
5055+
nullptr);
5056+
}
5057+
5058+
void swift::simple_display(llvm::raw_ostream &out,
5059+
const ClangCategoryLookupDescriptor &desc) {
5060+
out << "Looking up @interface for ";
5061+
if (!desc.categoryName.empty()) {
5062+
out << "category ";
5063+
simple_display(out, desc.categoryName);
5064+
}
5065+
else {
5066+
out << "main body";
5067+
}
5068+
out << " of ";
5069+
simple_display(out, desc.classDecl);
5070+
}
5071+
5072+
SourceLoc
5073+
swift::extractNearestSourceLoc(const ClangCategoryLookupDescriptor &desc) {
5074+
return extractNearestSourceLoc(desc.classDecl);
5075+
}
5076+
50275077
TinyPtrVector<ValueDecl *>
50285078
ClangImporter::Implementation::loadNamedMembers(
50295079
const IterableDeclContext *IDC, DeclBaseName N, uint64_t contextData) {

lib/Parse/ParseDecl.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2858,6 +2858,16 @@ bool Parser::parseNewDeclAttribute(DeclAttributes &Attributes, SourceLoc AtLoc,
28582858
Attributes.add(attr);
28592859
break;
28602860
}
2861+
case DAK_ObjCImplementation: {
2862+
SourceRange range;
2863+
auto name = parseSingleAttrOptionIdentifier(*this, Loc, range, AttrName, DK,
2864+
/*allowOmitted=*/true);
2865+
if (!name)
2866+
return false;
2867+
2868+
Attributes.add(new (Context) ObjCImplementationAttr(*name, AtLoc, range));
2869+
break;
2870+
}
28612871
case DAK_ObjCRuntimeName: {
28622872
SourceRange range;
28632873
auto name =

0 commit comments

Comments
 (0)