Skip to content

Add @_objcImplementation #60630

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 14 commits into from
Oct 21, 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
75 changes: 74 additions & 1 deletion docs/ReferenceGuides/UnderscoredAttributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,80 @@ be added to a given type, while `@_nonSendable` indicates that an unavailable
`(_assumed)` after it, in which case `@Sendable` "beats" it.
`@_nonSendable(_assumed)` is intended to be used when mass-marking whole regions
of a header as non-`Sendable` so that you can make spot exceptions with
`@Sendable`.
`@Sendable`.

## `@_objcImplementation(CategoryName)`

Declares an extension that defines an implementation for the Objective-C
category `CategoryName` on the class in question, or for the main `@interface`
if the argument list is omitted.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is just internal documentation, but I think I would phrase this as:

## `@_objcImplementation` and `@_objcImplementation(CategoryName)`

Must be written on an `extension` of a non-generic imported Objective-C class.
When written without an argument list, emits an Objective-C class implementation
for the class.  When written with a category name argument, emits an Objective-C
category implementation for that category of the class, which must be declared
in the imported headers.  In both cases, the emitted implementation will look
exactly like what an Objective-C compiler would have emitted for a corresponding
`@implementation`.

Some part of me really hates that the first use case is done with an extension.
I don't know that I have a better option, though. Having two ClassDecls, or
gutting one for the other, both seem worse.


This attribute is used to write fully Objective-C-compatible implementations in
Swift. Normal Objective-C interop allows Objective-C clients to use instances of
the subclass, but not to subclass them, and uses a generated header that is not
meant to be read by humans. `@_objcImplementation`, on the other hand, creates
classes that are virtually indistinguishable from classes implemented in native
Objective-C: they do not have a Swift vtable or any other Swift-specific
metadata, Swift does not use any special knowledge of the class's "Swiftiness"
when using the class so ObjC runtime calls work correctly and they can even be
subclassed by Objective-C code, and you write a header for the class by hand
that looks exactly like an equivalent ObjC class. Clients should not notice if
you replace a native Objective-C `@implementation Foo (Bar)` with a Swift
`@_objcImplementation(Bar) extension Foo`.

You create a class with this feature very differently from normal ObjC interop:

1. Hand-write headers that declare the class's Objective-C interface, just as
you would for a native Objective-C class. Since you're handwriting these
headers, you can write them just as you would for an Objective-C class:
splitting them across multiple files, grouping related declarations together,
adding comments, declaring Swift behavior using C attributes or API notes,
etc.

2. Import your headers into Swift using a bridging header or umbrella header so
Swift can see them.

3. Implement your class using a mixture of `@implementation` declarations in
`.m` files and `@_objcImplementation extension`s in `.swift` files. Each
`@interface` should have exactly one corresponding implementation; don't try
to implement some members of a single `@interface` in ObjC and others in
Swift.

* To implement the main `@interface` of a class in Swift, use
`@_objcImplementation extension ClassName`.

* To implement a category in Swift, use
`@_objcImplementation(CategoryName) extension ClassName`.

The members of an `@_objcImplementation` extension should fall into one of
three categories:

* **Swift-only members** include any member marked `final`. These are not
`@objc` or `dynamic` and are only callable from Swift. Use these for
Swift-only APIs, random helper methods, etc.

* **ObjC helper members** include any non-`final` member marked `fileprivate`
or `private`. These are implicitly `@objc dynamic`. Use these for action
methods, selector-based callbacks, and other situations where you need a
helper method to be accessible from an Objective-C message.

* **Member implementations** include any other non-`final` member. These are
implicitly `@objc dynamic` and must match a member declared in the
Objective-C header. Use these to implement the APIs declared in your
headers. Swift will emit an error if these don't match your headers.

Notes:

* We don't currently plan to support ObjC generics.

* Eventually, we want the main `@_objcImplementation` extension to be able to
declare stored properties that aren't in the interface. We also want
`final` stored properties to be allowed to be resilent Swift types, but
it's not clear how to achieve that without boxing them in `__SwiftValue`
(which we might do as a stopgap).

* We should think about ObjC "direct" members, but that would probably
require a way to spell this in Swift.

## `@_objc_non_lazy_realization`

Expand Down
29 changes: 29 additions & 0 deletions include/swift/AST/Attr.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ class DeclAttribute : public AttributeBase {
kind : NumKnownProtocolKindBits,
isUnchecked : 1
);

SWIFT_INLINE_BITFIELD(ObjCImplementationAttr, DeclAttribute, 1,
isCategoryNameInvalid : 1
);
} Bits;

DeclAttribute *Next = nullptr;
Expand Down Expand Up @@ -2275,6 +2279,31 @@ class DocumentationAttr: public DeclAttribute {
}
};

class ObjCImplementationAttr final : public DeclAttribute {
public:
Identifier CategoryName;

ObjCImplementationAttr(Identifier CategoryName, SourceLoc AtLoc,
SourceRange Range, bool Implicit = false,
bool isCategoryNameInvalid = false)
: DeclAttribute(DAK_ObjCImplementation, AtLoc, Range, Implicit),
CategoryName(CategoryName) {
Bits.ObjCImplementationAttr.isCategoryNameInvalid = isCategoryNameInvalid;
}

bool isCategoryNameInvalid() const {
return Bits.ObjCImplementationAttr.isCategoryNameInvalid;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that a category name was given but doesn't match anything from the class?


void setCategoryNameInvalid(bool newValue = true) {
Bits.ObjCImplementationAttr.isCategoryNameInvalid = newValue;
}

static bool classof(const DeclAttribute *DA) {
return DA->getKind() == DAK_ObjCImplementation;
}
};

/// Attributes that may be applied to declarations.
class DeclAttributes {
/// Linked list of declaration attributes.
Expand Down
51 changes: 50 additions & 1 deletion include/swift/AST/Decl.h
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,13 @@ class alignas(1 << DeclAlignInBits) Decl : public ASTAllocated<Decl> {
private:
llvm::PointerUnion<DeclContext *, ASTContext *> Context;

/// The imported Clang declaration representing the \c @_objcInterface for
/// this declaration (or vice versa), or \c nullptr if there is none.
///
/// If \c this (an otherwise nonsensical value), the value has not yet been
/// computed.
Decl *CachedObjCImplementationDecl;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to pay for this on literally every declaration we make in the compiler. I doubt this is even hot. Can you use out-of-line storage for it?


Decl(const Decl&) = delete;
void operator=(const Decl&) = delete;
SourceLoc getLocFromSource() const;
Expand All @@ -740,7 +747,7 @@ class alignas(1 << DeclAlignInBits) Decl : public ASTAllocated<Decl> {
protected:

Decl(DeclKind kind, llvm::PointerUnion<DeclContext *, ASTContext *> context)
: Context(context) {
: Context(context), CachedObjCImplementationDecl(this) {
Bits.OpaqueBits = 0;
Bits.Decl.Kind = unsigned(kind);
Bits.Decl.Invalid = false;
Expand Down Expand Up @@ -975,6 +982,32 @@ class alignas(1 << DeclAlignInBits) Decl : public ASTAllocated<Decl> {
return getClangNodeImpl().getAsMacro();
}

/// If this is the Swift implementation of a declaration imported from ObjC,
/// returns the imported declaration. Otherwise return \c nullptr.
///
/// \seeAlso ExtensionDecl::isObjCInterface()
Decl *getImplementedObjCDecl() const;

/// If this is the ObjC interface of a declaration implemented in Swift,
/// returns the implementating declaration. Otherwise return \c nullptr.
///
/// \seeAlso ExtensionDecl::isObjCInterface()
Decl *getObjCImplementationDecl() const;

Optional<Decl *> getCachedObjCImplementationDecl() const {
if (CachedObjCImplementationDecl == this)
return None;
return CachedObjCImplementationDecl;
}

void setCachedObjCImplementationDecl(Decl *decl) {
assert((CachedObjCImplementationDecl == this
|| CachedObjCImplementationDecl == decl)
&& "can't change CachedObjCInterfaceDecl once it's computed");
assert(decl != this && "can't form circular reference");
CachedObjCImplementationDecl = decl;
}

/// Return the GenericContext if the Decl has one.
LLVM_READONLY
const GenericContext *getAsGenericContext() const;
Expand Down Expand Up @@ -1496,6 +1529,16 @@ class ExtensionDecl final : public GenericContext, public Decl,
/// resiliently moved into the original protocol itself.
bool isEquivalentToExtendedContext() const;

/// True if this extension provides an implementation for an imported
/// Objective-C \c \@interface. This implies various restrictions and special
/// behaviors for its members.
bool isObjCImplementation() const;

/// Returns the name of the category specified by the \c \@_objcImplementation
/// attribute, or \c None if the name is invalid. Do not call unless
/// \c isObjCImplementation() returns \c true.
Optional<Identifier> getCategoryNameForObjCImplementation() const;

// Implement isa/cast/dyncast/etc.
static bool classof(const Decl *D) {
return D->getKind() == DeclKind::Extension;
Expand Down Expand Up @@ -4460,6 +4503,12 @@ class ClassDecl final : public NominalTypeDecl {
/// the Objective-C runtime.
StringRef getObjCRuntimeName(llvm::SmallVectorImpl<char> &buffer) const;

/// Return the imported declaration for the category with the given name; this
/// will always be an Objective-C-backed \c ExtensionDecl or, if \p name is
/// empty, \c ClassDecl. Returns \c nullptr if the class was not imported from
/// Objective-C or does not have an imported category by that name.
IterableDeclContext *getImportedObjCCategory(Identifier name) const;

// Implement isa/cast/dyncast/etc.
static bool classof(const Decl *D) {
return D->getKind() == DeclKind::Class;
Expand Down
10 changes: 10 additions & 0 deletions include/swift/AST/DeclContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,11 @@ class alignas(1 << DeclContextAlignInBits) DeclContext
LLVM_READONLY
DeclContext *getModuleScopeContext() const;

/// If this DeclContext is an \c \@_objcImplementation extension, returns the
/// \c DeclContext for the Objective-C declaration it implements. Otherwise
/// returns \c this.
DeclContext *getImplementedObjCContext() const;

/// Returns the source file that contains this context, or null if this
/// is not within a source file.
LLVM_READONLY
Expand Down Expand Up @@ -824,6 +829,11 @@ class IterableDeclContext {
/// abstractions on top of member loading, such as a name lookup table.
DeclRange getCurrentMembersWithoutLoading() const;

/// Return the context that contains the actual implemented members. This
/// is \em usually just \c this, but if \c this is an imported class or
/// category, it may be a Swift extension instead.
IterableDeclContext *getImplementationContext();

/// Add a member to this context.
///
/// If the hint decl is specified, the new decl is inserted immediately
Expand Down
7 changes: 7 additions & 0 deletions include/swift/AST/DiagnosticsClangImporter.def
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ WARNING(api_pattern_attr_ignored, none,
"'%0' swift attribute ignored on type '%1': type is not copyable or destructible",
(StringRef, StringRef))

ERROR(objc_implementation_two_impls, none,
"duplicate implementation of Objective-C %select{|category %0 on }0"
"class %1",
(Identifier, ValueDecl *))
NOTE(previous_objc_implementation, none,
"previously implemented by extension here", ())

NOTE(macro_not_imported_unsupported_operator, none, "operator not supported in macro arithmetic", ())
NOTE(macro_not_imported_unsupported_named_operator, none, "operator '%0' not supported in macro arithmetic", (StringRef))
NOTE(macro_not_imported_invalid_string_literal, none, "invalid string literal", ())
Expand Down
36 changes: 36 additions & 0 deletions include/swift/AST/DiagnosticsSema.def
Original file line number Diff line number Diff line change
Expand Up @@ -1492,6 +1492,42 @@ ERROR(swift_native_objc_runtime_base_not_on_root_class,none,
"@_swift_native_objc_runtime_base_not_on_root_class can only be applied "
"to root classes", ())

ERROR(attr_objc_implementation_must_be_unconditional,none,
"only unconditional extensions can implement an Objective-C '@interface'",
())
ERROR(attr_objc_implementation_must_extend_class,none,
"cannot mark extension of %0 %1 with '@_objcImplementation'; it is not "
"an imported Objective-C class",
(DescriptiveDeclKind, ValueDecl *))
ERROR(attr_objc_implementation_must_be_imported,none,
"'@_objcImplementation' cannot be used to extend %0 %1 because it was "
"defined by a Swift 'class' declaration, not an imported Objective-C "
"'@interface' declaration",
(DescriptiveDeclKind, ValueDecl *))
ERROR(attr_objc_implementation_category_not_found,none,
"could not find category %0 on Objective-C class %1; make sure your "
"umbrella or bridging header imports the header that declares it",
(Identifier, ValueDecl*))
NOTE(attr_objc_implementation_fixit_remove_category_name,none,
"remove arguments to implement the main '@interface' for this class",
())
ERROR(attr_objc_implementation_no_objc_final,none,
"%0 %1 cannot be 'final' because Objective-C subclasses of %2 can "
"override it",
(DescriptiveDeclKind, ValueDecl *, ValueDecl *))

ERROR(member_of_objc_implementation_not_objc_or_final,none,
"%0 %1 does not match any %0 declared in the headers for %2; did you use "
"the %0's Swift name?",
(DescriptiveDeclKind, ValueDecl *, ValueDecl *))
NOTE(fixit_add_objc_for_objc_implementation,none,
"add '@objc' to define an Objective-C-compatible %0 not declared in "
"the header",
(DescriptiveDeclKind))
NOTE(fixit_add_final_for_objc_implementation,none,
"add 'final' to define a Swift %0 that cannot be overridden",
(DescriptiveDeclKind))

ERROR(cdecl_not_at_top_level,none,
"@_cdecl can only be applied to global functions", ())
ERROR(cdecl_empty_name,none,
Expand Down
4 changes: 3 additions & 1 deletion include/swift/AST/Identifier.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ class Identifier {

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

bool is(StringRef string) const { return str().equals(string); }
LLVM_ATTRIBUTE_USED bool is(StringRef string) const {
return str().equals(string);
}

/// isOperator - Return true if this identifier is an operator, false if it is
/// a normal identifier.
Expand Down
22 changes: 22 additions & 0 deletions include/swift/AST/TypeMemberVisitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ class TypeMemberVisitor : public DeclVisitor<ImplClass, RetTy> {
asImpl().visit(member);
}
}

/// A convenience method to visit all the members in the implementation
/// context.
///
/// \seealso IterableDeclContext::getImplementationContext()
void visitImplementationMembers(NominalTypeDecl *D) {
for (Decl *member : D->getImplementationContext()->getMembers()) {
asImpl().visit(member);
}

// If this is a main-interface @_objcImplementation extension and the class
// has a synthesized destructor, visit it now.
if (auto cd = dyn_cast_or_null<ClassDecl>(D)) {
auto dd = cd->getDestructor();
if (dd->getDeclContext() == cd && cd->getImplementationContext() != cd)
asImpl().visit(dd);
}
}
};

template<typename ImplClass, typename RetTy = void>
Expand All @@ -70,6 +88,10 @@ class ClassMemberVisitor : public TypeMemberVisitor<ImplClass, RetTy> {
void visitMembers(ClassDecl *D) {
TypeMemberVisitor<ImplClass, RetTy>::visitMembers(D);
}

void visitImplementationMembers(ClassDecl *D) {
TypeMemberVisitor<ImplClass, RetTy>::visitImplementationMembers(D);
}
};

#undef BAD_MEMBER
Expand Down
Loading