[Proposal] Opt-in Reflection metadata

Opt-In Reflection metadata

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 to selectively enable reflection metadata only for types that need it and add a way to express a requirement of reflection metadata for APIs developers.

Previous discussion

Motivation

Binary Size

Currently, the only available way to enable/disable reflection metadata is to do that for a whole module at once.
However, enabling reflection for a whole module can be quite expensive especially in cases when an API’s surface is quite big. There are some libraries and APIs that require reflection metadata (like SwiftUI, many JSON and DI frameworks, etc). Therefore if an application contains a lot of modules that are users of such API, all of them must have reflection metadata enabled and the compiler will emit the metadata for all of the declarations inside even if not all of them are used in the API.

Type safety

Another important reason is the current inability to express reflection metadata requirements in API.
Mirror/SwiftUI use the metadata under the hood, but they don't have an option to express that and sometimes it becomes a surprise for developers. It makes such API is less safe since it becomes a runtime issue rather than compile-time. For example, SwiftUI internal implementation uses reflection metadata to trigger re-rendering of the view hierarchy when a state has changed, but if by some reason a user module compiled with the metadata disabled, changing of the state won't trigger that behaviour and will cause inconsistency between a state and a representation.

Proposed solution

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

Firstly, it 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 error which will make an API's user enable the feature and the compiler subsequently will generate metadata only for a type that need to have it.

Secondly, it will help to reduce the binary size overhead from the metadata that is not used at runtime.
Only classes, structs, enums, and protocols that are marked with that attribute or inherit it will get reflection metadata generated which will make the existing API more efficient.

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())

The compiler will emit reflection metadata for SomeView because it inherits the attribute from SwiftUI.View protocol, and skip emission for SomeModel struct. 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 with a -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 the reflection metadata. The attribute can be inherited 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 {}

The type’s reflectability is computed during Sema and cached in the Evaluator. If a declaration is marked @reflectable, but the reflection is disabled or enabled in full, the compiler will emit a warning. A new flag -enable-opt-in-reflection-metadata should be used to enable that behaviour explicitly.

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 is 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 will 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 emitted.

@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.

5 Likes

cc @Joe_Groff

What kind of info can I get from this metadata today and how do I get it?

1 Like

Currently, the only official API to get access to the reflection metadata is Mirror API

However, as far as I know, SwiftUI uses some private API for that.

Usually, it is used to get information about fields of nomial types like type and name(if names are enabled).

Reflection metadata is also used by developer tools that come with Xcode, like the memory graph debugger, Instruments, and the command-line leaks tool, in order to precisely walk Swift objects. Without reflection metadata those tools have to fall back to conservative guessing, which can make their understanding of the memory graph less accurate. The default implementation of print also uses reflection to print the fields of structs.

4 Likes

My understanding of print/dump and developer tools is all that intended for debugging when the compiler will keep all metadata in full (not only reflection).

That opt-in mode is mostly about release build, where it doesn't make a lot of sense to keep metadata that is not used at runtime.

I strongly support this pitch in principle. Binary size is one of the biggest problems for Swift apps and libraries targeting WebAssembly. I imagine this is also a big problem when targetting embedded platforms, and maybe even Swift on Linux, where people are concerned with the size of their Docker images.

At the same time, I wonder whether reflection metadata could be optimized away at LTO time completely if users enable static linking. I think this is a part of what @kateinoigakukun have been doing this summer as a part of his GSoC project. If I understand correctly, with static linking it's easy to infer what metadata is unused, so no opt-in is needed.

I imagine, this is not possible to infer with dynamic linking in all cases. Is that the reason for the explicit opt-in pitched here?

I think this is the key thing. Binary size is a secondary side effect of the semantic problem of reflection, that all reflectable types have to be conservatively considered to be "used" and their metadata kept around in case code expects to dynamically discover the type in a way the compiler and linker can't statically anticipate. Being able to assume that more types are unused will allow dead code elimination and linker dead stripping to eliminate more metadata.

Focusing on the size of the reflection metadata for types that are used may be a red herring—yes, the metadata takes up space, but we also have to consider the size of the code that reflection metadata is able to replace, since if default implementations of printing, serialization, SwiftUI view diffing, and such had specialized code generated for every type that has those operations, the code would likely use much more space than the reflection metadata itself. More and more of the Swift runtime has been changing over to consume reflection metadata in order to reduce overall code size for unspecialized generic type instantiation, since much of the information we need can be gleaned from metadata instead of from open-coded accessors, and in our measurements, the sum total of those miscellaneous accessors can be a much bigger problem than the reflection metadata itself. Furthermore, all of the reflection metadata is "true const" at runtime, and as such, doesn't allocate any dirty VM at runtime; it may be a nuisance for download size, but if your program doesn't use the reflection metadata at runtime, it's effectively free since it shouldn't even get paged in.

As such, I think we want @reflectable to be more akin to an __attribute__((used)) sort of annotation, to tell the compiler and linker that the type must be discoverable at runtime by _getTypeByMangledName even if it isn't statically used. The compiler and linker can then be much more aggressive about culling reflection metadata for types whose metadata is never used. And even for types that are used, but not marked as needing to be discoverable by name, we could emit reflection metadata that elides names, improving secrecy and reducing metadata size further.

21 Likes

So, if I got you correctly, you are talking about instead of limiting the amount of generated reflection metadata, we could stop marking all reflection globals with no_dead_strip by default, and start doing that only for those ones which are generated from declarations that were marked with @reflectable. In that case, it would allow linkers to remove those unused symbols.

It definitely sounds like a more general approach :slight_smile:

1 Like

Yeah, exactly.

1 Like

Cool, thanks!
I'll rework the proposal.

1 Like

This PR might be of interest to you: Add a -emit-dead-strippable-symbols flag that emits functions/variable/metadata in a dead_strip-friendly way. by kubamracek · Pull Request #34281 · apple/swift · GitHub

3 Likes

Apologies for the delay, I was on PTO :slightly_smiling_face:
Here is the fixed pitch and updated prototype.