Hello everyone!
The following is a pitch for adding an ability to declared a runtime discoverable attributes in the Swift language. We welcome any feedback or questions about this pitch!
- Authors: Pavel Yaskevich, Holly Borla, Alejandro Alonso
- Status: Awaiting implementation
- Implementation: [Sema/SILGen/IRGen] Implement runtime discoverable attributes (under flag) by xedin · Pull Request #62426 · apple/swift · GitHub
Introduction
In Swift, declarations are annotated with attributes to opt into both builtin language features (e.g. @available
) and library functionality (e.g. @RegexComponentBuilder
). However, custom attributes defined by libraries are limited - libraries can currently define custom attributes for property wrappers, result builders, and global actors. This proposal introduces the ability to attach library-defined runtime metadata to declarations using custom attributes, which can then be queried by the library to opt client code into library functionality.
Previous Swift Forum discussions
Motivation
There are some problem domains in which it can be beneficial for a library author to let a client annotate within their own code certain declarations that the library should be made aware of, since requiring the client call an explicit API instead would be too onerous, repetitive, or easy to forget.
One classic example is testing: there is a common pattern in unit testing libraries where users define a type that extends one of the library's types, they annotate some of its methods as tests, and the library locates and runs all tests automatically, after initializing a distinct instance of the containing type for each. There is no official mechanism in Swift to implement this test discovery pattern today, however XCTest—the language's current de-facto standard test library—has longstanding workarounds:
- On Apple platforms, XCTest relies on the Objective-C runtime to enumerate all subclasses of a known base class and their methods, and it considers all instance methods with a supported signature and name prefixed with "test" a test method.
- On other platforms, XCTest is typically used from a package, and the Swift Package Manager has special logic to introspect build-time indexer data, locate test methods, and explicitly pass the list of discovered tests to XCTest to run.
XCTest's current approach has some drawbacks and limitations:
- Users must adhere to a strict naming convention by prefixing all test methods with the word "test". This prefix can be redundant since all tests include it, and may surprise users if they accidentally use that prefix on a non-test method since the behavior is implicit.
- Since tests are declared implicitly, there is no way for a user to provide additional details about an individual test or group of tests. It would be useful to have a way to indicate whether a test is enabled, its requirements, or other metadata, for example, so that the testing library could use this information to inform how it executes tests and offer more powerful features.
- The lack of a built-in runtime discovery mechanism means that related tools (such as Swift Package Manager) require specialized discovery logic for each test library they support. This makes adding support for alternate test libraries to those tools very difficult and increases their implementation complexity.
The general pattern of registering code to be discovered by a framework is common across Swift programs. For example, a program that uses a plugin architecture commonly uses a protocol for the interface of the plugin, which is then implemented on concrete types in clients. This pattern imposes error-prone registration boilerplate, where clients must explicitly supply a list of concrete plugin types or explicitly register individual plugin types to be used by the framework before the framework needs them.
Java and C# programming languages both have a feature which is called “Java Annotations” and “C# attributes” respectively. It allows to add attributes on methods, variables, parameters, and, in case of Java, packages that are accessible to Java/C# compiler and at runtime via reflection APIs. We propose a similar addition to Swift called custom metadata attributes.
Proposed solution
- A new builtin attribute
@runtimeMetadata
that can be applied to structs and classes. - Types annotated with this builtin attribute can be used as custom attributes on declarations that can be used as values.
- The custom attribute can have additional arguments; the custom attribute application will turn into an initializer call on the attribute type, passing in the declaration value as the first argument.
- A reflection API that can gather all declarations with a given custom attribute attached.
Detailed design
Declaring runtime metadata attributes
Runtime metadata custom attributes are declared by attaching the built-in @runtimeMetadata
attribute to a struct or class:
@runtimeMetadata
struct Example { ... }
A runtime metadata type must have an initializer of the form init(attachedTo:)
. The type of the attachedTo
parameter dictates which types of declarations the custom attribute can be applied to, as described in the following section.
Applications of runtime metadata types
Runtime metadata attributes can be applied to any declaration that can be used as a first-class value in Swift, including:
- Types
- Global functions
- Static and instance methods
- Instance properties
Runtime metadata types opt into which kinds of declarations are supported using initializers. For an application of a runtime metadata attribute to be well-formed, the runtime metadata type must declare an initializer that accepts the appropriate value as the first argument. Applications of a runtime metadata type to a type will synthesize an initializer call with the attribute arguments, and the declaration value passed as the first initializer argument:
- Types will pass a metatype
- Global functions will pass an unapplied function reference
- Static and instance methods will pass an unapplied method reference
- Instance properties will pass a key-path
@runtimeMetadata
struct Flag {
// Initializer that accepts metatype of a nominal type
init<T>(attachedTo: T.Type) {
// ...
}
// Initializer that accepts an unapplied reference to a global function
init<Args, Result>(attachedTo: (Args) -> Result) {
// ...
}
// Initializer that accepts an unapplied reference to a method (static &
// instance).
init<T, Args, Result>(attachedTo: (T) -> (Args) -> Result) {
// ...
}
// Initializer that accepts a reference to an instance property
init<T, V>(attachedTo: KeyPath<T, V>, custom: Int) {
// ...
}
}
// Compiler is going to synthesize following generator call
// -> Flag.init(attachedTo: doSomething)
@Flag func doSomething(_: Int, other: String) {}
// Compiler is going to synthesize following generator call
// -> Flag.init(attachedTo: Test.self)
@Flag
struct Test {
// Compiler is going to synthesize following generator call
// -> Flag.init(attachedTo: Test.computeStateless)
@Flag static func computeStateless() {}
// Compiler is going to synthesize following generator call
// -> Flag.init(attachedTo: Test.compute(values:))
@Flag func compute(values: [Int]) {}
// Compiler is going to synthesize following generator call
// -> Flag.init(attachedTo: \Test.answer, custom: 42)
@Flag(custom: 42) var answer: Int = 42
}
A given declaration can have multiple runtime metadata attributes as long as a given runtime metadata type only appears once:
@Flag @Ignore func ignored() { 🟢
...
}
@Flag @Flag func specialFunction() { 🔴
^ error: duplicate runtime discoverable attribute
...
}
Runtime metadata attributes must be applied at the primary declaration of a type; applying the attribute to a type in an extension is prohibited to prevent the same type from having multiple runtime metadata annotations of the same type.
@Flag extension MyType [where ...] { 🔴
^ error: cannot associate runtime attribute @Flag with MyType in extension
}
Inference of metadata attributes
A metadata attribute can be applied to a protocol:
@EditorCommandRecord
protocol EditorCommand { ... }
Conceptually, the runtime metadata attribute is applied to the generic Self
type that represents the concrete conforming type. When a protocol conformance is written at the primary declaration of a concrete type, the runtime metadata attribute is inferred:
// @EditorCommandRecord is inferred
struct SelectWordCommand: EditorCommand { ... }
If the protocol conformance is written in an extension on the conforming type, attribute inference is prohibited.
// @EditorCommandRecord is not inferred
extension SelectWordCommand : EditorCommand { 🔴
...
}
Runtime metadata attributes applied to protocols cannot have additional attribute arguments; attribute arguments must be explicitly written on the conforming type.
Accessing metadata through reflection
With the introduction of the new Reflection module, we feel a natural place to reflectively retrieve these attributes is there.
public struct Attribute {
public static func getAllInstances<T>(of: T.Type) -> [T?]
}
This API will retrieve all of the instances of your runtime attribute across all modules. Instances of metadata types are initialized in the runtime query to gather the metadata.
API Availability
Custom metadata attributes can be attached to declarations with limited availability. The runtime query for an individual instance of the metadata attribute type will be gated on a matching availability condition, returning nil
if the availability condition is not met at runtime. For example:
@available(macOS, introduced: 12)
@Flag
struct NewType { ... }
The runtime query that produces the Flag
instance attached to NewType
will effectively execute the following code:
if #available(macOS 12, *) {
return Flat(attachedTo: NewType.self)
} else {
return nil
}
Alternatives considered
Alternative attribute names
@runtimeMetadata
@dynamicMetadata
@metadata
@runtimeAnnotation
@runtimeAttribute
Bespoke @test
attribute
A previous Swift Evolution discussion suggested adding a built-in @test
attribute to the language. However, registration is a general code pattern that is also used outside of testing, so allowing libraries to declare their own domain-specific attributes is a more general approach that supports a wider set of use cases.