Why nominal type descriptors are always marked 'no dead strip'?

I looked into ld logs to see why some symbols remain unstripped and discovered that nominal type descriptors and reflection metadata field descriptors always 'no dead strip' (@llvm.used). Can someone explain why, please?

class SomeClass {}

Compiled with -emit-ir -O -internalize-at-link:
@llvm.used = appending global [5 x ptr] [
  ptr @"\01l_entry_point", 
  ptr @"nominal type descriptor runtime record for output.SomeClass", 
  ptr @"reflection metadata field descriptor output.SomeClass", 
  ptr @__swift_reflection_version, 
  ptr @_swift1_autolink_entries
], section "llvm.metadata"

The reason I ask is because nominal type descriptors contain a reference to the type's init which may refer to a bunch of other symbols.

3 Likes

i am speculating somewhat here, but after poring through some history, my best guess is that it was originally added to support the _typeByName() function in the stdlib, and associated swift-corelibs-foundation functionality to implement NSClassFromString (as outlined in this issue).

it appears reflection metadata can be either fully removed, or retained just for the debugger (via -disable-reflection-metadata and -reflection-metadata-for-debugger-only, respectively). there's also the -conditional-runtime-records frontend flag that might be of use for identifying more unused symbols (as outlined here). however, i've yet to find a means of removing nominal type descriptors even if the type itself is unused.

4 Likes

i've yet to find a means of removing nominal type descriptors even if the type itself is unused.

apparently, on Darwin, the combination of -conditional-runtime-records and -lto=llvm-thin[1] results in the nominal type descriptors being stripped if they are unreferenced. however, for unknown reasons, when testing on Linux (via compiler explorer), this does not appear to be the case.

having now looked into the implementation somewhat, it does make me wonder:

given that the motivating use case for _typeByName() is seemingly class-from-string support – could the implementation reasonably be changed to stop treating all type metadata uniformly for this purpose? i.e. don't emit the type metadata for enums, structs, etc since they aren't expected to support being looked up in this way?

the conditional runtime records approach seems like a more general solution for reducing unwanted metadata and it avoids breaking clients depending on the current functionality (which GitHub suggests there are a decent number).


  1. or -lto=llvm-full ↩ī¸Ž

Unfortunately, if you look up a generic class by name, you may end up having to resolve structs and enums that are in its generic parameters. There could probably be an opt-out or default-flipping feature here, but just "is it a class" isn't sufficient.

2 Likes

Can you share your full compile command? I tried those flags but still see the nominal type descriptor and its referents in the final binary.

main.swift:
class SomeClass {
  init(a: Void) {}
}

@main
struct App {
  static func main() {}
}
TC='com.apple.dt.toolchain.XcodeDefault'
xcrun --toolchain $TC \
  swiftc -parse-as-library -emit-executable -target arm64-apple-macos14 -Osize \
  -Xfrontend -conditional-runtime-records \
  -Xfrontend -lto=llvm-thin \
  -Xfrontend -internalize-at-link \
  -Xfrontend -disable-reflection-metadata \
  -o main main.swift -v
nm -m main | xcrun swift-demangle | grep 'SomeClass'

0000000100003e80 (__TEXT,__text) non-external (was a private external) main.SomeClass.__allocating_init(a: ()) -> main.SomeClass
0000000100003f78 (__TEXT,__constg_swiftt) non-external (was a private external) method descriptor for main.SomeClass.__allocating_init(a: ()) -> main.SomeClass
0000000100003e90 (__TEXT,__text) non-external (was a private external) main.SomeClass.init(a: ()) -> main.SomeClass
0000000100003eb4 (__TEXT,__text) non-external (was a private external) type metadata accessor for main.SomeClass
00000001000080b8 (__DATA,__data) non-external full type metadata for main.SomeClass
0000000100008090 (__DATA,__data) non-external (was a private external) metaclass for main.SomeClass
0000000100003f44 (__TEXT,__constg_swiftt) non-external (was a private external) nominal type descriptor for main.SomeClass
00000001000080d0 (__DATA,__data) non-external (was a private external) type metadata for main.SomeClass
0000000100003e98 (__TEXT,__text) non-external (was a private external) main.SomeClass.__deallocating_deinit
0000000100003e90 (__TEXT,__text) non-external (was a private external) main.SomeClass.deinit
0000000100008048 (__DATA,__objc_const) non-external __DATA_main.SomeClass
0000000100008000 (__DATA,__objc_const) non-external __METACLASS_DATA_main.SomeClass

The nominal type descriptor isn't marked as no dead strip in the object anymore. But there is l_$s4main9SomeClassCHn (nominal type descriptor runtime record for main.SomeClass prefixed with an l) that is marked, and it refers to the nominal type descriptor.

xcrun --toolchain $TC \
  swiftc -parse-as-library -emit-object -target arm64-apple-macos14 -Osize \
  -Xfrontend -conditional-runtime-records \
  -Xfrontend -lto=llvm-thin \
  -Xfrontend -internalize-at-link \
  -Xfrontend -disable-reflection-metadata \
  -o main.o main.swift -v

nm -m main.o | grep 'SomeClass'
...
0000000000000260 (__TEXT,__swift5_types) non-external [no dead strip] l_$s4main9SomeClassCHn

# And l_$s4main9SomeClassCHn refers to itself and _$s4main9SomeClassCMn
objdump -r --macho main.o
...
Relocation information (__TEXT,__swift5_types) 4 entries
address  pcrel length extern type    scattered symbolnum/value
00000000 False long   True   SUB     False     l_$s4main9SomeClassCHn
00000000 False long   True   UNSIGND False     _$s4main9SomeClassCMn

For what it's worth, stripping type metadata will break Swift Testing (although we are working on that.)

1 Like

i tested your example and changed -Xfrontend -lto=llvm-thin to just -lto=llvm-thin and then the symbols appear to be removed. not sure if passing directly to the frontend like that should produce a warning/error if it's unsupported, but also haven't looked very closely into how the options flow through the various internal bits.

1 Like

That did the trick, thanks!

1 Like

FWIW, the -conditional-runtime-records and -internalize-at-link should be considered highly experimental, and I'm aware of even more situations where they will break things (we could even say miscompile).

If you want to eliminate metadata from binaries, I recommend Embedded Swift.

Hm, I don't see any open issues on GitHub for -internalize-at-link. If there are known cases where it doesn't work as expected it might be worth opening an issue for each of them? So the community could see them (and tackle some).

FWIW, we've been using -conditional-runtime-records and -internalize-at-link in production for all of our Swift code for a couple years now and have yet to run into any issues related to it.

2 Likes