Skip to content

Commit 82d78a3

Browse files
authored
Merge pull request #60630 from beccadax/at-implementation
Add @_objcImplementation
2 parents c2db1b5 + 7f0791e commit 82d78a3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2207
-101
lines changed

docs/ReferenceGuides/UnderscoredAttributes.md

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,80 @@ 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 class's 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+
The members of an `@_objcImplementation` extension should fall into one of
725+
three categories:
726+
727+
* **Swift-only members** include any member marked `final`. These are not
728+
`@objc` or `dynamic` and are only callable from Swift. Use these for
729+
Swift-only APIs, random helper methods, etc.
730+
731+
* **ObjC helper members** include any non-`final` member marked `fileprivate`
732+
or `private`. These are implicitly `@objc dynamic`. Use these for action
733+
methods, selector-based callbacks, and other situations where you need a
734+
helper method to be accessible from an Objective-C message.
735+
736+
* **Member implementations** include any other non-`final` member. These are
737+
implicitly `@objc dynamic` and must match a member declared in the
738+
Objective-C header. Use these to implement the APIs declared in your
739+
headers. Swift will emit an error if these don't match your headers.
740+
741+
Notes:
742+
743+
* We don't currently plan to support ObjC generics.
744+
745+
* Eventually, we want the main `@_objcImplementation` extension to be able to
746+
declare stored properties that aren't in the interface. We also want
747+
`final` stored properties to be allowed to be resilent Swift types, but
748+
it's not clear how to achieve that without boxing them in `__SwiftValue`
749+
(which we might do as a stopgap).
750+
751+
* We should think about ObjC "direct" members, but that would probably
752+
require a way to spell this in Swift.
680753

681754
## `@_objc_non_lazy_realization`
682755

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;
@@ -2275,6 +2279,31 @@ class DocumentationAttr: public DeclAttribute {
22752279
}
22762280
};
22772281

2282+
class ObjCImplementationAttr final : public DeclAttribute {
2283+
public:
2284+
Identifier CategoryName;
2285+
2286+
ObjCImplementationAttr(Identifier CategoryName, SourceLoc AtLoc,
2287+
SourceRange Range, bool Implicit = false,
2288+
bool isCategoryNameInvalid = false)
2289+
: DeclAttribute(DAK_ObjCImplementation, AtLoc, Range, Implicit),
2290+
CategoryName(CategoryName) {
2291+
Bits.ObjCImplementationAttr.isCategoryNameInvalid = isCategoryNameInvalid;
2292+
}
2293+
2294+
bool isCategoryNameInvalid() const {
2295+
return Bits.ObjCImplementationAttr.isCategoryNameInvalid;
2296+
}
2297+
2298+
void setCategoryNameInvalid(bool newValue = true) {
2299+
Bits.ObjCImplementationAttr.isCategoryNameInvalid = newValue;
2300+
}
2301+
2302+
static bool classof(const DeclAttribute *DA) {
2303+
return DA->getKind() == DAK_ObjCImplementation;
2304+
}
2305+
};
2306+
22782307
/// Attributes that may be applied to declarations.
22792308
class DeclAttributes {
22802309
/// Linked list of declaration attributes.

include/swift/AST/Decl.h

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,13 @@ class alignas(1 << DeclAlignInBits) Decl : public ASTAllocated<Decl> {
723723
private:
724724
llvm::PointerUnion<DeclContext *, ASTContext *> Context;
725725

726+
/// The imported Clang declaration representing the \c @_objcInterface for
727+
/// this declaration (or vice versa), or \c nullptr if there is none.
728+
///
729+
/// If \c this (an otherwise nonsensical value), the value has not yet been
730+
/// computed.
731+
Decl *CachedObjCImplementationDecl;
732+
726733
Decl(const Decl&) = delete;
727734
void operator=(const Decl&) = delete;
728735
SourceLoc getLocFromSource() const;
@@ -740,7 +747,7 @@ class alignas(1 << DeclAlignInBits) Decl : public ASTAllocated<Decl> {
740747
protected:
741748

742749
Decl(DeclKind kind, llvm::PointerUnion<DeclContext *, ASTContext *> context)
743-
: Context(context) {
750+
: Context(context), CachedObjCImplementationDecl(this) {
744751
Bits.OpaqueBits = 0;
745752
Bits.Decl.Kind = unsigned(kind);
746753
Bits.Decl.Invalid = false;
@@ -975,6 +982,32 @@ class alignas(1 << DeclAlignInBits) Decl : public ASTAllocated<Decl> {
975982
return getClangNodeImpl().getAsMacro();
976983
}
977984

985+
/// If this is the Swift implementation of a declaration imported from ObjC,
986+
/// returns the imported declaration. Otherwise return \c nullptr.
987+
///
988+
/// \seeAlso ExtensionDecl::isObjCInterface()
989+
Decl *getImplementedObjCDecl() const;
990+
991+
/// If this is the ObjC interface of a declaration implemented in Swift,
992+
/// returns the implementating declaration. Otherwise return \c nullptr.
993+
///
994+
/// \seeAlso ExtensionDecl::isObjCInterface()
995+
Decl *getObjCImplementationDecl() const;
996+
997+
Optional<Decl *> getCachedObjCImplementationDecl() const {
998+
if (CachedObjCImplementationDecl == this)
999+
return None;
1000+
return CachedObjCImplementationDecl;
1001+
}
1002+
1003+
void setCachedObjCImplementationDecl(Decl *decl) {
1004+
assert((CachedObjCImplementationDecl == this
1005+
|| CachedObjCImplementationDecl == decl)
1006+
&& "can't change CachedObjCInterfaceDecl once it's computed");
1007+
assert(decl != this && "can't form circular reference");
1008+
CachedObjCImplementationDecl = decl;
1009+
}
1010+
9781011
/// Return the GenericContext if the Decl has one.
9791012
LLVM_READONLY
9801013
const GenericContext *getAsGenericContext() const;
@@ -1496,6 +1529,16 @@ class ExtensionDecl final : public GenericContext, public Decl,
14961529
/// resiliently moved into the original protocol itself.
14971530
bool isEquivalentToExtendedContext() const;
14981531

1532+
/// True if this extension provides an implementation for an imported
1533+
/// Objective-C \c \@interface. This implies various restrictions and special
1534+
/// behaviors for its members.
1535+
bool isObjCImplementation() const;
1536+
1537+
/// Returns the name of the category specified by the \c \@_objcImplementation
1538+
/// attribute, or \c None if the name is invalid. Do not call unless
1539+
/// \c isObjCImplementation() returns \c true.
1540+
Optional<Identifier> getCategoryNameForObjCImplementation() const;
1541+
14991542
// Implement isa/cast/dyncast/etc.
15001543
static bool classof(const Decl *D) {
15011544
return D->getKind() == DeclKind::Extension;
@@ -4460,6 +4503,12 @@ class ClassDecl final : public NominalTypeDecl {
44604503
/// the Objective-C runtime.
44614504
StringRef getObjCRuntimeName(llvm::SmallVectorImpl<char> &buffer) const;
44624505

4506+
/// Return the imported declaration for the category with the given name; this
4507+
/// will always be an Objective-C-backed \c ExtensionDecl or, if \p name is
4508+
/// empty, \c ClassDecl. Returns \c nullptr if the class was not imported from
4509+
/// Objective-C or does not have an imported category by that name.
4510+
IterableDeclContext *getImportedObjCCategory(Identifier name) const;
4511+
44634512
// Implement isa/cast/dyncast/etc.
44644513
static bool classof(const Decl *D) {
44654514
return D->getKind() == DeclKind::Class;

include/swift/AST/DeclContext.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,11 @@ class alignas(1 << DeclContextAlignInBits) DeclContext
507507
LLVM_READONLY
508508
DeclContext *getModuleScopeContext() const;
509509

510+
/// If this DeclContext is an \c \@_objcImplementation extension, returns the
511+
/// \c DeclContext for the Objective-C declaration it implements. Otherwise
512+
/// returns \c this.
513+
DeclContext *getImplementedObjCContext() const;
514+
510515
/// Returns the source file that contains this context, or null if this
511516
/// is not within a source file.
512517
LLVM_READONLY
@@ -824,6 +829,11 @@ class IterableDeclContext {
824829
/// abstractions on top of member loading, such as a name lookup table.
825830
DeclRange getCurrentMembersWithoutLoading() const;
826831

832+
/// Return the context that contains the actual implemented members. This
833+
/// is \em usually just \c this, but if \c this is an imported class or
834+
/// category, it may be a Swift extension instead.
835+
IterableDeclContext *getImplementationContext();
836+
827837
/// Add a member to this context.
828838
///
829839
/// If the hint decl is specified, the new decl is inserted immediately

include/swift/AST/DiagnosticsClangImporter.def

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,13 @@ WARNING(api_pattern_attr_ignored, none,
133133
"'%0' swift attribute ignored on type '%1': type is not copyable or destructible",
134134
(StringRef, StringRef))
135135

136+
ERROR(objc_implementation_two_impls, none,
137+
"duplicate implementation of Objective-C %select{|category %0 on }0"
138+
"class %1",
139+
(Identifier, ValueDecl *))
140+
NOTE(previous_objc_implementation, none,
141+
"previously implemented by extension here", ())
142+
136143
NOTE(macro_not_imported_unsupported_operator, none, "operator not supported in macro arithmetic", ())
137144
NOTE(macro_not_imported_unsupported_named_operator, none, "operator '%0' not supported in macro arithmetic", (StringRef))
138145
NOTE(macro_not_imported_invalid_string_literal, none, "invalid string literal", ())

include/swift/AST/DiagnosticsSema.def

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1492,6 +1492,42 @@ 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+
ERROR(attr_objc_implementation_no_objc_final,none,
1515+
"%0 %1 cannot be 'final' because Objective-C subclasses of %2 can "
1516+
"override it",
1517+
(DescriptiveDeclKind, ValueDecl *, ValueDecl *))
1518+
1519+
ERROR(member_of_objc_implementation_not_objc_or_final,none,
1520+
"%0 %1 does not match any %0 declared in the headers for %2; did you use "
1521+
"the %0's Swift name?",
1522+
(DescriptiveDeclKind, ValueDecl *, ValueDecl *))
1523+
NOTE(fixit_add_objc_for_objc_implementation,none,
1524+
"add '@objc' to define an Objective-C-compatible %0 not declared in "
1525+
"the header",
1526+
(DescriptiveDeclKind))
1527+
NOTE(fixit_add_final_for_objc_implementation,none,
1528+
"add 'final' to define a Swift %0 that cannot be overridden",
1529+
(DescriptiveDeclKind))
1530+
14951531
ERROR(cdecl_not_at_top_level,none,
14961532
"@_cdecl can only be applied to global functions", ())
14971533
ERROR(cdecl_empty_name,none,

include/swift/AST/Identifier.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ class Identifier {
9494

9595
bool empty() const { return Pointer == nullptr; }
9696

97-
bool is(StringRef string) const { return str().equals(string); }
97+
LLVM_ATTRIBUTE_USED bool is(StringRef string) const {
98+
return str().equals(string);
99+
}
98100

99101
/// isOperator - Return true if this identifier is an operator, false if it is
100102
/// a normal identifier.

include/swift/AST/TypeMemberVisitor.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,24 @@ class TypeMemberVisitor : public DeclVisitor<ImplClass, RetTy> {
5959
asImpl().visit(member);
6060
}
6161
}
62+
63+
/// A convenience method to visit all the members in the implementation
64+
/// context.
65+
///
66+
/// \seealso IterableDeclContext::getImplementationContext()
67+
void visitImplementationMembers(NominalTypeDecl *D) {
68+
for (Decl *member : D->getImplementationContext()->getMembers()) {
69+
asImpl().visit(member);
70+
}
71+
72+
// If this is a main-interface @_objcImplementation extension and the class
73+
// has a synthesized destructor, visit it now.
74+
if (auto cd = dyn_cast_or_null<ClassDecl>(D)) {
75+
auto dd = cd->getDestructor();
76+
if (dd->getDeclContext() == cd && cd->getImplementationContext() != cd)
77+
asImpl().visit(dd);
78+
}
79+
}
6280
};
6381

6482
template<typename ImplClass, typename RetTy = void>
@@ -70,6 +88,10 @@ class ClassMemberVisitor : public TypeMemberVisitor<ImplClass, RetTy> {
7088
void visitMembers(ClassDecl *D) {
7189
TypeMemberVisitor<ImplClass, RetTy>::visitMembers(D);
7290
}
91+
92+
void visitImplementationMembers(ClassDecl *D) {
93+
TypeMemberVisitor<ImplClass, RetTy>::visitImplementationMembers(D);
94+
}
7395
};
7496

7597
#undef BAD_MEMBER

0 commit comments

Comments
 (0)