Pitch #2: Opt-in Reflection metadata

[Proposal] Opt-In Reflection metadata

Pitch #1

Introduction

Reflection can be a useful thing to create convenient and concise APIs for libraries. This proposal seeks to improve the safety of such APIs and to tackle the binary size problem by introducing a mechanism of selectively keeping reflection metadata only for types that need it and dead strip it for all others. Developers will gain an opportunity to express a requirement to have reflection metadata in source-code.

Motivation

API safety
Currently, it is impossible for APIs to express reflection metadata requirements which can lead to unexpected behavior. For example, SwiftUI implementation uses reflection metadata from user modules to trigger re-rendering of the view hierarchy when a state has changed. If by some reason a user module was compiled with the disabled metadata, changing the state won't trigger that behavior and will cause inconsistency between state and representation which will make such API less safe since it becomes a runtime issue rather than compile-time one.

Dead Stripping
The compiler can’t statically determine the uses of reflection metadata between the boundaries of the main binary and frameworks. Therefore all symbols must be no_dead_strippable just in case other frameworks uses that metadata. Hereupon the linker can't strip such symbols which can lead to an increase of binary size especially in cases when API's surface is big.

Proposed solution

Introducing a new attribute @reflectable for nominal types will help to solve two issues mentioned above at once:

  • Firstly, this will allow libraries to express a requirement to have reflection metadata for types on the source-code level which will make such APIs safer. If a declaration is marked with @reflectable or inherits it, but the opt-in reflection metadata is not enabled, the compiler will emit a compile-time warning suggesting to enable the feature.

  • Secondly, this will help to reduce the binary size overhead from the metadata that is not used at runtime and possibly lead to other optimizations. The reflection metadata for nominal declarations (classes, structs, enums, and protocols) marked with @reflectable will be forced to stay at the binary while the other can be dead stripped by DCE or the linker if are not used within a module.

Study Case:

SwiftUI Framework:

@reflectable
public protocol SwiftUI.View {}

User module:

import SwiftUI

struct SomeView: SwiftUI.View {
    var body: some View {        
        Text("Hello, World!")
            .frame(...)    
    }
}

struct SomeModel {}

window.contentView = NSHostingView(rootView: SomeView())

Reflection metadata for SomeView will be emitted with the no_dead_strip label because it inherits the attribute from SwiftUI.View protocol, while SomeModel metadata might be stripped unless it’s referenced from somewhere else. If an API's user module gets compiled without the -enable-opt-in-reflection-metadata flag, the compiler will emit a warning.

Detailed design

One more level of reflection metadata is introduced in addition to two existing ones:

  1. Full Reflection metadata is enabled by default.
  2. Opt-In reflection metadata is enabled explicitly with -enable-opt-in-reflection-metadata flag.
  3. Reflection metadata is completely disabled with -disable-reflection-metadata flag.

A new attribute for nominal types @reflectable marks nominal types for which the compiler will emit reflection metadata symbols with no_dead_strip directive while other reflection symbols might be dead stripped by the linker in case they are not in use within a module.

The attribute should be inheritable from parents for classes and protocols:

Example 1:

@reflectable
protocol Ref {}
// Class A will inherit the attribute from the protocol conformance.
class A: Ref {}

Example 2:

@reflectable
class B {}
// Class C will inherit the protocol from the parent class.
class C: B {}

Source compatibility

The change won’t break source compatibility due to the purely additive nature of the change.

Effect on ABI stability

The proposal doesn’t change the existing ABI but introduces new guarantees between libraries and clients.

Effect on API resilience

Adding/removing the @reflectable attribute won’t affect API resilience and break any existing guarantees. However, it’s still possible that some reflection metadata might be unavailable for cases when a library was updated with the new attribute, but a client wasn’t recompiled against a new version of the library.

Future direction

If this proposal gets accepted, it will allow extending the Swift Standard library with a new protocol Reflectable that will be marked with @reflectable attribute. All existing APIs that require reflection metadata might be extended to accept objects conforming to that protocol. It will help to improve the safety of such API because the compiler will ensure that for all types, conforming to the Reflectable protocol, the required reflection metadata is discoverable at runtime.

@reflectable
public protocol Reflectable {}

public struct Mirror {
    // A new constructor that takes an object conforming to Reflectable protocol.
    public init(reflecting subject: Reflectable) {
        ...
    }
}

Alternatives considered

An implementation with a magic protocol also was considered. However, It seems that an attribute will be a more generic approach in this case.

18 Likes

+1
Fine grained metadata control and flexible design pattern.

1 Like

Can you elaborate on this? It is not obvious how or why this is the case. I think we’re starting to see too many features reach first for magic annotations where a magic protocol should be sufficient and would integrate much better with the type system.

2 Likes

That's a good question, thanks!

By

a more generic approach

I meant that an attribute is more flexible and doesn't require conformance for cases when it's not necessary to introduce a new protocol (Swift can be written not only in protocol-oriented style). It's also possible that in the future, an attribute will gain some additional parameters to setup IRGen. For instance, the level of emitted reflection and/or turn on/off names.

Another point, or at least how I understand the difference, is that a protocol implies some semantic traits that are used on the language level (like derived conformance protocols when the compiler adds synthesized functions), while an attribute is more about some internal compiler's behavior that doesn't change any traits but affects CodeGen, ABI, etc.

However, if the community thinks it should be a magic protocol, I am completely fine with that.

This proposal looks really great @omax!

1 Like

Did you mean to use a generic signature instead:

init<Subject : Reflectable>(
    reflecting subject: Subject 
)

or is the use of the existential of “Reflectable” intentional. If it is intentional could you elaborate?

1 Like

You are right, a constrained generic suits better for this example than existential type!
Thanks!
I'll fix the PR.

1 Like

This proposal solves a very important issue with code size, while allowing reflection to be used. Very nice!

5 Likes